From 90889c12d8db8a0dc745fc111b25142e41a21d94 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 31 Mar 2026 08:09:00 -0500 Subject: [PATCH] fix(db): make document revision migration replay-safe --- packages/db/src/client.test.ts | 72 +++++++++++++++++++ .../src/migrations/0046_smooth_sentinels.sql | 12 ++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/packages/db/src/client.test.ts b/packages/db/src/client.test.ts index 622130ac..9b4c46ea 100644 --- a/packages/db/src/client.test.ts +++ b/packages/db/src/client.test.ts @@ -169,4 +169,76 @@ describeEmbeddedPostgres("applyPendingMigrations", () => { }, 20_000, ); + + it( + "replays migration 0046 safely when document revision columns already exist", + async () => { + const connectionString = await createTempDatabase(); + + await applyPendingMigrations(connectionString); + + const sql = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const smoothSentinelsHash = await migrationHash("0046_smooth_sentinels.sql"); + + await sql.unsafe( + `DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${smoothSentinelsHash}'`, + ); + + const columns = await sql.unsafe<{ column_name: string; is_nullable: string; column_default: string | null }[]>( + ` + SELECT column_name, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'document_revisions' + AND column_name IN ('title', 'format') + ORDER BY column_name + `, + ); + expect(columns).toHaveLength(2); + } finally { + await sql.end(); + } + + const pendingState = await inspectMigrations(connectionString); + expect(pendingState).toMatchObject({ + status: "needsMigrations", + pendingMigrations: ["0046_smooth_sentinels.sql"], + reason: "pending-migrations", + }); + + await applyPendingMigrations(connectionString); + + const finalState = await inspectMigrations(connectionString); + expect(finalState.status).toBe("upToDate"); + + const verifySql = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const columns = await verifySql.unsafe<{ column_name: string; is_nullable: string; column_default: string | null }[]>( + ` + SELECT column_name, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'document_revisions' + AND column_name IN ('title', 'format') + ORDER BY column_name + `, + ); + expect(columns).toEqual([ + expect.objectContaining({ + column_name: "format", + is_nullable: "NO", + }), + expect.objectContaining({ + column_name: "title", + is_nullable: "YES", + }), + ]); + expect(columns[0]?.column_default).toContain("'markdown'"); + } finally { + await verifySql.end(); + } + }, + 20_000, + ); }); diff --git a/packages/db/src/migrations/0046_smooth_sentinels.sql b/packages/db/src/migrations/0046_smooth_sentinels.sql index 858124c3..8ce9da44 100644 --- a/packages/db/src/migrations/0046_smooth_sentinels.sql +++ b/packages/db/src/migrations/0046_smooth_sentinels.sql @@ -1,9 +1,11 @@ -ALTER TABLE "document_revisions" ADD COLUMN "title" text;--> statement-breakpoint -ALTER TABLE "document_revisions" ADD COLUMN "format" text DEFAULT 'markdown' NOT NULL; +ALTER TABLE "document_revisions" ADD COLUMN IF NOT EXISTS "title" text;--> statement-breakpoint +ALTER TABLE "document_revisions" ADD COLUMN IF NOT EXISTS "format" text;--> statement-breakpoint +ALTER TABLE "document_revisions" ALTER COLUMN "format" SET DEFAULT 'markdown'; --> statement-breakpoint UPDATE "document_revisions" AS "dr" SET - "title" = "d"."title", - "format" = COALESCE("d"."format", 'markdown') + "title" = COALESCE("dr"."title", "d"."title"), + "format" = COALESCE("dr"."format", "d"."format", 'markdown') FROM "documents" AS "d" -WHERE "d"."id" = "dr"."document_id"; +WHERE "d"."id" = "dr"."document_id";--> statement-breakpoint +ALTER TABLE "document_revisions" ALTER COLUMN "format" SET NOT NULL;