@vibeorm/migrate 1.3.1 → 2.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +33 -66
  2. package/dist/ddl/drop-order.d.ts +25 -0
  3. package/dist/ddl/drop-order.d.ts.map +1 -0
  4. package/dist/ddl/mysql.d.ts +46 -0
  5. package/dist/ddl/mysql.d.ts.map +1 -0
  6. package/dist/ddl/postgres.d.ts +24 -0
  7. package/dist/ddl/postgres.d.ts.map +1 -0
  8. package/dist/ddl/render.d.ts +25 -0
  9. package/dist/ddl/render.d.ts.map +1 -0
  10. package/dist/ddl/rls.d.ts +40 -0
  11. package/dist/ddl/rls.d.ts.map +1 -0
  12. package/dist/ddl/sqlite.d.ts +79 -0
  13. package/dist/ddl/sqlite.d.ts.map +1 -0
  14. package/dist/differ.d.ts +41 -0
  15. package/dist/differ.d.ts.map +1 -0
  16. package/dist/down.d.ts +57 -0
  17. package/dist/down.d.ts.map +1 -0
  18. package/dist/enum-values.d.ts +32 -0
  19. package/dist/enum-values.d.ts.map +1 -0
  20. package/dist/execute.d.ts +124 -0
  21. package/dist/execute.d.ts.map +1 -0
  22. package/dist/execution.d.ts +168 -0
  23. package/dist/execution.d.ts.map +1 -0
  24. package/dist/extensions.d.ts +74 -0
  25. package/dist/extensions.d.ts.map +1 -0
  26. package/dist/index.d.ts +32 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +5688 -0
  29. package/dist/index.js.map +37 -0
  30. package/dist/introspect/mysql.d.ts +31 -0
  31. package/dist/introspect/mysql.d.ts.map +1 -0
  32. package/dist/introspect/postgres-rls.d.ts +41 -0
  33. package/dist/introspect/postgres-rls.d.ts.map +1 -0
  34. package/dist/introspect/postgres.d.ts +12 -0
  35. package/dist/introspect/postgres.d.ts.map +1 -0
  36. package/dist/introspect/shared.d.ts +78 -0
  37. package/dist/introspect/shared.d.ts.map +1 -0
  38. package/dist/introspect/sqlite.d.ts +53 -0
  39. package/dist/introspect/sqlite.d.ts.map +1 -0
  40. package/dist/journal-table.d.ts +15 -0
  41. package/dist/journal-table.d.ts.map +1 -0
  42. package/dist/mysql-types.d.ts +66 -0
  43. package/dist/mysql-types.d.ts.map +1 -0
  44. package/dist/normalize.d.ts +143 -0
  45. package/dist/normalize.d.ts.map +1 -0
  46. package/dist/postgres-types.d.ts +95 -0
  47. package/dist/postgres-types.d.ts.map +1 -0
  48. package/dist/push.d.ts +51 -0
  49. package/dist/push.d.ts.map +1 -0
  50. package/dist/recovery.d.ts +41 -0
  51. package/dist/recovery.d.ts.map +1 -0
  52. package/dist/relations.d.ts +40 -0
  53. package/dist/relations.d.ts.map +1 -0
  54. package/dist/renames.d.ts +58 -0
  55. package/dist/renames.d.ts.map +1 -0
  56. package/dist/runner.d.ts +181 -0
  57. package/dist/runner.d.ts.map +1 -0
  58. package/dist/sqlite-types.d.ts +101 -0
  59. package/dist/sqlite-types.d.ts.map +1 -0
  60. package/dist/types.d.ts +329 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/package.json +34 -23
  63. package/src/cascade-actions.ts +0 -88
  64. package/src/ddl-builder.ts +0 -415
  65. package/src/index.ts +0 -41
  66. package/src/introspector.ts +0 -684
  67. package/src/migration-runner.ts +0 -127
  68. package/src/relation-utils.ts +0 -79
  69. package/src/schema-differ.ts +0 -865
  70. package/src/schema-printer.ts +0 -259
  71. package/src/snapshot.ts +0 -141
  72. package/src/sql-utils.ts +0 -45
  73. package/src/types.ts +0 -13
