diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index 3545259ffcd..4ba782b169c 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -53,6 +53,7 @@ import { execute, init, insert, + makePostgresDropsIdempotent, requireDrizzleKit, } from '@payloadcms/drizzle/postgres' import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core' @@ -104,7 +105,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj sqlExecute: string statements: string[] }): string => { - return `${sqlExecute}\n ${statements.join('\n')}\`)` + return `${sqlExecute}\n ${makePostgresDropsIdempotent(statements).join('\n')}\`)` } const executeMethod = 'execute' diff --git a/packages/db-vercel-postgres/src/index.ts b/packages/db-vercel-postgres/src/index.ts index 37e3761271e..7044a3fdfca 100644 --- a/packages/db-vercel-postgres/src/index.ts +++ b/packages/db-vercel-postgres/src/index.ts @@ -54,6 +54,7 @@ import { execute, init, insert, + makePostgresDropsIdempotent, requireDrizzleKit, } from '@payloadcms/drizzle/postgres' import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core' @@ -100,7 +101,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj `${sqlExecute}\n ${statements.join('\n')}\`)` + }) => `${sqlExecute}\n ${makePostgresDropsIdempotent(statements).join('\n')}\`)` const adapter = createDatabaseAdapter({ name: 'postgres', diff --git a/packages/drizzle/src/exports/postgres.ts b/packages/drizzle/src/exports/postgres.ts index 5110bebeea1..ec70cd79785 100644 --- a/packages/drizzle/src/exports/postgres.ts +++ b/packages/drizzle/src/exports/postgres.ts @@ -9,6 +9,7 @@ export { dropDatabase } from '../postgres/dropDatabase.js' export { execute } from '../postgres/execute.js' export { init } from '../postgres/init.js' export { insert } from '../postgres/insert.js' +export { makePostgresDropsIdempotent } from '../utilities/makePostgresDropsIdempotent.js' export { migratePostgresLocalizeStatus } from '../postgres/predefinedMigrations/localize-status/index.js' export { migratePostgresV2toV3 } from '../postgres/predefinedMigrations/v2-v3/index.js' export { requireDrizzleKit } from '../postgres/requireDrizzleKit.js' diff --git a/packages/drizzle/src/utilities/makePostgresDropsIdempotent.spec.ts b/packages/drizzle/src/utilities/makePostgresDropsIdempotent.spec.ts new file mode 100644 index 00000000000..594f6b4a288 --- /dev/null +++ b/packages/drizzle/src/utilities/makePostgresDropsIdempotent.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' + +import { makePostgresDropsIdempotent } from './makePostgresDropsIdempotent.js' + +describe('makePostgresDropsIdempotent', () => { + it('rewrites DROP CONSTRAINT to DROP CONSTRAINT IF EXISTS', () => { + const input = ['ALTER TABLE "posts_rels" DROP CONSTRAINT "posts_rels_posts_fk";'] + expect(makePostgresDropsIdempotent(input)).toEqual([ + 'ALTER TABLE "posts_rels" DROP CONSTRAINT IF EXISTS "posts_rels_posts_fk";', + ]) + }) + + it('rewrites DROP INDEX to DROP INDEX IF EXISTS', () => { + const input = ['DROP INDEX "posts_title_idx";'] + expect(makePostgresDropsIdempotent(input)).toEqual(['DROP INDEX IF EXISTS "posts_title_idx";']) + }) + + it('does not double-rewrite statements that already use IF EXISTS', () => { + const input = [ + 'ALTER TABLE "posts_rels" DROP CONSTRAINT IF EXISTS "posts_rels_posts_fk";', + 'DROP INDEX IF EXISTS "posts_title_idx";', + ] + expect(makePostgresDropsIdempotent(input)).toEqual(input) + }) + + it('leaves CREATE INDEX and other DDL untouched', () => { + const input = [ + 'CREATE INDEX IF NOT EXISTS "posts_title_idx" ON "posts" USING btree ("title");', + 'DROP TABLE "posts_rels" CASCADE;', + 'ALTER TABLE "posts" ADD COLUMN "slug" varchar;', + ] + expect(makePostgresDropsIdempotent(input)).toEqual(input) + }) + + it('handles multiple drops in a single statement', () => { + const input = [ + 'ALTER TABLE "posts_rels" DROP CONSTRAINT "posts_rels_posts_fk", DROP CONSTRAINT "posts_rels_categories_fk";', + ] + expect(makePostgresDropsIdempotent(input)).toEqual([ + 'ALTER TABLE "posts_rels" DROP CONSTRAINT IF EXISTS "posts_rels_posts_fk", DROP CONSTRAINT IF EXISTS "posts_rels_categories_fk";', + ]) + }) +}) diff --git a/packages/drizzle/src/utilities/makePostgresDropsIdempotent.ts b/packages/drizzle/src/utilities/makePostgresDropsIdempotent.ts new file mode 100644 index 00000000000..1966d3e2b33 --- /dev/null +++ b/packages/drizzle/src/utilities/makePostgresDropsIdempotent.ts @@ -0,0 +1,21 @@ +/** + * Rewrites `DROP CONSTRAINT` and `DROP INDEX` statements emitted by drizzle-kit so they + * include `IF EXISTS`. Postgres resolves dependent foreign keys when a referenced table + * is dropped with `CASCADE`, which causes follow-up explicit drops in the same migration + * to fail. Making these drops idempotent keeps the generated up/down migrations runnable + * end-to-end. + * + * Only rewrites statements that don't already contain `IF EXISTS` to stay safe against + * future drizzle-kit output changes. + */ +export const makePostgresDropsIdempotent = (statements: string[]): string[] => + statements.map((statement) => { + let next = statement.replace( + /\bDROP CONSTRAINT\b(?! IF EXISTS\b)/gi, + 'DROP CONSTRAINT IF EXISTS', + ) + + next = next.replace(/\bDROP INDEX\b(?! IF EXISTS\b)/gi, 'DROP INDEX IF EXISTS') + + return next + })