diff --git a/drizzle-kit/package.json b/drizzle-kit/package.json index 8a887914b3..de75985cd9 100644 --- a/drizzle-kit/package.json +++ b/drizzle-kit/package.json @@ -47,7 +47,9 @@ "build:ext": "rm -rf ./dist && vitest run bin.test && vitest run ./tests/postgres/ && vitest run ./tests/sqlite && vitest run ./tests/mysql && tsx build.ext.ts", "pack": "cp package.json README.md dist/ && (cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz", "pack:artifact": "pnpm run pack", + "pack:replit": "pnpm run build && tsx scripts/prepare-replit-package.ts && (cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz", "publish": "npm publish package.tgz", + "publish:replit": "pnpm run pack:replit && npm publish package.tgz --provenance=false", "test:postgres": "vitest run ./postgres/", "test:other": "vitest run ./mysql/ ./sqlite/ ./other", "test:cockroach": "vitest run ./cockroach", @@ -141,6 +143,18 @@ "types": "./index.d.mts", "default": "./index.mjs" }, + "./api": { + "import": { + "types": "./api.d.mts", + "default": "./api.mjs" + }, + "require": { + "types": "./api.d.ts", + "default": "./api.js" + }, + "types": "./api.d.mts", + "default": "./api.mjs" + }, "./api-postgres": { "import": { "types": "./api-postgres.d.mts", diff --git a/drizzle-kit/scripts/build.ts b/drizzle-kit/scripts/build.ts index 9f02ac80e1..34354541d5 100644 --- a/drizzle-kit/scripts/build.ts +++ b/drizzle-kit/scripts/build.ts @@ -115,6 +115,7 @@ async function buildDeclarations() { await tsdown({ entry: { + api: './src/ext/api.ts', 'api-postgres': './src/ext/api-postgres.ts', 'api-mysql': './src/ext/api-mysql.ts', 'api-sqlite': './src/ext/api-sqlite.ts', @@ -152,6 +153,7 @@ async function copyDeclarationsAndCleanTemp() { async function postProcessApiFiles() { const apiFiles = [ + 'dist/api.js', 'dist/api-postgres.js', 'dist/api-mysql.js', 'dist/api-sqlite.js', @@ -188,6 +190,18 @@ async function main() { outputName: 'index.mjs', format: 'esm', }), + buildBundle({ + name: 'api-cjs', + input: './src/ext/api.ts', + outputName: 'api.js', + format: 'cjs', + }), + buildBundle({ + name: 'api-esm', + input: './src/ext/api.ts', + outputName: 'api.mjs', + format: 'esm', + }), buildBundle({ name: 'api-postgres-cjs', input: './src/ext/api-postgres.ts', diff --git a/drizzle-kit/scripts/prepare-replit-package.ts b/drizzle-kit/scripts/prepare-replit-package.ts new file mode 100644 index 0000000000..f80c6cb2a3 --- /dev/null +++ b/drizzle-kit/scripts/prepare-replit-package.ts @@ -0,0 +1,32 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +async function main() { + const packagePath = 'dist/package.json'; + const parsed: unknown = JSON.parse(await readFile(packagePath, 'utf8')); + + if ( + typeof parsed !== 'object' + || parsed === null + || !('name' in parsed) + || parsed.name !== 'drizzle-kit' + || !('dependencies' in parsed) + || typeof parsed.dependencies !== 'object' + || parsed.dependencies === null + || !('@drizzle-team/brocli' in parsed.dependencies) + ) { + throw new Error('Expected the built drizzle-kit package manifest'); + } + const dependencies = Object.fromEntries( + Object.entries(parsed.dependencies).filter(([name]) => name !== '@drizzle-team/brocli'), + ); + + await writeFile( + packagePath, + `${JSON.stringify({ ...parsed, name: '@drizzle-team/drizzle-kit', dependencies }, null, '\t')}\n`, + ); +} + +main().catch((error: unknown) => { + console.error(error); + process.exit(1); +}); diff --git a/drizzle-kit/src/dialects/postgres/convertor.ts b/drizzle-kit/src/dialects/postgres/convertor.ts index 85cf5a7829..c72e4aae10 100644 --- a/drizzle-kit/src/dialects/postgres/convertor.ts +++ b/drizzle-kit/src/dialects/postgres/convertor.ts @@ -612,10 +612,10 @@ const recreateFKConvertor = convertor('recreate_fk', (st) => { ? `"${fk.schema}"."${fk.table}"` : `"${fk.table}"`; - const onDeleteStatement = fk.onDelete !== 'NO ACTION' + const onDeleteStatement = fk.onDelete && !isDefaultAction(fk.onDelete) ? ` ON DELETE ${fk.onDelete}` : ''; - const onUpdateStatement = fk.onUpdate !== 'NO ACTION' + const onUpdateStatement = fk.onUpdate && !isDefaultAction(fk.onUpdate) ? ` ON UPDATE ${fk.onUpdate}` : ''; @@ -628,7 +628,7 @@ const recreateFKConvertor = convertor('recreate_fk', (st) => { ? `"${fk.schemaTo}"."${fk.tableTo}"` : `"${fk.tableTo}"`; - let sql = `ALTER TABLE ${key} DROP CONSTRAINT "${fk.name}", `; + let sql = `ALTER TABLE ${key} DROP CONSTRAINT IF EXISTS "${fk.name}", `; sql += `ADD CONSTRAINT "${fk.name}" FOREIGN KEY (${fromColumnsString}) `; sql += `REFERENCES ${tableToNameWithSchema}(${toColumnsString})${onDeleteStatement}${onUpdateStatement};`; @@ -642,7 +642,7 @@ const dropForeignKeyConvertor = convertor('drop_fk', (st) => { ? `"${schema}"."${table}"` : `"${table}"`; - return `ALTER TABLE ${tableNameWithSchema} DROP CONSTRAINT "${name}";`; + return `ALTER TABLE ${tableNameWithSchema} DROP CONSTRAINT IF EXISTS "${name}";`; }); const addCheckConvertor = convertor('add_check', (st) => { diff --git a/drizzle-kit/src/ext/api.ts b/drizzle-kit/src/ext/api.ts index 52dfb8dfb1..5c4d260900 100644 --- a/drizzle-kit/src/ext/api.ts +++ b/drizzle-kit/src/ext/api.ts @@ -1,345 +1,189 @@ -// import { LibSQLDatabase } from 'drizzle-orm/libsql'; -// import type { MySql2Database } from 'drizzle-orm/mysql2'; -// import { PgDatabase } from 'drizzle-orm/pg-core'; -// import { SingleStoreDriverDatabase } from 'drizzle-orm/singlestore'; -// import { introspect as postgresIntrospect } from '../cli/commands/pull-postgres'; -// import { sqliteIntrospect } from '../cli/commands/pull-sqlite'; -// import { suggestions } from '../cli/commands/push-postgres'; -// import { updateUpToV6 as upPgV6, updateUpToV7 as upPgV7 } from '../cli/commands/up-postgres'; -// import { resolver } from '../cli/prompts'; -// import type { CasingType } from '../cli/validations/common'; -// import { ProgressView, schemaError, schemaWarning } from '../cli/views'; -// import { fromDrizzleSchema, fromExports } from '../dialects/postgres/drizzle'; -// import { PostgresSnapshot, toJsonSnapshot } from '../dialects/postgres/snapshot'; -// import type { Config } from '../index'; -// import { originUUID } from '../utils'; -// import type { DB, SQLiteDB } from '../utils'; -// import { getTablesFilterByExtensions } from './extensions/getTablesFilterByExtensions'; - -// import * as postgres from './api-postgres'; - -// SQLite - -// TODO commented this because of build error -// export const generateSQLiteDrizzleJson = async ( -// imports: Record, -// prevId?: string, -// casing?: CasingType, -// ): Promise => { -// const { prepareFromExports } = await import('./dialects/sqlite/imports'); - -// const prepared = prepareFromExports(imports); - -// const id = randomUUID(); - -// const snapshot = fromDrizzleSchema(prepared.tables, prepared.views, casing); - -// return { -// ...snapshot, -// id, -// prevId: prevId ?? originUUID, -// }; -// }; - -// export const generateSQLiteMigration = async ( -// prev: DrizzleSQLiteSnapshotJSON, -// cur: DrizzleSQLiteSnapshotJSON, -// ) => { -// const { applySqliteSnapshotsDiff } = await import('./dialects/sqlite/diff'); - -// const validatedPrev = sqliteSchema.parse(prev); -// const validatedCur = sqliteSchema.parse(cur); - -// const squashedPrev = squashSqliteScheme(validatedPrev); -// const squashedCur = squashSqliteScheme(validatedCur); - -// const { sqlStatements } = await applySqliteSnapshotsDiff( -// squashedPrev, -// squashedCur, -// tablesResolver, -// columnsResolver, -// sqliteViewsResolver, -// validatedPrev, -// validatedCur, -// ); - -// return sqlStatements; -// }; - -// export const pushSQLiteSchema = async ( -// imports: Record, -// drizzleInstance: LibSQLDatabase, -// ) => { -// const { applySqliteSnapshotsDiff } = await import('./dialects/sqlite/diff'); -// const { sql } = await import('drizzle-orm'); - -// const db: SQLiteDB = { -// query: async (query: string, params?: any[]) => { -// const res = drizzleInstance.all(sql.raw(query)); -// return res; -// }, -// run: async (query: string) => { -// return Promise.resolve(drizzleInstance.run(sql.raw(query))).then( -// () => {}, -// ); -// }, -// }; - -// const cur = await generateSQLiteDrizzleJson(imports); -// const progress = new ProgressView( -// 'Pulling schema from database...', -// 'Pulling schema from database...', -// ); - -// const { schema: prev } = await sqliteIntrospect(db, [], progress); - -// const validatedPrev = sqliteSchema.parse(prev); -// const validatedCur = sqliteSchema.parse(cur); - -// const squashedPrev = squashSqliteScheme(validatedPrev, 'push'); -// const squashedCur = squashSqliteScheme(validatedCur, 'push'); - -// const { statements, _meta } = await applySqliteSnapshotsDiff( -// squashedPrev, -// squashedCur, -// tablesResolver, -// columnsResolver, -// sqliteViewsResolver, -// validatedPrev, -// validatedCur, -// 'push', -// ); - -// const { shouldAskForApprove, statementsToExecute, infoToPrint } = await logSuggestionsAndReturn( -// db, -// statements, -// squashedPrev, -// squashedCur, -// _meta!, -// ); - -// return { -// hasDataLoss: shouldAskForApprove, -// warnings: infoToPrint, -// statementsToExecute, -// apply: async () => { -// for (const dStmnt of statementsToExecute) { -// await db.query(dStmnt); -// } -// }, -// }; -// }; - -// MySQL -// TODO commented this because of build error -// export const generateMySQLDrizzleJson = async ( -// imports: Record, -// prevId?: string, -// casing?: CasingType, -// ): Promise => { -// const { prepareFromExports } = await import('./serializer/mysqlImports'); - -// const prepared = prepareFromExports(imports); - -// const id = randomUUID(); - -// const snapshot = generateMySqlSnapshot(prepared.tables, prepared.views, casing); - -// return { -// ...snapshot, -// id, -// prevId: prevId ?? originUUID, -// }; -// }; - -// export const generateMySQLMigration = async ( -// prev: DrizzleMySQLSnapshotJSON, -// cur: DrizzleMySQLSnapshotJSON, -// ) => { -// const { ddlDiff: applyMysqlSnapshotsDiff } = await import('./dialects/mysql/mysql'); - -// const validatedPrev = mysqlSchema.parse(prev); -// const validatedCur = mysqlSchema.parse(cur); - -// const squashedPrev = squashMysqlScheme(validatedPrev); -// const squashedCur = squashMysqlScheme(validatedCur); - -// const { sqlStatements } = await applyMysqlSnapshotsDiff( -// squashedPrev, -// squashedCur, -// tablesResolver, -// columnsResolver, -// mySqlViewsResolver, -// uniqueResolver, -// validatedPrev, -// validatedCur, -// ); - -// return sqlStatements; -// }; - -// export const pushMySQLSchema = async ( -// imports: Record, -// drizzleInstance: MySql2Database, -// databaseName: string, -// ) => { -// const { ddlDiff: applyMysqlSnapshotsDiff } = await import('./dialects/mysql/mysql'); -// const { logSuggestionsAndReturn } = await import( -// './cli/commands/mysqlPushUtils' -// ); -// const { mysqlPushIntrospect } = await import( -// './cli/commands/pull-mysql' -// ); -// const { sql } = await import('drizzle-orm'); - -// const db: DB = { -// query: async (query: string, params?: any[]) => { -// const res = await drizzleInstance.execute(sql.raw(query)); -// return res[0] as unknown as any[]; -// }, -// }; -// const cur = await generateMySQLDrizzleJson(imports); -// const { schema: prev } = await mysqlPushIntrospect(db, databaseName, []); - -// const validatedPrev = mysqlSchema.parse(prev); -// const validatedCur = mysqlSchema.parse(cur); - -// const squashedPrev = squashMysqlScheme(validatedPrev); -// const squashedCur = squashMysqlScheme(validatedCur); - -// const { statements } = await applyMysqlSnapshotsDiff( -// squashedPrev, -// squashedCur, -// tablesResolver, -// columnsResolver, -// mySqlViewsResolver, -// uniqueResolver, -// validatedPrev, -// validatedCur, -// 'push', -// ); - -// const { shouldAskForApprove, statementsToExecute, infoToPrint } = await logSuggestionsAndReturn( -// db, -// statements, -// validatedCur, -// ); - -// return { -// hasDataLoss: shouldAskForApprove, -// warnings: infoToPrint, -// statementsToExecute, -// apply: async () => { -// for (const dStmnt of statementsToExecute) { -// await db.query(dStmnt); -// } -// }, -// }; -// }; - -// SingleStore -// TODO commented this because of build error -// export const generateSingleStoreDrizzleJson = async ( -// imports: Record, -// prevId?: string, -// casing?: CasingType, -// ): Promise => { -// const { prepareFromExports } = await import('./serializer/singlestoreImports'); - -// const prepared = prepareFromExports(imports); - -// const id = randomUUID(); - -// const snapshot = generateSingleStoreSnapshot(prepared.tables, /* prepared.views, */ casing); - -// return { -// ...snapshot, -// id, -// prevId: prevId ?? originUUID, -// }; -// }; - -// export const generateSingleStoreMigration = async ( -// prev: DrizzleSingleStoreSnapshotJSON, -// cur: DrizzleSingleStoreSnapshotJSON, -// ) => { -// const { applySingleStoreSnapshotsDiff } = await import('./snapshot-differ/singlestore'); - -// const validatedPrev = singlestoreSchema.parse(prev); -// const validatedCur = singlestoreSchema.parse(cur); - -// const squashedPrev = squashSingleStoreScheme(validatedPrev); -// const squashedCur = squashSingleStoreScheme(validatedCur); - -// const { sqlStatements } = await applySingleStoreSnapshotsDiff( -// squashedPrev, -// squashedCur, -// tablesResolver, -// columnsResolver, -// /* singleStoreViewsResolver, */ -// validatedPrev, -// validatedCur, -// 'push', -// ); - -// return sqlStatements; -// }; - -// export const pushSingleStoreSchema = async ( -// imports: Record, -// drizzleInstance: SingleStoreDriverDatabase, -// databaseName: string, -// ) => { -// const { applySingleStoreSnapshotsDiff } = await import('./snapshot-differ/singlestore'); -// const { logSuggestionsAndReturn } = await import( -// './cli/commands/singlestorePushUtils' -// ); -// const { singlestorePushIntrospect } = await import( -// './cli/commands/pull-singlestore' -// ); -// const { sql } = await import('drizzle-orm'); - -// const db: DB = { -// query: async (query: string) => { -// const res = await drizzleInstance.execute(sql.raw(query)); -// return res[0] as unknown as any[]; -// }, -// }; -// const cur = await generateSingleStoreDrizzleJson(imports); -// const { schema: prev } = await singlestorePushIntrospect(db, databaseName, []); - -// const validatedPrev = singlestoreSchema.parse(prev); -// const validatedCur = singlestoreSchema.parse(cur); - -// const squashedPrev = squashSingleStoreScheme(validatedPrev); -// const squashedCur = squashSingleStoreScheme(validatedCur); - -// const { statements } = await applySingleStoreSnapshotsDiff( -// squashedPrev, -// squashedCur, -// tablesResolver, -// columnsResolver, -// /* singleStoreViewsResolver, */ -// validatedPrev, -// validatedCur, -// 'push', -// ); - -// const { shouldAskForApprove, statementsToExecute, infoToPrint } = await logSuggestionsAndReturn( -// db, -// statements, -// validatedCur, -// validatedPrev, -// ); - -// return { -// hasDataLoss: shouldAskForApprove, -// warnings: infoToPrint, -// statementsToExecute, -// apply: async () => { -// for (const dStmnt of statementsToExecute) { -// await db.query(dStmnt); -// } -// }, -// }; -// }; +import type { Resolver } from '../dialects/common'; +import type { + CheckConstraint, + Column, + Enum, + ForeignKey, + Identity, + Index, + IndexColumn, + InterimColumn, + InterimIndex, + InterimSchema, + Policy, + PostgresDDL, + PostgresEntities, + PostgresEntity, + PrimaryKey, + Privilege, + Role, + Schema, + Sequence, + Table, + UniqueConstraint, + View, + ViewColumn, +} from '../dialects/postgres/ddl'; +import { createDDL, interimToDDL } from '../dialects/postgres/ddl'; +import { ddlDiff } from '../dialects/postgres/diff'; +import { fromDatabaseForDrizzle } from '../dialects/postgres/introspect'; +import type { JsonStatement } from '../dialects/postgres/statements'; +import { prepareEntityFilter } from '../dialects/pull-utils'; + +export type { + CheckConstraint, + Column, + Enum, + ForeignKey, + Identity, + Index, + IndexColumn, + InterimColumn, + InterimIndex, + InterimSchema, + JsonStatement, + Policy, + PostgresDDL, + PostgresEntities, + PostgresEntity, + PrimaryKey, + Privilege, + Resolver, + Role, + Schema, + Sequence, + Table, + UniqueConstraint, + View, + ViewColumn, +}; + +export type PgQuery = { + query: (sql: string, params?: any[]) => Promise; +}; + +export type PgIntrospectionFilters = { + tables?: string[]; + schemas?: string[]; +}; + +export type PgMigrationResolvers = { + schemas?: Resolver; + enums?: Resolver; + sequences?: Resolver; + policies?: Resolver; + roles?: Resolver; + privileges?: Resolver; + tables?: Resolver; + columns?: Resolver; + views?: Resolver; + uniques?: Resolver; + indexes?: Resolver; + checks?: Resolver; + primaryKeys?: Resolver; + foreignKeys?: Resolver; +}; + +export type PgMigrationPlan = { + statements: JsonStatement[]; + sqlStatements: string[]; + groupedStatements: { jsonStatement: JsonStatement; sqlStatements: string[] }[]; + renames: string[]; +}; + +export type CreatePgMigrationPlanOptions = { + current: PostgresDDL; + desired: PostgresDDL; + resolvers?: PgMigrationResolvers; + mode?: 'default' | 'push'; +}; + +const defaultMigrationsConfig = { + schema: 'drizzle', + table: '__drizzle_migrations', +}; + +const passthroughResolver = async ({ + created, + deleted, +}: Parameters>[0]) => { + return { created, deleted, renamedOrMoved: [] }; +}; + +const resolverOrDefault = ( + resolver: Resolver | undefined, +): Resolver => resolver ?? passthroughResolver; + +const clonePostgresDDL = (source: PostgresDDL): PostgresDDL => { + const clone = createDDL(); + clone.entities.pushAll(source.entities.list().map((entity) => structuredClone(entity))); + return clone; +}; + +export const createEmptyPgSchema = (): InterimSchema => ({ + schemas: [], + enums: [], + tables: [], + columns: [], + indexes: [], + pks: [], + fks: [], + uniques: [], + checks: [], + sequences: [], + roles: [], + privileges: [], + policies: [], + views: [], + viewColumns: [], +}); + +export const introspectPgDB = async ( + query: PgQuery, + filters: PgIntrospectionFilters = {}, +): Promise => { + const filter = prepareEntityFilter('postgresql', { + tables: filters.tables ?? [], + schemas: filters.schemas ?? [], + entities: undefined, + extensions: [], + }, []); + + return fromDatabaseForDrizzle(query, filter, () => {}, defaultMigrationsConfig); +}; + +export const toPostgresDDL = (schema: InterimSchema): PostgresDDL => { + const { ddl, errors } = interimToDDL(schema); + if (errors.length > 0) { + throw new Error(`Failed to convert Postgres schema: ${JSON.stringify(errors)}`); + } + + return ddl; +}; + +export const createPgMigrationPlan = async ({ + current, + desired, + resolvers = {}, + mode = 'default', +}: CreatePgMigrationPlanOptions): Promise => { + return ddlDiff( + clonePostgresDDL(current), + clonePostgresDDL(desired), + resolverOrDefault(resolvers.schemas), + resolverOrDefault(resolvers.enums), + resolverOrDefault(resolvers.sequences), + resolverOrDefault(resolvers.policies), + resolverOrDefault(resolvers.roles), + resolverOrDefault(resolvers.privileges), + resolverOrDefault(resolvers.tables), + resolverOrDefault(resolvers.columns), + resolverOrDefault(resolvers.views), + resolverOrDefault(resolvers.uniques), + resolverOrDefault(resolvers.indexes), + resolverOrDefault(resolvers.checks), + resolverOrDefault(resolvers.primaryKeys), + resolverOrDefault(resolvers.foreignKeys), + mode, + ); +}; diff --git a/drizzle-kit/tests/other/replit-api.test.ts b/drizzle-kit/tests/other/replit-api.test.ts new file mode 100644 index 0000000000..3c03599c2e --- /dev/null +++ b/drizzle-kit/tests/other/replit-api.test.ts @@ -0,0 +1,286 @@ +import { describe, expect, test, vi } from 'vitest'; +import { createEmptyPgSchema, createPgMigrationPlan, introspectPgDB, toPostgresDDL } from '../../src/ext/api'; +import type { Resolver } from '../../src/ext/api'; + +function schemaWithTable(name = 'users') { + const ddl = toPostgresDDL(createEmptyPgSchema()); + ddl.tables.push({ + schema: 'public', + name, + isRlsEnabled: false, + }); + ddl.columns.push({ + schema: 'public', + table: name, + name: 'id', + type: 'integer', + typeSchema: null, + notNull: true, + dimensions: 0, + default: null, + generated: null, + identity: null, + }); + return ddl; +} + +function schemaWithEveryResolvableEntity() { + const ddl = schemaWithTable(); + ddl.indexes.push({ + schema: 'public', + table: 'users', + name: 'users_id_idx', + nameExplicit: true, + columns: [{ + value: 'id', + isExpression: false, + asc: true, + nullsFirst: false, + opclass: null, + }], + isUnique: false, + where: null, + with: '', + method: 'btree', + concurrently: false, + }); + ddl.pks.push({ + schema: 'public', + table: 'users', + name: 'users_pkey', + nameExplicit: true, + columns: ['id'], + }); + ddl.fks.push({ + schema: 'public', + table: 'users', + name: 'users_id_users_id_fkey', + nameExplicit: true, + columns: ['id'], + schemaTo: 'public', + tableTo: 'users', + columnsTo: ['id'], + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }); + ddl.uniques.push({ + schema: 'public', + table: 'users', + name: 'users_id_key', + nameExplicit: true, + columns: ['id'], + nullsNotDistinct: false, + }); + ddl.checks.push({ + schema: 'public', + table: 'users', + name: 'users_id_check', + value: 'id > 0', + }); + ddl.policies.push({ + schema: 'public', + table: 'users', + name: 'users_select', + as: 'PERMISSIVE', + for: 'SELECT', + roles: ['public'], + using: null, + withCheck: null, + }); + return ddl; +} + +function schemaWithForeignKey(onDelete: 'NO ACTION' | 'CASCADE' | null | undefined) { + const ddl = toPostgresDDL(createEmptyPgSchema()); + ddl.tables.push({ schema: 'public', name: 'parents', isRlsEnabled: false }); + ddl.tables.push({ schema: 'public', name: 'children', isRlsEnabled: false }); + if (onDelete !== undefined) { + ddl.fks.push({ + schema: 'public', + table: 'children', + name: 'children_parent_id_parents_id_fkey', + nameExplicit: false, + columns: ['parent_id'], + schemaTo: 'public', + tableTo: 'parents', + columnsTo: ['id'], + onDelete, + onUpdate: 'NO ACTION', + }); + } + return ddl; +} + +describe('headless Postgres API', () => { + test('introspects through a structural query interface', async () => { + const query = vi.fn(async () => []); + + const schema = await introspectPgDB({ query }, { + tables: ['users'], + schemas: ['app'], + }); + + expect(schema).toEqual(createEmptyPgSchema()); + expect(query).toHaveBeenCalled(); + }); + + test('maps every named resolver category', async () => { + const calls = new Set(); + const resolver = (name: string): Resolver => async ({ created, deleted }) => { + calls.add(name); + return { created, deleted, renamedOrMoved: [] }; + }; + + await createPgMigrationPlan({ + current: toPostgresDDL(createEmptyPgSchema()), + desired: schemaWithEveryResolvableEntity(), + resolvers: { + schemas: resolver('schemas'), + enums: resolver('enums'), + sequences: resolver('sequences'), + policies: resolver('policies'), + roles: resolver('roles'), + privileges: resolver('privileges'), + tables: resolver('tables'), + columns: resolver('columns'), + views: resolver('views'), + uniques: resolver('uniques'), + indexes: resolver('indexes'), + checks: resolver('checks'), + primaryKeys: resolver('primaryKeys'), + foreignKeys: resolver('foreignKeys'), + }, + mode: 'push', + }); + + expect(calls).toEqual( + new Set([ + 'schemas', + 'enums', + 'sequences', + 'policies', + 'roles', + 'privileges', + 'tables', + 'columns', + 'views', + 'uniques', + 'indexes', + 'checks', + 'primaryKeys', + 'foreignKeys', + ]), + ); + }); + + test('creates a plan with default pass-through resolvers', async () => { + const tables = vi.fn(async ({ created, deleted }) => ({ + created, + deleted, + renamedOrMoved: [], + })); + + const plan = await createPgMigrationPlan({ + current: toPostgresDDL(createEmptyPgSchema()), + desired: schemaWithTable(), + resolvers: { tables }, + mode: 'push', + }); + + expect(tables).toHaveBeenCalledOnce(); + expect(plan.sqlStatements).toEqual([ + expect.stringContaining('CREATE TABLE "users"'), + ]); + }); + + test('uses a named resolver to rename tables', async () => { + const plan = await createPgMigrationPlan({ + current: schemaWithTable('users'), + desired: schemaWithTable('accounts'), + resolvers: { + tables: async ({ created, deleted }) => ({ + created, + deleted, + renamedOrMoved: [{ from: deleted[0]!, to: created[0]! }], + }), + }, + mode: 'push', + }); + + expect(plan.sqlStatements).toContain('ALTER TABLE "users" RENAME TO "accounts";'); + }); + + test('does not mutate supplied DDL schemas', async () => { + const current = schemaWithTable('users'); + const desired = schemaWithTable('accounts'); + const currentBefore = structuredClone(current.entities.list()); + const desiredBefore = structuredClone(desired.entities.list()); + + await createPgMigrationPlan({ + current, + desired, + resolvers: { + tables: async ({ created, deleted }) => ({ + created, + deleted, + renamedOrMoved: [{ from: deleted[0]!, to: created[0]! }], + }), + }, + mode: 'push', + }); + + expect(current.entities.list()).toEqual(currentBefore); + expect(desired.entities.list()).toEqual(desiredBefore); + }); + + test('propagates resolver failures', async () => { + await expect(createPgMigrationPlan({ + current: toPostgresDDL(createEmptyPgSchema()), + desired: schemaWithTable(), + resolvers: { + tables: async () => { + throw new Error('resolver failed'); + }, + }, + mode: 'push', + })).rejects.toThrow('resolver failed'); + }); + + test('rejects an interim schema with conversion errors', () => { + const schema = createEmptyPgSchema(); + schema.tables.push( + { entityType: 'tables', schema: 'public', name: 'users', isRlsEnabled: false }, + { entityType: 'tables', schema: 'public', name: 'users', isRlsEnabled: false }, + ); + + expect(() => toPostgresDDL(schema)).toThrow('table_name_duplicate'); + }); + + test('uses IF EXISTS when dropping or recreating foreign keys', async () => { + const dropped = await createPgMigrationPlan({ + current: schemaWithForeignKey('NO ACTION'), + desired: schemaWithForeignKey(undefined), + mode: 'push', + }); + const recreated = await createPgMigrationPlan({ + current: schemaWithForeignKey('NO ACTION'), + desired: schemaWithForeignKey('CASCADE'), + mode: 'push', + }); + const resetToDefault = await createPgMigrationPlan({ + current: schemaWithForeignKey('CASCADE'), + desired: schemaWithForeignKey(null), + mode: 'push', + }); + + expect(dropped.sqlStatements).toContain( + 'ALTER TABLE "children" DROP CONSTRAINT IF EXISTS "children_parent_id_parents_id_fkey";', + ); + expect(recreated.sqlStatements).toContain( + 'ALTER TABLE "children" DROP CONSTRAINT IF EXISTS "children_parent_id_parents_id_fkey", ADD CONSTRAINT "children_parent_id_parents_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "parents"("id") ON DELETE CASCADE;', + ); + expect(resetToDefault.sqlStatements).toContain( + 'ALTER TABLE "children" DROP CONSTRAINT IF EXISTS "children_parent_id_parents_id_fkey", ADD CONSTRAINT "children_parent_id_parents_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "parents"("id");', + ); + }); +}); diff --git a/drizzle-kit/tests/postgres/pg-columns.test.ts b/drizzle-kit/tests/postgres/pg-columns.test.ts index 0cb501cae2..aebd0bcc04 100644 --- a/drizzle-kit/tests/postgres/pg-columns.test.ts +++ b/drizzle-kit/tests/postgres/pg-columns.test.ts @@ -367,8 +367,8 @@ test.skipIf(Date.now() < +new Date('2026-07-01'))('alter integer type to text ty const { sqlStatements: pst2 } = await push({ db, to: schema2 }); const expectedSt2 = [ - 'ALTER TABLE "sessions" DROP CONSTRAINT "sessions_userId_users_id_fkey";', - 'ALTER TABLE "content" DROP CONSTRAINT "content_userId_users_id_fkey";', + 'ALTER TABLE "sessions" DROP CONSTRAINT IF EXISTS "sessions_userId_users_id_fkey";', + 'ALTER TABLE "content" DROP CONSTRAINT IF EXISTS "content_userId_users_id_fkey";', 'ALTER TABLE "users" ALTER COLUMN "id" SET DATA TYPE text;', 'ALTER TABLE "sessions" ALTER COLUMN "userId" SET DATA TYPE text;', 'ALTER TABLE "content" ALTER COLUMN "userId" SET DATA TYPE text;', @@ -600,9 +600,9 @@ test('add multiple constraints #1', async (t) => { }); const st0 = [ - 'ALTER TABLE "ref1" DROP CONSTRAINT "ref1_id1_t1_id_fkey", ADD CONSTRAINT "ref1_id1_t1_id_fkey" FOREIGN KEY ("id1") REFERENCES "t1"("id") ON DELETE CASCADE;', - 'ALTER TABLE "ref1" DROP CONSTRAINT "ref1_id2_t2_id_fkey", ADD CONSTRAINT "ref1_id2_t2_id_fkey" FOREIGN KEY ("id2") REFERENCES "t2"("id") ON DELETE SET NULL;', - 'ALTER TABLE "ref1" DROP CONSTRAINT "ref1_id3_t3_id_fkey", ADD CONSTRAINT "ref1_id3_t3_id_fkey" FOREIGN KEY ("id3") REFERENCES "t3"("id") ON DELETE CASCADE;', + 'ALTER TABLE "ref1" DROP CONSTRAINT IF EXISTS "ref1_id1_t1_id_fkey", ADD CONSTRAINT "ref1_id1_t1_id_fkey" FOREIGN KEY ("id1") REFERENCES "t1"("id") ON DELETE CASCADE;', + 'ALTER TABLE "ref1" DROP CONSTRAINT IF EXISTS "ref1_id2_t2_id_fkey", ADD CONSTRAINT "ref1_id2_t2_id_fkey" FOREIGN KEY ("id2") REFERENCES "t2"("id") ON DELETE SET NULL;', + 'ALTER TABLE "ref1" DROP CONSTRAINT IF EXISTS "ref1_id3_t3_id_fkey", ADD CONSTRAINT "ref1_id3_t3_id_fkey" FOREIGN KEY ("id3") REFERENCES "t3"("id") ON DELETE CASCADE;', ]; expect(st).toStrictEqual(st0); expect(pst).toStrictEqual(st0); @@ -640,9 +640,9 @@ test('add multiple constraints #2', async (t) => { const { sqlStatements: pst } = await push({ db, to: schema2 }); const st0 = [ - 'ALTER TABLE "ref1" DROP CONSTRAINT "ref1_id1_t1_id1_fkey", ADD CONSTRAINT "ref1_id1_t1_id1_fkey" FOREIGN KEY ("id1") REFERENCES "t1"("id1") ON DELETE CASCADE;', - 'ALTER TABLE "ref1" DROP CONSTRAINT "ref1_id2_t1_id2_fkey", ADD CONSTRAINT "ref1_id2_t1_id2_fkey" FOREIGN KEY ("id2") REFERENCES "t1"("id2") ON DELETE SET NULL;', - 'ALTER TABLE "ref1" DROP CONSTRAINT "ref1_id3_t1_id3_fkey", ADD CONSTRAINT "ref1_id3_t1_id3_fkey" FOREIGN KEY ("id3") REFERENCES "t1"("id3") ON DELETE CASCADE;', + 'ALTER TABLE "ref1" DROP CONSTRAINT IF EXISTS "ref1_id1_t1_id1_fkey", ADD CONSTRAINT "ref1_id1_t1_id1_fkey" FOREIGN KEY ("id1") REFERENCES "t1"("id1") ON DELETE CASCADE;', + 'ALTER TABLE "ref1" DROP CONSTRAINT IF EXISTS "ref1_id2_t1_id2_fkey", ADD CONSTRAINT "ref1_id2_t1_id2_fkey" FOREIGN KEY ("id2") REFERENCES "t1"("id2") ON DELETE SET NULL;', + 'ALTER TABLE "ref1" DROP CONSTRAINT IF EXISTS "ref1_id3_t1_id3_fkey", ADD CONSTRAINT "ref1_id3_t1_id3_fkey" FOREIGN KEY ("id3") REFERENCES "t1"("id3") ON DELETE CASCADE;', ]; expect(st).toStrictEqual(st0); expect(pst).toStrictEqual(st0); @@ -691,9 +691,9 @@ test('add multiple constraints #3', async (t) => { }); const st0 = [ - 'ALTER TABLE "ref1" DROP CONSTRAINT "ref1_id_t1_id1_fkey", ADD CONSTRAINT "ref1_id_t1_id1_fkey" FOREIGN KEY ("id") REFERENCES "t1"("id1") ON DELETE CASCADE;', - 'ALTER TABLE "ref2" DROP CONSTRAINT "ref2_id_t1_id2_fkey", ADD CONSTRAINT "ref2_id_t1_id2_fkey" FOREIGN KEY ("id") REFERENCES "t1"("id2") ON DELETE SET NULL;', - 'ALTER TABLE "ref3" DROP CONSTRAINT "ref3_id_t1_id3_fkey", ADD CONSTRAINT "ref3_id_t1_id3_fkey" FOREIGN KEY ("id") REFERENCES "t1"("id3") ON DELETE CASCADE;', + 'ALTER TABLE "ref1" DROP CONSTRAINT IF EXISTS "ref1_id_t1_id1_fkey", ADD CONSTRAINT "ref1_id_t1_id1_fkey" FOREIGN KEY ("id") REFERENCES "t1"("id1") ON DELETE CASCADE;', + 'ALTER TABLE "ref2" DROP CONSTRAINT IF EXISTS "ref2_id_t1_id2_fkey", ADD CONSTRAINT "ref2_id_t1_id2_fkey" FOREIGN KEY ("id") REFERENCES "t1"("id2") ON DELETE SET NULL;', + 'ALTER TABLE "ref3" DROP CONSTRAINT IF EXISTS "ref3_id_t1_id3_fkey", ADD CONSTRAINT "ref3_id_t1_id3_fkey" FOREIGN KEY ("id") REFERENCES "t1"("id3") ON DELETE CASCADE;', ]; expect(st).toStrictEqual(st0); expect(pst).toStrictEqual(st0); diff --git a/drizzle-kit/tests/postgres/pg-constraints.test.ts b/drizzle-kit/tests/postgres/pg-constraints.test.ts index 3f487796a0..e4e22f8d96 100644 --- a/drizzle-kit/tests/postgres/pg-constraints.test.ts +++ b/drizzle-kit/tests/postgres/pg-constraints.test.ts @@ -1989,7 +1989,7 @@ test('fk #8', async () => { const { sqlStatements: pst } = await push({ db, to }); const e = [ - 'ALTER TABLE "users" DROP CONSTRAINT "users_id3_users_id1_fkey";', + 'ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "users_id3_users_id1_fkey";', 'ALTER TABLE "users" ADD CONSTRAINT "users_id3_users_id2_fkey" FOREIGN KEY ("id3") REFERENCES "users"("id2");', ]; expect(sqlStatements).toStrictEqual(e); @@ -2017,7 +2017,7 @@ test('fk #9', async () => { const { sqlStatements: pst } = await push({ db, to }); const e = [ - 'ALTER TABLE "users" DROP CONSTRAINT "fk1", ADD CONSTRAINT "fk1" FOREIGN KEY ("id3") REFERENCES "users"("id2");', + 'ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "fk1", ADD CONSTRAINT "fk1" FOREIGN KEY ("id3") REFERENCES "users"("id2");', ]; expect(sqlStatements).toStrictEqual(e); expect(pst).toStrictEqual(e); @@ -2071,7 +2071,7 @@ test('fk #11', async () => { const e = [ 'ALTER TABLE "users" RENAME TO "users2";', - 'ALTER TABLE "users2" DROP CONSTRAINT "users_id2_users_id1_fkey";', + 'ALTER TABLE "users2" DROP CONSTRAINT IF EXISTS "users_id2_users_id1_fkey";', ]; expect(sqlStatements).toStrictEqual(e); expect(pst).toStrictEqual(e); @@ -2126,8 +2126,8 @@ test('fk multistep #1', async () => { const { sqlStatements: st4 } = await diff(n3, sch3, []); const { sqlStatements: pst4 } = await push({ db, to: sch3 }); - expect(st4).toStrictEqual(['ALTER TABLE "users2" DROP CONSTRAINT "users_id2_users_id_fkey";']); - expect(pst4).toStrictEqual(['ALTER TABLE "users2" DROP CONSTRAINT "users_id2_users_id_fkey";']); + expect(st4).toStrictEqual(['ALTER TABLE "users2" DROP CONSTRAINT IF EXISTS "users_id2_users_id_fkey";']); + expect(pst4).toStrictEqual(['ALTER TABLE "users2" DROP CONSTRAINT IF EXISTS "users_id2_users_id_fkey";']); }); test('fk multistep #2', async () => { @@ -2223,7 +2223,7 @@ test('fk multistep #4', async () => { const { sqlStatements: st2 } = await diff(n1, schema2, []); const { sqlStatements: pst2 } = await push({ db, to: schema2 }); const expectedSt2 = [ - 'ALTER TABLE "bar" DROP CONSTRAINT "bar_fooId_foo_id_fkey";', + 'ALTER TABLE "bar" DROP CONSTRAINT IF EXISTS "bar_fooId_foo_id_fkey";', 'DROP TABLE "foo";', ]; expect(st2).toStrictEqual(expectedSt2); @@ -2370,7 +2370,7 @@ test('generated + fk', async (t) => { const { sqlStatements: pst } = await push({ db, to: schema2 }); expect(st).toStrictEqual([ - 'ALTER TABLE "table" DROP CONSTRAINT "table_bool_table_with_gen_bool_fkey";', + 'ALTER TABLE "table" DROP CONSTRAINT IF EXISTS "table_bool_table_with_gen_bool_fkey";', `ALTER TABLE \"table_with_gen\" DROP COLUMN \"bool\";`, `ALTER TABLE \"table_with_gen\" ADD COLUMN \"bool\" boolean GENERATED ALWAYS AS (("table_with_gen"."column2" is null)) STORED;`, 'ALTER TABLE "table_with_gen" ADD CONSTRAINT "table_with_gen_bool_key" UNIQUE("bool");', diff --git a/drizzle-kit/tests/postgres/pg-tables.test.ts b/drizzle-kit/tests/postgres/pg-tables.test.ts index 8a53a59586..bb6dca0291 100644 --- a/drizzle-kit/tests/postgres/pg-tables.test.ts +++ b/drizzle-kit/tests/postgres/pg-tables.test.ts @@ -859,7 +859,7 @@ test('drop tables with fk constraint #1', async () => { const { sqlStatements: pst2 } = await push({ db, to: {} }); const expectedSt2 = [ - 'ALTER TABLE "table2" DROP CONSTRAINT "table2_column2_table1_column1_fkey";', + 'ALTER TABLE "table2" DROP CONSTRAINT IF EXISTS "table2_column2_table1_column1_fkey";', 'DROP TABLE "table1";', 'DROP TABLE "table2";', ]; @@ -899,7 +899,7 @@ test('drop tables with fk constraint #2', async () => { const { sqlStatements: pst2 } = await push({ db, to: schema2 }); const expectedSt2 = [ - 'ALTER TABLE "table2" DROP CONSTRAINT "table2_column2_table1_column1_fkey";', + 'ALTER TABLE "table2" DROP CONSTRAINT IF EXISTS "table2_column2_table1_column1_fkey";', 'DROP TABLE "table1";', ]; expect(st2).toStrictEqual(expectedSt2);