@@ -1,865 +0,0 @@
1
- /**
2
- * Schema Differ — Diff Two Schema IRs → Migration Operations
3
- *
4
- * Compares a "previous" and "current" Schema IR and produces
5
- * an ordered list of DDL operations to migrate between them.
6
- */
7
-
8
- import type {
9
- Schema,
10
- Model,
11
- Field,
12
- ScalarField,
13
- EnumField,
14
- RelationField,
15
- Enum,
16
- UniqueConstraint,
17
- IndexDefinition,
18
- } from "@vibeorm/parser";
19
- import { PRISMA_TO_PG } from "@vibeorm/parser";
20
- import { buildFkActionClause, fkActionFingerprint } from "./cascade-actions.ts";
21
- import { detectImplicitM2MPairs } from "./relation-utils.ts";
22
-
23
- // ─── Types ────────────────────────────────────────────────────────
24
-
25
- export type DiffOperationType =
26
- | "createEnum"
27
- | "addEnumValue"
28
- | "removeEnumValue"
29
- | "dropEnum"
30
- | "createTable"
31
- | "dropTable"
32
- | "addColumn"
33
- | "dropColumn"
34
- | "alterColumnType"
35
- | "alterColumnNullability"
36
- | "alterColumnDefault"
37
- | "addUnique"
38
- | "dropUnique"
39
- | "addIndex"
40
- | "dropIndex"
41
- | "addForeignKey"
42
- | "dropForeignKey"
43
- | "createJoinTable"
44
- | "dropJoinTable";
45
-
46
- export type DiffOperation = {
47
- type: DiffOperationType;
48
- sql: string;
49
- /** Reverse SQL to undo this operation (for down migrations) */
50
- downSql: string;
51
- isDestructive: boolean;
52
- /** Whether this operation can be cleanly reversed without data loss */
53
- isReversible: boolean;
54
- description: string;
55
- };
56
-
57
- // ─── Helpers ──────────────────────────────────────────────────────
58
-
59
- function q(params: { name: string }): string {
60
- return `"${params.name}"`;
61
- }
62
-
63
- function getScalarFields(params: { model: Model }): (ScalarField | EnumField)[] {
64
- return params.model.fields.filter(
65
- (f): f is ScalarField | EnumField => f.kind === "scalar" || f.kind === "enum"
66
- );
67
- }
68
-
69
- function getRelationFields(params: { model: Model }): RelationField[] {
70
- return params.model.fields.filter((f): f is RelationField => f.kind === "relation");
71
- }
72
-
73
- function resolveColumnType(params: { field: ScalarField | EnumField; enums: Enum[] }): string {
74
- const { field, enums } = params;
75
- if (field.kind === "enum") {
76
- // Enum lists must emit `EnumName[]`, not the scalar enum type. Mirrors
77
- // `ddl-builder.ts:135-141`. Bug 2 — see .ai/bug-report-2026-05-24.md.
78
- const enumDef = enums.find((e) => e.name === field.enumName);
79
- const enumType = q({ name: enumDef?.dbName ?? field.enumName });
80
- return field.isList ? `${enumType}[]` : enumType;
81
- }
82
- if (field.default?.kind === "autoincrement") {
83
- return field.prismaType === "BigInt" ? "BIGSERIAL" : "SERIAL";
84
- }
85
- if (field.nativeType) return field.nativeType.toLowerCase();
86
- if (field.isList) return `${PRISMA_TO_PG[field.prismaType]}[]`;
87
- return PRISMA_TO_PG[field.prismaType];
88
- }
89
-
90
- /** Resolve to the base column type (no SERIAL — used for FK reference columns in join tables) */
91
- function resolveBaseColumnType(params: { field: ScalarField | EnumField; enums: Enum[] }): string {
92
- const { field, enums } = params;
93
- if (field.kind === "enum") {
94
- // Same enum-list handling as resolveColumnType (Bug 2).
95
- const enumDef = enums.find((e) => e.name === field.enumName);
96
- const enumType = q({ name: enumDef?.dbName ?? field.enumName });
97
- return field.isList ? `${enumType}[]` : enumType;
98
- }
99
- if (field.nativeType) return field.nativeType.toLowerCase();
100
- if (field.isList) return `${PRISMA_TO_PG[field.prismaType]}[]`;
101
- return PRISMA_TO_PG[field.prismaType];
102
- }
103
-
104
- function resolveDefault(params: { field: ScalarField | EnumField }): string | undefined {
105
- const { field } = params;
106
- const def = field.default;
107
- if (!def) return undefined;
108
- switch (def.kind) {
109
- case "autoincrement": return undefined;
110
- case "now": return "CURRENT_TIMESTAMP";
111
- case "uuid": return "gen_random_uuid()::TEXT";
112
- case "cuid": return undefined;
113
- case "nanoid": return undefined;
114
- case "ulid": return undefined;
115
- case "dbgenerated": return def.value;
116
- case "literal": {
117
- const val = def.value;
118
- if (typeof val === "string") return `'${val.replace(/'/g, "''")}'`;
119
- if (typeof val === "boolean") return val ? "true" : "false";
120
- return String(val);
121
- }
122
- default: return undefined;
123
- }
124
- }
125
-
126
- function uniqueKey(params: { uc: UniqueConstraint; model: Model }): string {
127
- return params.uc.fields
128
- .map((f: string) => resolveFieldDbName({ model: params.model, fieldName: f }))
129
- .sort()
130
- .join(",");
131
- }
132
-
133
- /**
134
- * Build the effective set of unique constraints for a model by merging
135
- * explicit `uniqueConstraints` entries with synthetic single-field entries
136
- * from fields that have `isUnique: true`.
137
- *
138
- * This normalizes the two representations of single-field uniqueness:
139
- * - `@unique` on a field → `isUnique: true`, no `uniqueConstraints` entry
140
- * - `@@unique([field])` on a model → `uniqueConstraints` entry, `isUnique: false`
141
- *
142
- * The database represents both identically, so the differ must treat them
143
- * as equivalent to avoid phantom diffs when comparing parsed schemas against
144
- * introspected schemas (which always use `isUnique: true` for single-field uniques).
145
- */
146
- function getEffectiveUniques(params: { model: Model }): Map<string, UniqueConstraint> {
147
- const { model } = params;
148
- const result = new Map<string, UniqueConstraint>();
149
-
150
- // Add all explicit uniqueConstraints entries
151
- for (const uc of model.uniqueConstraints) {
152
- result.set(uniqueKey({ uc, model }), uc);
153
- }
154
-
155
- // Add synthetic entries for isUnique fields not already covered
156
- for (const field of model.fields) {
157
- if (field.kind === "relation") continue;
158
- if (!field.isUnique) continue;
159
- const syntheticUc: UniqueConstraint = { fields: [field.name], name: undefined };
160
- const key = uniqueKey({ uc: syntheticUc, model });
161
- if (!result.has(key)) {
162
- result.set(key, syntheticUc);
163
- }
164
- }
165
-
166
- return result;
167
- }
168
-
169
- function indexKey(params: { idx: IndexDefinition; model: Model }): string {
170
- return params.idx.fields
171
- .map((f: string) => resolveFieldDbName({ model: params.model, fieldName: f }))
172
- .sort()
173
- .join(",");
174
- }
175
-
176
- function fkKey(params: { rel: RelationField; model: Model; allModels: Model[] }): string {
177
- const { rel, model, allModels } = params;
178
- const resolvedFields = rel.relation.fields
179
- .map((f: string) => resolveFieldDbName({ model, fieldName: f }))
180
- .join(",");
181
- const relatedModel = allModels.find((m: Model) => m.name === rel.relatedModel);
182
- const relatedTableName = relatedModel?.dbName ?? rel.relatedModel;
183
- const resolvedRefs = rel.relation.references
184
- .map((f: string) => {
185
- if (relatedModel) return resolveFieldDbName({ model: relatedModel, fieldName: f });
186
- return f;
187
- })
188
- .join(",");
189
- return `${resolvedFields}->${relatedTableName}.${resolvedRefs}`;
190
- }
191
-
192
- /** Resolve a logical field name to its dbName by looking up the model's fields. */
193
- function resolveFieldDbName(params: { model: Model; fieldName: string }): string {
194
- const { model, fieldName } = params;
195
- const field = model.fields.find((f: Field) => f.name === fieldName);
196
- if (field && field.kind !== "relation") return field.dbName;
197
- return fieldName;
198
- }
199
-
200
- // ─── Implicit M:N Detection ──────────────────────────────────────
201
-
202
- type JoinTableInfo = {
203
- modelA: string;
204
- modelB: string;
205
- tableName: string;
206
- };
207
-
208
- function detectImplicitManyToMany(params: { schema: Schema }): JoinTableInfo[] {
209
- const { schema } = params;
210
-
211
- // Detection lives in the shared helper (single source of truth with the DDL
212
- // builder); here we only attach the `_AToB` join-table name to each pair.
213
- return detectImplicitM2MPairs({ schema }).map((pair) => ({
214
- modelA: pair.modelA,
215
- modelB: pair.modelB,
216
- tableName: `_${pair.modelA}To${pair.modelB}`,
217
- }));
218
- }
219
-
220
- // ─── Main Differ ──────────────────────────────────────────────────
221
-
222
- export function diffSchemas(params: { previous: Schema; current: Schema }): DiffOperation[] {
223
- const { previous, current } = params;
224
- const ops: DiffOperation[] = [];
225
-
226
- // Match models by dbName so that @@map("table_name") models match introspected models
227
- const prevModelMap = new Map(previous.models.map((m: Model) => [m.dbName, m]));
228
- const currModelMap = new Map(current.models.map((m: Model) => [m.dbName, m]));
229
- const prevEnumMap = new Map(previous.enums.map((e: Enum) => [e.dbName ?? e.name, e]));
230
- const currEnumMap = new Map(current.enums.map((e: Enum) => [e.dbName ?? e.name, e]));
231
-
232
- // ─── Enum Diffs ───────────────────────────────────────────────
233
-
234
- // New enums
235
- for (const [name, enumDef] of currEnumMap) {
236
- if (!prevEnumMap.has(name)) {
237
- const typeName = enumDef.dbName ?? enumDef.name;
238
- const values = enumDef.values
239
- .map((v) => `'${(v.dbName ?? v.name).replace(/'/g, "''")}'`)
240
- .join(", ");
241
- ops.push({
242
- type: "createEnum",
243
- sql: `DO $$ BEGIN\n CREATE TYPE ${q({ name: typeName })} AS ENUM (${values});\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;`,
244
- downSql: `DROP TYPE IF EXISTS ${q({ name: typeName })};`,
245
- isDestructive: false,
246
- isReversible: true,
247
- description: `Create enum type "${name}"`,
248
- });
249
- }
250
- }
251
-
252
- // Dropped enums — reverse is recreating the enum with its previous values
253
- for (const [name, enumDef] of prevEnumMap) {
254
- if (!currEnumMap.has(name)) {
255
- const typeName = enumDef.dbName ?? enumDef.name;
256
- const values = enumDef.values
257
- .map((v) => `'${(v.dbName ?? v.name).replace(/'/g, "''")}'`)
258
- .join(", ");
259
- ops.push({
260
- type: "dropEnum",
261
- sql: `DROP TYPE IF EXISTS ${q({ name: typeName })};`,
262
- downSql: `DO $$ BEGIN\n CREATE TYPE ${q({ name: typeName })} AS ENUM (${values});\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;`,
263
- isDestructive: true,
264
- isReversible: true,
265
- description: `Drop enum type "${name}"`,
266
- });
267
- }
268
- }
269
-
270
- // Modified enums (added/removed values)
271
- for (const [name, currEnum] of currEnumMap) {
272
- const prevEnum = prevEnumMap.get(name);
273
- if (!prevEnum) continue;
274
-
275
- const typeName = currEnum.dbName ?? currEnum.name;
276
- const prevValues = new Set(prevEnum.values.map((v) => v.name));
277
- const currValues = new Set(currEnum.values.map((v) => v.name));
278
-
279
- // Added values — reverse requires type recreation (PG cannot DROP VALUE)
280
- for (const val of currEnum.values) {
281
- if (!prevValues.has(val.name)) {
282
- const dbVal = val.dbName ?? val.name;
283
- ops.push({
284
- type: "addEnumValue",
285
- sql: `ALTER TYPE ${q({ name: typeName })} ADD VALUE IF NOT EXISTS '${dbVal.replace(/'/g, "''")}';`,
286
- downSql: `-- WARNING: PostgreSQL does not support removing enum values directly.\n-- Value "${val.name}" was added to enum "${name}" and cannot be cleanly removed.\n-- Manual type recreation may be required.`,
287
- isDestructive: false,
288
- isReversible: false,
289
- description: `Add value "${val.name}" to enum "${name}"`,
290
- });
291
- }
292
- }
293
-
294
- // Removed values (destructive — PG doesn't support DROP VALUE easily)
295
- for (const val of prevEnum.values) {
296
- if (!currValues.has(val.name)) {
297
- const dbVal = val.dbName ?? val.name;
298
- ops.push({
299
- type: "removeEnumValue",
300
- sql: `-- WARNING: PostgreSQL does not support removing enum values directly.\n-- You need to recreate the type. Value "${val.name}" removed from enum "${name}".`,
301
- downSql: `ALTER TYPE ${q({ name: typeName })} ADD VALUE IF NOT EXISTS '${dbVal.replace(/'/g, "''")}';`,
302
- isDestructive: true,
303
- isReversible: true,
304
- description: `Remove value "${val.name}" from enum "${name}" (requires type recreation)`,
305
- });
306
- }
307
- }
308
- }
309
-
310
- // ─── Table Diffs ──────────────────────────────────────────────
311
-
312
- // New tables
313
- for (const [name, model] of currModelMap) {
314
- if (prevModelMap.has(name)) continue;
315
-
316
- const tableName = model.dbName;
317
- const columns: string[] = [];
318
-
319
- for (const field of getScalarFields({ model })) {
320
- const colName = field.dbName;
321
- const colType = resolveColumnType({ field, enums: current.enums });
322
- let colDef = ` ${q({ name: colName })} ${colType}`;
323
-
324
- if (field.kind === "scalar" && field.isRequired && field.default?.kind !== "autoincrement") {
325
- colDef += " NOT NULL";
326
- } else if (field.kind === "enum" && field.isRequired) {
327
- colDef += " NOT NULL";
328
- }
329
-
330
- const defaultExpr = resolveDefault({ field });
331
- if (defaultExpr) colDef += ` DEFAULT ${defaultExpr}`;
332
- if (field.isUnique) colDef += " UNIQUE";
333
- columns.push(colDef);
334
- }
335
-
336
- const pkFields = model.primaryKey.fields
337
- .map((f: string) => {
338
- const fld = model.fields.find((mf: Field) => mf.name === f);
339
- if (fld && fld.kind !== "relation") return q({ name: fld.dbName });
340
- return q({ name: f });
341
- })
342
- .join(", ");
343
-
344
- columns.push(` CONSTRAINT ${q({ name: `${tableName}_pkey` })} PRIMARY KEY (${pkFields})`);
345
-
346
- ops.push({
347
- type: "createTable",
348
- sql: `CREATE TABLE ${q({ name: tableName })} (\n${columns.join(",\n")}\n);`,
349
- downSql: `DROP TABLE IF EXISTS ${q({ name: tableName })} CASCADE;`,
350
- isDestructive: false,
351
- isReversible: true,
352
- description: `Create table "${name}"`,
353
- });
354
- }
355
-
356
- // Unique constraints, indexes, and foreign keys for new tables
357
- for (const [name, model] of currModelMap) {
358
- if (prevModelMap.has(name)) continue;
359
-
360
- const tableName = model.dbName;
361
-
362
- // Unique constraints from @@unique (single-field @unique is inline on the column)
363
- for (const uc of model.uniqueConstraints) {
364
- const cols = uc.fields.map((f: string) => q({ name: resolveFieldDbName({ model, fieldName: f }) })).join(", ");
365
- const resolvedFieldNames = uc.fields.map((f: string) => resolveFieldDbName({ model, fieldName: f }));
366
- const idxName = uc.name ?? `${tableName}_${resolvedFieldNames.join("_")}_key`;
367
- ops.push({
368
- type: "addUnique",
369
- sql: `CREATE UNIQUE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
370
- downSql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
371
- isDestructive: false,
372
- isReversible: true,
373
- description: `Add unique constraint on (${uc.fields.join(", ")}) in "${name}"`,
374
- });
375
- }
376
-
377
- // Regular indexes
378
- for (const idx of model.indexes) {
379
- const cols = idx.fields.map((f: string) => q({ name: resolveFieldDbName({ model, fieldName: f }) })).join(", ");
380
- const resolvedFieldNames = idx.fields.map((f: string) => resolveFieldDbName({ model, fieldName: f }));
381
- const idxName = idx.name ?? `${tableName}_${resolvedFieldNames.join("_")}_idx`;
382
- ops.push({
383
- type: "addIndex",
384
- sql: `CREATE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
385
- downSql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
386
- isDestructive: false,
387
- isReversible: true,
388
- description: `Add index on (${idx.fields.join(", ")}) in "${name}"`,
389
- });
390
- }
391
-
392
- // Foreign keys
393
- const fkRels = getRelationFields({ model })
394
- .filter((r: RelationField) => r.relation.isForeignKey && r.relation.fields.length > 0);
395
- for (const rel of fkRels) {
396
- const fkCols = rel.relation.fields.map((f: string) => q({ name: resolveFieldDbName({ model, fieldName: f }) })).join(", ");
397
- const relatedModel = current.models.find((m: Model) => m.name === rel.relatedModel);
398
- const relatedTableName = relatedModel?.dbName ?? rel.relatedModel;
399
- const refCols = rel.relation.references.map((f: string) => {
400
- if (relatedModel) return q({ name: resolveFieldDbName({ model: relatedModel, fieldName: f }) });
401
- return q({ name: f });
402
- }).join(", ");
403
- const resolvedFkNames = rel.relation.fields.map((f: string) => resolveFieldDbName({ model, fieldName: f }));
404
- const constraintName = `${tableName}_${resolvedFkNames.join("_")}_fkey`;
405
- const actionClause = buildFkActionClause({ rel });
406
- ops.push({
407
- type: "addForeignKey",
408
- sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${actionClause};`,
409
- downSql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
410
- isDestructive: false,
411
- isReversible: true,
412
- description: `Add foreign key on "${name}" (${rel.relation.fields.join(", ")}) \u2192 "${rel.relatedModel}"`,
413
- });
414
- }
415
- }
416
-
417
- // Dropped tables — reverse is recreating the table from the previous schema
418
- for (const [name, model] of prevModelMap) {
419
- if (currModelMap.has(name)) continue;
420
-
421
- // Build the CREATE TABLE statement for the down migration
422
- const dropTableName = model.dbName;
423
- const downColumns: string[] = [];
424
- for (const field of getScalarFields({ model })) {
425
- const colName = field.dbName;
426
- const colType = resolveColumnType({ field, enums: previous.enums });
427
- let colDef = ` ${q({ name: colName })} ${colType}`;
428
- if (field.kind === "scalar" && field.isRequired && field.default?.kind !== "autoincrement") {
429
- colDef += " NOT NULL";
430
- } else if (field.kind === "enum" && field.isRequired) {
431
- colDef += " NOT NULL";
432
- }
433
- const defaultExpr = resolveDefault({ field });
434
- if (defaultExpr) colDef += ` DEFAULT ${defaultExpr}`;
435
- if (field.isUnique) colDef += " UNIQUE";
436
- downColumns.push(colDef);
437
- }
438
- const downPkFields = model.primaryKey.fields
439
- .map((f: string) => {
440
- const fld = model.fields.find((mf: Field) => mf.name === f);
441
- if (fld && fld.kind !== "relation") return q({ name: fld.dbName });
442
- return q({ name: f });
443
- })
444
- .join(", ");
445
- downColumns.push(` CONSTRAINT ${q({ name: `${dropTableName}_pkey` })} PRIMARY KEY (${downPkFields})`);
446
-
447
- ops.push({
448
- type: "dropTable",
449
- sql: `DROP TABLE IF EXISTS ${q({ name: model.dbName })} CASCADE;`,
450
- downSql: `CREATE TABLE ${q({ name: dropTableName })} (\n${downColumns.join(",\n")}\n);`,
451
- isDestructive: true,
452
- isReversible: true,
453
- description: `Drop table "${name}"`,
454
- });
455
- }
456
-
457
- // ─── Column Diffs (for surviving models) ──────────────────────
458
-
459
- for (const [name, currModel] of currModelMap) {
460
- const prevModel = prevModelMap.get(name);
461
- if (!prevModel) continue; // New table, already handled
462
-
463
- const tableName = currModel.dbName;
464
- const prevFields = new Map(
465
- getScalarFields({ model: prevModel }).map((f) => [f.dbName, f])
466
- );
467
- const currFields = new Map(
468
- getScalarFields({ model: currModel }).map((f) => [f.dbName, f])
469
- );
470
-
471
- // Added columns — reverse is dropping the column
472
- for (const [fieldName, field] of currFields) {
473
- if (prevFields.has(fieldName)) continue;
474
-
475
- const colName = field.dbName;
476
- const colType = resolveColumnType({ field, enums: current.enums });
477
- const defaultExpr = resolveDefault({ field });
478
-
479
- // Safe if nullable or has default
480
- const isDestructive = field.isRequired && !defaultExpr;
481
-
482
- let colDef = `${colType}`;
483
- if (field.kind === "scalar" && field.isRequired && field.default?.kind !== "autoincrement") {
484
- colDef += " NOT NULL";
485
- } else if (field.kind === "enum" && field.isRequired) {
486
- colDef += " NOT NULL";
487
- }
488
- if (defaultExpr) colDef += ` DEFAULT ${defaultExpr}`;
489
-
490
- ops.push({
491
- type: "addColumn",
492
- sql: `ALTER TABLE ${q({ name: tableName })} ADD COLUMN ${q({ name: colName })} ${colDef};`,
493
- downSql: `ALTER TABLE ${q({ name: tableName })} DROP COLUMN IF EXISTS ${q({ name: colName })};`,
494
- isDestructive,
495
- isReversible: true,
496
- description: `Add column "${fieldName}" to "${name}"`,
497
- });
498
- }
499
-
500
- // Dropped columns — reverse is re-adding the column with its previous definition
501
- for (const [fieldName, field] of prevFields) {
502
- if (currFields.has(fieldName)) continue;
503
-
504
- const colName = field.dbName;
505
- const colType = resolveColumnType({ field, enums: previous.enums });
506
- const defaultExpr = resolveDefault({ field });
507
- // Down migration always adds the column back as nullable because
508
- // the original data is lost after DROP COLUMN — adding NOT NULL
509
- // would fail if the table has existing rows with null values.
510
- let downColDef = `${colType}`;
511
- if (defaultExpr) downColDef += ` DEFAULT ${defaultExpr}`;
512
-
513
- ops.push({
514
- type: "dropColumn",
515
- sql: `ALTER TABLE ${q({ name: tableName })} DROP COLUMN IF EXISTS ${q({ name: field.dbName })};`,
516
- downSql: `ALTER TABLE ${q({ name: tableName })} ADD COLUMN ${q({ name: colName })} ${downColDef};`,
517
- isDestructive: true,
518
- isReversible: true,
519
- description: `Drop column "${fieldName}" from "${name}"`,
520
- });
521
- }
522
-
523
- // Modified columns
524
- for (const [fieldName, currField] of currFields) {
525
- const prevField = prevFields.get(fieldName);
526
- if (!prevField) continue;
527
-
528
- const colName = currField.dbName;
529
- const prevType = resolveColumnType({ field: prevField, enums: previous.enums });
530
- const currType = resolveColumnType({ field: currField, enums: current.enums });
531
-
532
- // Type change — reverse is setting back to the previous type
533
- if (prevType !== currType) {
534
- // Detect scalar -> array transition of same base type. PG can't implicit-cast,
535
- // so we add a USING clause that wraps the existing value in a 1-element array
536
- // (or NULL if the column is NULL). This heals databases broken by an earlier
537
- // enum-array bug (Bug 2) where the column was created as the scalar type.
538
- // We intentionally do NOT handle the reverse direction (array -> scalar) — that
539
- // is lossy and the user must decide which element to keep.
540
- const isScalarToArray =
541
- currType.endsWith("[]") && !prevType.endsWith("[]") &&
542
- currType.slice(0, -2) === prevType;
543
-
544
- const usingClause = isScalarToArray
545
- ? ` USING CASE WHEN ${q({ name: colName })} IS NULL THEN NULL ELSE ARRAY[${q({ name: colName })}] END`
546
- : "";
547
-
548
- ops.push({
549
- type: "alterColumnType",
550
- sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DATA TYPE ${currType}${usingClause};`,
551
- downSql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DATA TYPE ${prevType};`,
552
- isDestructive: !isScalarToArray, // scalar -> array is non-destructive (data preserved as 1-element arrays)
553
- isReversible: true,
554
- description: `Change type of "${fieldName}" in "${name}" from ${prevType} to ${currType}`,
555
- });
556
- }
557
-
558
- // Nullability change — reverse is toggling back
559
- if (prevField.isRequired !== currField.isRequired) {
560
- if (currField.isRequired) {
561
- ops.push({
562
- type: "alterColumnNullability",
563
- sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET NOT NULL;`,
564
- downSql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} DROP NOT NULL;`,
565
- isDestructive: true,
566
- isReversible: true,
567
- description: `Set NOT NULL on "${fieldName}" in "${name}"`,
568
- });
569
- } else {
570
- ops.push({
571
- type: "alterColumnNullability",
572
- sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} DROP NOT NULL;`,
573
- downSql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET NOT NULL;`,
574
- isDestructive: false,
575
- isReversible: true,
576
- description: `Drop NOT NULL on "${fieldName}" in "${name}"`,
577
- });
578
- }
579
- }
580
-
581
- // Default change — reverse is restoring the previous default
582
- const prevDefault = resolveDefault({ field: prevField });
583
- const currDefault = resolveDefault({ field: currField });
584
- if (prevDefault !== currDefault) {
585
- if (currDefault) {
586
- const downDefaultSql = prevDefault
587
- ? `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DEFAULT ${prevDefault};`
588
- : `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} DROP DEFAULT;`;
589
- ops.push({
590
- type: "alterColumnDefault",
591
- sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DEFAULT ${currDefault};`,
592
- downSql: downDefaultSql,
593
- isDestructive: false,
594
- isReversible: true,
595
- description: `Set default on "${fieldName}" in "${name}" to ${currDefault}`,
596
- });
597
- } else {
598
- const downDefaultSql = prevDefault
599
- ? `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DEFAULT ${prevDefault};`
600
- : `-- No previous default to restore`;
601
- ops.push({
602
- type: "alterColumnDefault",
603
- sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} DROP DEFAULT;`,
604
- downSql: downDefaultSql,
605
- isDestructive: false,
606
- isReversible: true,
607
- description: `Drop default on "${fieldName}" in "${name}"`,
608
- });
609
- }
610
- }
611
- }
612
-
613
- // ─── Unique Constraint Diffs ──────────────────────────────
614
- // Use effective uniques to normalize @unique (field-level) and
615
- // @@unique([field]) (model-level) representations before diffing.
616
-
617
- const prevUniques = getEffectiveUniques({ model: prevModel });
618
- const currUniques = getEffectiveUniques({ model: currModel });
619
-
620
- for (const [key, uc] of currUniques) {
621
- if (prevUniques.has(key)) continue;
622
- const cols = uc.fields.map((f: string) => q({ name: resolveFieldDbName({ model: currModel, fieldName: f }) })).join(", ");
623
- const resolvedFieldNames = uc.fields.map((f: string) => resolveFieldDbName({ model: currModel, fieldName: f }));
624
- const idxName = uc.name ?? `${tableName}_${resolvedFieldNames.join("_")}_key`;
625
- ops.push({
626
- type: "addUnique",
627
- sql: `CREATE UNIQUE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
628
- downSql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
629
- isDestructive: false,
630
- isReversible: true,
631
- description: `Add unique constraint on (${uc.fields.join(", ")}) in "${name}"`,
632
- });
633
- }
634
-
635
- for (const [key, uc] of prevUniques) {
636
- if (currUniques.has(key)) continue;
637
- const cols = uc.fields.map((f: string) => q({ name: resolveFieldDbName({ model: prevModel, fieldName: f }) })).join(", ");
638
- const resolvedFieldNames = uc.fields.map((f: string) => resolveFieldDbName({ model: prevModel, fieldName: f }));
639
- const idxName = uc.name ?? `${tableName}_${resolvedFieldNames.join("_")}_key`;
640
- ops.push({
641
- type: "dropUnique",
642
- sql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
643
- downSql: `CREATE UNIQUE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
644
- isDestructive: true,
645
- isReversible: true,
646
- description: `Drop unique constraint on (${uc.fields.join(", ")}) in "${name}"`,
647
- });
648
- }
649
-
650
- // ─── Index Diffs ──────────────────────────────────────────
651
-
652
- const prevIndexes = new Map(
653
- prevModel.indexes.map((idx: IndexDefinition) => [indexKey({ idx, model: prevModel }), idx])
654
- );
655
- const currIndexes = new Map(
656
- currModel.indexes.map((idx: IndexDefinition) => [indexKey({ idx, model: currModel }), idx])
657
- );
658
-
659
- for (const [key, idx] of currIndexes) {
660
- if (prevIndexes.has(key)) continue;
661
- const cols = idx.fields.map((f: string) => q({ name: resolveFieldDbName({ model: currModel, fieldName: f }) })).join(", ");
662
- const resolvedFieldNames = idx.fields.map((f: string) => resolveFieldDbName({ model: currModel, fieldName: f }));
663
- const idxName = idx.name ?? `${tableName}_${resolvedFieldNames.join("_")}_idx`;
664
- ops.push({
665
- type: "addIndex",
666
- sql: `CREATE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
667
- downSql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
668
- isDestructive: false,
669
- isReversible: true,
670
- description: `Add index on (${idx.fields.join(", ")}) in "${name}"`,
671
- });
672
- }
673
-
674
- for (const [key, idx] of prevIndexes) {
675
- if (currIndexes.has(key)) continue;
676
- const cols = idx.fields.map((f: string) => q({ name: resolveFieldDbName({ model: prevModel, fieldName: f }) })).join(", ");
677
- const resolvedFieldNames = idx.fields.map((f: string) => resolveFieldDbName({ model: prevModel, fieldName: f }));
678
- const idxName = idx.name ?? `${tableName}_${resolvedFieldNames.join("_")}_idx`;
679
- ops.push({
680
- type: "dropIndex",
681
- sql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
682
- downSql: `CREATE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
683
- isDestructive: true,
684
- isReversible: true,
685
- description: `Drop index on (${idx.fields.join(", ")}) in "${name}"`,
686
- });
687
- }
688
-
689
- // ─── Foreign Key Diffs ────────────────────────────────────
690
-
691
- const prevFKs = new Map(
692
- getRelationFields({ model: prevModel })
693
- .filter((r: RelationField) => r.relation.isForeignKey && r.relation.fields.length > 0)
694
- .map((r: RelationField) => [fkKey({ rel: r, model: prevModel, allModels: previous.models }), r])
695
- );
696
- const currFKs = new Map(
697
- getRelationFields({ model: currModel })
698
- .filter((r: RelationField) => r.relation.isForeignKey && r.relation.fields.length > 0)
699
- .map((r: RelationField) => [fkKey({ rel: r, model: currModel, allModels: current.models }), r])
700
- );
701
-
702
- for (const [key, rel] of currFKs) {
703
- const prev = prevFKs.get(key);
704
- const fkCols = rel.relation.fields.map((f: string) => q({ name: resolveFieldDbName({ model: currModel, fieldName: f }) })).join(", ");
705
- const relatedModel = current.models.find((m: Model) => m.name === rel.relatedModel);
706
- const relatedTableName = relatedModel?.dbName ?? rel.relatedModel;
707
- const refCols = rel.relation.references.map((f: string) => {
708
- if (relatedModel) return q({ name: resolveFieldDbName({ model: relatedModel, fieldName: f }) });
709
- return q({ name: f });
710
- }).join(", ");
711
- const resolvedFkNames = rel.relation.fields.map((f: string) => resolveFieldDbName({ model: currModel, fieldName: f }));
712
- const constraintName = `${tableName}_${resolvedFkNames.join("_")}_fkey`;
713
- const actionClause = buildFkActionClause({ rel });
714
-
715
- if (!prev) {
716
- ops.push({
717
- type: "addForeignKey",
718
- sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${actionClause};`,
719
- downSql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
720
- isDestructive: false,
721
- isReversible: true,
722
- description: `Add foreign key on "${name}" (${rel.relation.fields.join(", ")}) → "${rel.relatedModel}"`,
723
- });
724
- continue;
725
- }
726
-
727
- // FK exists on both sides — detect cascade-action drift. Postgres has no
728
- // `ALTER CONSTRAINT ... ON DELETE` for FKs, so we emit a DROP + ADD pair.
729
- const prevFingerprint = fkActionFingerprint({ rel: prev });
730
- const currFingerprint = fkActionFingerprint({ rel });
731
- if (prevFingerprint !== currFingerprint) {
732
- const prevActionClause = buildFkActionClause({ rel: prev });
733
- ops.push({
734
- type: "dropForeignKey",
735
- sql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
736
- downSql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${prevActionClause};`,
737
- isDestructive: false,
738
- isReversible: true,
739
- description: `Drop foreign key on "${name}" (${rel.relation.fields.join(", ")}) to change cascade actions`,
740
- });
741
- ops.push({
742
- type: "addForeignKey",
743
- sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${actionClause};`,
744
- downSql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
745
- isDestructive: false,
746
- isReversible: true,
747
- description: `Re-add foreign key on "${name}" (${rel.relation.fields.join(", ")}) with new cascade actions (${currFingerprint.replace("|", " / ")})`,
748
- });
749
- }
750
- }
751
-
752
- for (const [key, rel] of prevFKs) {
753
- if (currFKs.has(key)) continue;
754
- const fkCols = rel.relation.fields.map((f: string) => q({ name: resolveFieldDbName({ model: prevModel, fieldName: f }) })).join(", ");
755
- const relatedModel = previous.models.find((m: Model) => m.name === rel.relatedModel);
756
- const relatedTableName = relatedModel?.dbName ?? rel.relatedModel;
757
- const refCols = rel.relation.references.map((f: string) => {
758
- if (relatedModel) return q({ name: resolveFieldDbName({ model: relatedModel, fieldName: f }) });
759
- return q({ name: f });
760
- }).join(", ");
761
- const resolvedFkNames = rel.relation.fields.map((f: string) => resolveFieldDbName({ model: prevModel, fieldName: f }));
762
- const constraintName = `${tableName}_${resolvedFkNames.join("_")}_fkey`;
763
- const actionClause = buildFkActionClause({ rel });
764
- ops.push({
765
- type: "dropForeignKey",
766
- sql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
767
- downSql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${actionClause};`,
768
- isDestructive: true,
769
- isReversible: true,
770
- description: `Drop foreign key on "${name}" (${rel.relation.fields.join(", ")}) → "${rel.relatedModel}"`,
771
- });
772
- }
773
- }
774
-
775
- // ─── Implicit M:N Join Table Diffs ────────────────────────────
776
-
777
- const prevJoinTables = detectImplicitManyToMany({ schema: previous });
778
- const currJoinTables = detectImplicitManyToMany({ schema: current });
779
-
780
- const prevJoinSet = new Set(prevJoinTables.map((j: JoinTableInfo) => j.tableName));
781
- const currJoinSet = new Set(currJoinTables.map((j: JoinTableInfo) => j.tableName));
782
-
783
- for (const jt of currJoinTables) {
784
- if (prevJoinSet.has(jt.tableName)) continue;
785
-
786
- const modelADef = currModelMap.get(jt.modelA);
787
- const modelBDef = currModelMap.get(jt.modelB);
788
- if (!modelADef || !modelBDef) continue;
789
-
790
- const pkFieldA = modelADef.primaryKey.fields[0]!;
791
- const pkFieldB = modelBDef.primaryKey.fields[0]!;
792
-
793
- // Resolve PK types
794
- const pkFieldDefA = modelADef.fields.find((f: Field) => f.name === pkFieldA);
795
- const pkFieldDefB = modelBDef.fields.find((f: Field) => f.name === pkFieldB);
796
-
797
- const pkTypeA = pkFieldDefA && (pkFieldDefA.kind === "scalar" || pkFieldDefA.kind === "enum")
798
- ? resolveBaseColumnType({ field: pkFieldDefA, enums: current.enums })
799
- : "INTEGER";
800
- const pkTypeB = pkFieldDefB && (pkFieldDefB.kind === "scalar" || pkFieldDefB.kind === "enum")
801
- ? resolveBaseColumnType({ field: pkFieldDefB, enums: current.enums })
802
- : "INTEGER";
803
-
804
- const tableA = modelADef.dbName;
805
- const tableB = modelBDef.dbName;
806
-
807
- ops.push({
808
- type: "createJoinTable",
809
- sql: [
810
- `CREATE TABLE ${q({ name: jt.tableName })} (\n "A" ${pkTypeA} NOT NULL,\n "B" ${pkTypeB} NOT NULL\n);`,
811
- `CREATE UNIQUE INDEX ${q({ name: `${jt.tableName}_AB_unique` })} ON ${q({ name: jt.tableName })} ("A", "B");`,
812
- `CREATE INDEX ${q({ name: `${jt.tableName}_B_index` })} ON ${q({ name: jt.tableName })} ("B");`,
813
- `ALTER TABLE ${q({ name: jt.tableName })} ADD CONSTRAINT ${q({ name: `${jt.tableName}_A_fkey` })} FOREIGN KEY ("A") REFERENCES ${q({ name: tableA })} ("${pkFieldA}") ON DELETE CASCADE ON UPDATE CASCADE;`,
814
- `ALTER TABLE ${q({ name: jt.tableName })} ADD CONSTRAINT ${q({ name: `${jt.tableName}_B_fkey` })} FOREIGN KEY ("B") REFERENCES ${q({ name: tableB })} ("${pkFieldB}") ON DELETE CASCADE ON UPDATE CASCADE;`,
815
- ].join("\n"),
816
- downSql: `DROP TABLE IF EXISTS ${q({ name: jt.tableName })} CASCADE;`,
817
- isDestructive: false,
818
- isReversible: true,
819
- description: `Create join table "${jt.tableName}" for ${jt.modelA} ↔ ${jt.modelB}`,
820
- });
821
- }
822
-
823
- for (const jt of prevJoinTables) {
824
- if (currJoinSet.has(jt.tableName)) continue;
825
-
826
- // For the down migration, reconstruct the join table from the previous schema
827
- const prevModelADef = prevModelMap.get(jt.modelA);
828
- const prevModelBDef = prevModelMap.get(jt.modelB);
829
- let downJoinSql = `-- WARNING: Cannot fully reconstruct join table "${jt.tableName}". Manual intervention required.`;
830
-
831
- if (prevModelADef && prevModelBDef) {
832
- const prevPkFieldA = prevModelADef.primaryKey.fields[0]!;
833
- const prevPkFieldB = prevModelBDef.primaryKey.fields[0]!;
834
- const prevPkFieldDefA = prevModelADef.fields.find((f: Field) => f.name === prevPkFieldA);
835
- const prevPkFieldDefB = prevModelBDef.fields.find((f: Field) => f.name === prevPkFieldB);
836
- const prevPkTypeA = prevPkFieldDefA && (prevPkFieldDefA.kind === "scalar" || prevPkFieldDefA.kind === "enum")
837
- ? resolveBaseColumnType({ field: prevPkFieldDefA, enums: previous.enums })
838
- : "INTEGER";
839
- const prevPkTypeB = prevPkFieldDefB && (prevPkFieldDefB.kind === "scalar" || prevPkFieldDefB.kind === "enum")
840
- ? resolveBaseColumnType({ field: prevPkFieldDefB, enums: previous.enums })
841
- : "INTEGER";
842
- const prevTableA = prevModelADef.dbName;
843
- const prevTableB = prevModelBDef.dbName;
844
-
845
- downJoinSql = [
846
- `CREATE TABLE ${q({ name: jt.tableName })} (\n "A" ${prevPkTypeA} NOT NULL,\n "B" ${prevPkTypeB} NOT NULL\n);`,
847
- `CREATE UNIQUE INDEX ${q({ name: `${jt.tableName}_AB_unique` })} ON ${q({ name: jt.tableName })} ("A", "B");`,
848
- `CREATE INDEX ${q({ name: `${jt.tableName}_B_index` })} ON ${q({ name: jt.tableName })} ("B");`,
849
- `ALTER TABLE ${q({ name: jt.tableName })} ADD CONSTRAINT ${q({ name: `${jt.tableName}_A_fkey` })} FOREIGN KEY ("A") REFERENCES ${q({ name: prevTableA })} ("${prevPkFieldA}") ON DELETE CASCADE ON UPDATE CASCADE;`,
850
- `ALTER TABLE ${q({ name: jt.tableName })} ADD CONSTRAINT ${q({ name: `${jt.tableName}_B_fkey` })} FOREIGN KEY ("B") REFERENCES ${q({ name: prevTableB })} ("${prevPkFieldB}") ON DELETE CASCADE ON UPDATE CASCADE;`,
851
- ].join("\n");
852
- }
853
-
854
- ops.push({
855
- type: "dropJoinTable",
856
- sql: `DROP TABLE IF EXISTS ${q({ name: jt.tableName })} CASCADE;`,
857
- downSql: downJoinSql,
858
- isDestructive: true,
859
- isReversible: prevModelADef !== undefined && prevModelBDef !== undefined,
860
- description: `Drop join table "${jt.tableName}" for ${jt.modelA} ↔ ${jt.modelB}`,
861
- });
862
- }
863
-
864
- return ops;
865
- }