@vibeorm/migrate 1.1.1 → 1.1.3
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.
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/migration-runner.ts +24 -0
- package/src/schema-differ.ts +145 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeorm/migrate",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Migration, introspection, and schema diff toolkit for VibeORM",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"bun": ">=1.1.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@vibeorm/parser": "1.1.
|
|
40
|
+
"@vibeorm/parser": "1.1.3"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/index.ts
CHANGED
package/src/migration-runner.ts
CHANGED
|
@@ -101,3 +101,27 @@ export async function removeMigrationRecord(params: {
|
|
|
101
101
|
values: [migrationName],
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
// ─── Rollback Migration ───────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export async function rollbackMigration(params: {
|
|
108
|
+
executor: SqlExecutor;
|
|
109
|
+
migrationName: string;
|
|
110
|
+
sql: string;
|
|
111
|
+
}): Promise<{ executionTime: number }> {
|
|
112
|
+
const { executor, migrationName, sql } = params;
|
|
113
|
+
const start = Date.now();
|
|
114
|
+
|
|
115
|
+
// Split down migration SQL into individual statements
|
|
116
|
+
const statements = splitSqlStatements({ sql });
|
|
117
|
+
for (const stmt of statements) {
|
|
118
|
+
await executor({ text: stmt });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const executionTime = Date.now() - start;
|
|
122
|
+
|
|
123
|
+
// Remove the migration tracking record
|
|
124
|
+
await removeMigrationRecord({ executor, migrationName });
|
|
125
|
+
|
|
126
|
+
return { executionTime };
|
|
127
|
+
}
|
package/src/schema-differ.ts
CHANGED
|
@@ -44,7 +44,11 @@ export type DiffOperationType =
|
|
|
44
44
|
export type DiffOperation = {
|
|
45
45
|
type: DiffOperationType;
|
|
46
46
|
sql: string;
|
|
47
|
+
/** Reverse SQL to undo this operation (for down migrations) */
|
|
48
|
+
downSql: string;
|
|
47
49
|
isDestructive: boolean;
|
|
50
|
+
/** Whether this operation can be cleanly reversed without data loss */
|
|
51
|
+
isReversible: boolean;
|
|
48
52
|
description: string;
|
|
49
53
|
};
|
|
50
54
|
|
|
@@ -225,20 +229,27 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
225
229
|
ops.push({
|
|
226
230
|
type: "createEnum",
|
|
227
231
|
sql: `DO $$ BEGIN\n CREATE TYPE ${q({ name: typeName })} AS ENUM (${values});\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;`,
|
|
232
|
+
downSql: `DROP TYPE IF EXISTS ${q({ name: typeName })};`,
|
|
228
233
|
isDestructive: false,
|
|
234
|
+
isReversible: true,
|
|
229
235
|
description: `Create enum type "${name}"`,
|
|
230
236
|
});
|
|
231
237
|
}
|
|
232
238
|
}
|
|
233
239
|
|
|
234
|
-
// Dropped enums
|
|
240
|
+
// Dropped enums — reverse is recreating the enum with its previous values
|
|
235
241
|
for (const [name, enumDef] of prevEnumMap) {
|
|
236
242
|
if (!currEnumMap.has(name)) {
|
|
237
243
|
const typeName = enumDef.dbName ?? enumDef.name;
|
|
244
|
+
const values = enumDef.values
|
|
245
|
+
.map((v) => `'${(v.dbName ?? v.name).replace(/'/g, "''")}'`)
|
|
246
|
+
.join(", ");
|
|
238
247
|
ops.push({
|
|
239
248
|
type: "dropEnum",
|
|
240
249
|
sql: `DROP TYPE IF EXISTS ${q({ name: typeName })};`,
|
|
250
|
+
downSql: `DO $$ BEGIN\n CREATE TYPE ${q({ name: typeName })} AS ENUM (${values});\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;`,
|
|
241
251
|
isDestructive: true,
|
|
252
|
+
isReversible: true,
|
|
242
253
|
description: `Drop enum type "${name}"`,
|
|
243
254
|
});
|
|
244
255
|
}
|
|
@@ -253,14 +264,16 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
253
264
|
const prevValues = new Set(prevEnum.values.map((v) => v.name));
|
|
254
265
|
const currValues = new Set(currEnum.values.map((v) => v.name));
|
|
255
266
|
|
|
256
|
-
// Added values
|
|
267
|
+
// Added values — reverse requires type recreation (PG cannot DROP VALUE)
|
|
257
268
|
for (const val of currEnum.values) {
|
|
258
269
|
if (!prevValues.has(val.name)) {
|
|
259
270
|
const dbVal = val.dbName ?? val.name;
|
|
260
271
|
ops.push({
|
|
261
272
|
type: "addEnumValue",
|
|
262
273
|
sql: `ALTER TYPE ${q({ name: typeName })} ADD VALUE IF NOT EXISTS '${dbVal.replace(/'/g, "''")}';`,
|
|
274
|
+
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.`,
|
|
263
275
|
isDestructive: false,
|
|
276
|
+
isReversible: false,
|
|
264
277
|
description: `Add value "${val.name}" to enum "${name}"`,
|
|
265
278
|
});
|
|
266
279
|
}
|
|
@@ -269,10 +282,13 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
269
282
|
// Removed values (destructive — PG doesn't support DROP VALUE easily)
|
|
270
283
|
for (const val of prevEnum.values) {
|
|
271
284
|
if (!currValues.has(val.name)) {
|
|
285
|
+
const dbVal = val.dbName ?? val.name;
|
|
272
286
|
ops.push({
|
|
273
287
|
type: "removeEnumValue",
|
|
274
288
|
sql: `-- WARNING: PostgreSQL does not support removing enum values directly.\n-- You need to recreate the type. Value "${val.name}" removed from enum "${name}".`,
|
|
289
|
+
downSql: `ALTER TYPE ${q({ name: typeName })} ADD VALUE IF NOT EXISTS '${dbVal.replace(/'/g, "''")}';`,
|
|
275
290
|
isDestructive: true,
|
|
291
|
+
isReversible: true,
|
|
276
292
|
description: `Remove value "${val.name}" from enum "${name}" (requires type recreation)`,
|
|
277
293
|
});
|
|
278
294
|
}
|
|
@@ -318,7 +334,9 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
318
334
|
ops.push({
|
|
319
335
|
type: "createTable",
|
|
320
336
|
sql: `CREATE TABLE ${q({ name: tableName })} (\n${columns.join(",\n")}\n);`,
|
|
337
|
+
downSql: `DROP TABLE IF EXISTS ${q({ name: tableName })} CASCADE;`,
|
|
321
338
|
isDestructive: false,
|
|
339
|
+
isReversible: true,
|
|
322
340
|
description: `Create table "${name}"`,
|
|
323
341
|
});
|
|
324
342
|
}
|
|
@@ -337,7 +355,9 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
337
355
|
ops.push({
|
|
338
356
|
type: "addUnique",
|
|
339
357
|
sql: `CREATE UNIQUE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
|
|
358
|
+
downSql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
|
|
340
359
|
isDestructive: false,
|
|
360
|
+
isReversible: true,
|
|
341
361
|
description: `Add unique constraint on (${uc.fields.join(", ")}) in "${name}"`,
|
|
342
362
|
});
|
|
343
363
|
}
|
|
@@ -350,7 +370,9 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
350
370
|
ops.push({
|
|
351
371
|
type: "addIndex",
|
|
352
372
|
sql: `CREATE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
|
|
373
|
+
downSql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
|
|
353
374
|
isDestructive: false,
|
|
375
|
+
isReversible: true,
|
|
354
376
|
description: `Add index on (${idx.fields.join(", ")}) in "${name}"`,
|
|
355
377
|
});
|
|
356
378
|
}
|
|
@@ -371,19 +393,50 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
371
393
|
ops.push({
|
|
372
394
|
type: "addForeignKey",
|
|
373
395
|
sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ON DELETE RESTRICT ON UPDATE CASCADE;`,
|
|
396
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
|
|
374
397
|
isDestructive: false,
|
|
398
|
+
isReversible: true,
|
|
375
399
|
description: `Add foreign key on "${name}" (${rel.relation.fields.join(", ")}) \u2192 "${rel.relatedModel}"`,
|
|
376
400
|
});
|
|
377
401
|
}
|
|
378
402
|
}
|
|
379
403
|
|
|
380
|
-
// Dropped tables
|
|
404
|
+
// Dropped tables — reverse is recreating the table from the previous schema
|
|
381
405
|
for (const [name, model] of prevModelMap) {
|
|
382
406
|
if (currModelMap.has(name)) continue;
|
|
407
|
+
|
|
408
|
+
// Build the CREATE TABLE statement for the down migration
|
|
409
|
+
const dropTableName = model.dbName;
|
|
410
|
+
const downColumns: string[] = [];
|
|
411
|
+
for (const field of getScalarFields({ model })) {
|
|
412
|
+
const colName = field.dbName;
|
|
413
|
+
const colType = resolveColumnType({ field, enums: previous.enums });
|
|
414
|
+
let colDef = ` ${q({ name: colName })} ${colType}`;
|
|
415
|
+
if (field.kind === "scalar" && field.isRequired && field.default?.kind !== "autoincrement") {
|
|
416
|
+
colDef += " NOT NULL";
|
|
417
|
+
} else if (field.kind === "enum" && field.isRequired) {
|
|
418
|
+
colDef += " NOT NULL";
|
|
419
|
+
}
|
|
420
|
+
const defaultExpr = resolveDefault({ field });
|
|
421
|
+
if (defaultExpr) colDef += ` DEFAULT ${defaultExpr}`;
|
|
422
|
+
if (field.isUnique) colDef += " UNIQUE";
|
|
423
|
+
downColumns.push(colDef);
|
|
424
|
+
}
|
|
425
|
+
const downPkFields = model.primaryKey.fields
|
|
426
|
+
.map((f: string) => {
|
|
427
|
+
const fld = model.fields.find((mf: Field) => mf.name === f);
|
|
428
|
+
if (fld && fld.kind !== "relation") return q({ name: fld.dbName });
|
|
429
|
+
return q({ name: f });
|
|
430
|
+
})
|
|
431
|
+
.join(", ");
|
|
432
|
+
downColumns.push(` CONSTRAINT ${q({ name: `${dropTableName}_pkey` })} PRIMARY KEY (${downPkFields})`);
|
|
433
|
+
|
|
383
434
|
ops.push({
|
|
384
435
|
type: "dropTable",
|
|
385
436
|
sql: `DROP TABLE IF EXISTS ${q({ name: model.dbName })} CASCADE;`,
|
|
437
|
+
downSql: `CREATE TABLE ${q({ name: dropTableName })} (\n${downColumns.join(",\n")}\n);`,
|
|
386
438
|
isDestructive: true,
|
|
439
|
+
isReversible: true,
|
|
387
440
|
description: `Drop table "${name}"`,
|
|
388
441
|
});
|
|
389
442
|
}
|
|
@@ -402,7 +455,7 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
402
455
|
getScalarFields({ model: currModel }).map((f) => [f.dbName, f])
|
|
403
456
|
);
|
|
404
457
|
|
|
405
|
-
// Added columns
|
|
458
|
+
// Added columns — reverse is dropping the column
|
|
406
459
|
for (const [fieldName, field] of currFields) {
|
|
407
460
|
if (prevFields.has(fieldName)) continue;
|
|
408
461
|
|
|
@@ -424,18 +477,32 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
424
477
|
ops.push({
|
|
425
478
|
type: "addColumn",
|
|
426
479
|
sql: `ALTER TABLE ${q({ name: tableName })} ADD COLUMN ${q({ name: colName })} ${colDef};`,
|
|
480
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} DROP COLUMN IF EXISTS ${q({ name: colName })};`,
|
|
427
481
|
isDestructive,
|
|
482
|
+
isReversible: true,
|
|
428
483
|
description: `Add column "${fieldName}" to "${name}"`,
|
|
429
484
|
});
|
|
430
485
|
}
|
|
431
486
|
|
|
432
|
-
// Dropped columns
|
|
487
|
+
// Dropped columns — reverse is re-adding the column with its previous definition
|
|
433
488
|
for (const [fieldName, field] of prevFields) {
|
|
434
489
|
if (currFields.has(fieldName)) continue;
|
|
490
|
+
|
|
491
|
+
const colName = field.dbName;
|
|
492
|
+
const colType = resolveColumnType({ field, enums: previous.enums });
|
|
493
|
+
const defaultExpr = resolveDefault({ field });
|
|
494
|
+
// Down migration always adds the column back as nullable because
|
|
495
|
+
// the original data is lost after DROP COLUMN — adding NOT NULL
|
|
496
|
+
// would fail if the table has existing rows with null values.
|
|
497
|
+
let downColDef = `${colType}`;
|
|
498
|
+
if (defaultExpr) downColDef += ` DEFAULT ${defaultExpr}`;
|
|
499
|
+
|
|
435
500
|
ops.push({
|
|
436
501
|
type: "dropColumn",
|
|
437
502
|
sql: `ALTER TABLE ${q({ name: tableName })} DROP COLUMN IF EXISTS ${q({ name: field.dbName })};`,
|
|
503
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} ADD COLUMN ${q({ name: colName })} ${downColDef};`,
|
|
438
504
|
isDestructive: true,
|
|
505
|
+
isReversible: true,
|
|
439
506
|
description: `Drop column "${fieldName}" from "${name}"`,
|
|
440
507
|
});
|
|
441
508
|
}
|
|
@@ -449,51 +516,67 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
449
516
|
const prevType = resolveColumnType({ field: prevField, enums: previous.enums });
|
|
450
517
|
const currType = resolveColumnType({ field: currField, enums: current.enums });
|
|
451
518
|
|
|
452
|
-
// Type change
|
|
519
|
+
// Type change — reverse is setting back to the previous type
|
|
453
520
|
if (prevType !== currType) {
|
|
454
521
|
ops.push({
|
|
455
522
|
type: "alterColumnType",
|
|
456
523
|
sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DATA TYPE ${currType};`,
|
|
524
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DATA TYPE ${prevType};`,
|
|
457
525
|
isDestructive: true,
|
|
526
|
+
isReversible: true,
|
|
458
527
|
description: `Change type of "${fieldName}" in "${name}" from ${prevType} to ${currType}`,
|
|
459
528
|
});
|
|
460
529
|
}
|
|
461
530
|
|
|
462
|
-
// Nullability change
|
|
531
|
+
// Nullability change — reverse is toggling back
|
|
463
532
|
if (prevField.isRequired !== currField.isRequired) {
|
|
464
533
|
if (currField.isRequired) {
|
|
465
534
|
ops.push({
|
|
466
535
|
type: "alterColumnNullability",
|
|
467
536
|
sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET NOT NULL;`,
|
|
537
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} DROP NOT NULL;`,
|
|
468
538
|
isDestructive: true,
|
|
539
|
+
isReversible: true,
|
|
469
540
|
description: `Set NOT NULL on "${fieldName}" in "${name}"`,
|
|
470
541
|
});
|
|
471
542
|
} else {
|
|
472
543
|
ops.push({
|
|
473
544
|
type: "alterColumnNullability",
|
|
474
545
|
sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} DROP NOT NULL;`,
|
|
546
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET NOT NULL;`,
|
|
475
547
|
isDestructive: false,
|
|
548
|
+
isReversible: true,
|
|
476
549
|
description: `Drop NOT NULL on "${fieldName}" in "${name}"`,
|
|
477
550
|
});
|
|
478
551
|
}
|
|
479
552
|
}
|
|
480
553
|
|
|
481
|
-
// Default change
|
|
554
|
+
// Default change — reverse is restoring the previous default
|
|
482
555
|
const prevDefault = resolveDefault({ field: prevField });
|
|
483
556
|
const currDefault = resolveDefault({ field: currField });
|
|
484
557
|
if (prevDefault !== currDefault) {
|
|
485
558
|
if (currDefault) {
|
|
559
|
+
const downDefaultSql = prevDefault
|
|
560
|
+
? `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DEFAULT ${prevDefault};`
|
|
561
|
+
: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} DROP DEFAULT;`;
|
|
486
562
|
ops.push({
|
|
487
563
|
type: "alterColumnDefault",
|
|
488
564
|
sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DEFAULT ${currDefault};`,
|
|
565
|
+
downSql: downDefaultSql,
|
|
489
566
|
isDestructive: false,
|
|
567
|
+
isReversible: true,
|
|
490
568
|
description: `Set default on "${fieldName}" in "${name}" to ${currDefault}`,
|
|
491
569
|
});
|
|
492
570
|
} else {
|
|
571
|
+
const downDefaultSql = prevDefault
|
|
572
|
+
? `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DEFAULT ${prevDefault};`
|
|
573
|
+
: `-- No previous default to restore`;
|
|
493
574
|
ops.push({
|
|
494
575
|
type: "alterColumnDefault",
|
|
495
576
|
sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} DROP DEFAULT;`,
|
|
577
|
+
downSql: downDefaultSql,
|
|
496
578
|
isDestructive: false,
|
|
579
|
+
isReversible: true,
|
|
497
580
|
description: `Drop default on "${fieldName}" in "${name}"`,
|
|
498
581
|
});
|
|
499
582
|
}
|
|
@@ -517,19 +600,24 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
517
600
|
ops.push({
|
|
518
601
|
type: "addUnique",
|
|
519
602
|
sql: `CREATE UNIQUE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
|
|
603
|
+
downSql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
|
|
520
604
|
isDestructive: false,
|
|
605
|
+
isReversible: true,
|
|
521
606
|
description: `Add unique constraint on (${uc.fields.join(", ")}) in "${name}"`,
|
|
522
607
|
});
|
|
523
608
|
}
|
|
524
609
|
|
|
525
610
|
for (const [key, uc] of prevUniques) {
|
|
526
611
|
if (currUniques.has(key)) continue;
|
|
612
|
+
const cols = uc.fields.map((f: string) => q({ name: resolveFieldDbName({ model: prevModel, fieldName: f }) })).join(", ");
|
|
527
613
|
const resolvedFieldNames = uc.fields.map((f: string) => resolveFieldDbName({ model: prevModel, fieldName: f }));
|
|
528
614
|
const idxName = uc.name ?? `${tableName}_${resolvedFieldNames.join("_")}_key`;
|
|
529
615
|
ops.push({
|
|
530
616
|
type: "dropUnique",
|
|
531
617
|
sql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
|
|
618
|
+
downSql: `CREATE UNIQUE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
|
|
532
619
|
isDestructive: true,
|
|
620
|
+
isReversible: true,
|
|
533
621
|
description: `Drop unique constraint on (${uc.fields.join(", ")}) in "${name}"`,
|
|
534
622
|
});
|
|
535
623
|
}
|
|
@@ -551,19 +639,24 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
551
639
|
ops.push({
|
|
552
640
|
type: "addIndex",
|
|
553
641
|
sql: `CREATE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
|
|
642
|
+
downSql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
|
|
554
643
|
isDestructive: false,
|
|
644
|
+
isReversible: true,
|
|
555
645
|
description: `Add index on (${idx.fields.join(", ")}) in "${name}"`,
|
|
556
646
|
});
|
|
557
647
|
}
|
|
558
648
|
|
|
559
649
|
for (const [key, idx] of prevIndexes) {
|
|
560
650
|
if (currIndexes.has(key)) continue;
|
|
651
|
+
const cols = idx.fields.map((f: string) => q({ name: resolveFieldDbName({ model: prevModel, fieldName: f }) })).join(", ");
|
|
561
652
|
const resolvedFieldNames = idx.fields.map((f: string) => resolveFieldDbName({ model: prevModel, fieldName: f }));
|
|
562
653
|
const idxName = idx.name ?? `${tableName}_${resolvedFieldNames.join("_")}_idx`;
|
|
563
654
|
ops.push({
|
|
564
655
|
type: "dropIndex",
|
|
565
656
|
sql: `DROP INDEX IF EXISTS ${q({ name: idxName })};`,
|
|
657
|
+
downSql: `CREATE INDEX ${q({ name: idxName })} ON ${q({ name: tableName })} (${cols});`,
|
|
566
658
|
isDestructive: true,
|
|
659
|
+
isReversible: true,
|
|
567
660
|
description: `Drop index on (${idx.fields.join(", ")}) in "${name}"`,
|
|
568
661
|
});
|
|
569
662
|
}
|
|
@@ -595,19 +688,30 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
595
688
|
ops.push({
|
|
596
689
|
type: "addForeignKey",
|
|
597
690
|
sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ON DELETE RESTRICT ON UPDATE CASCADE;`,
|
|
691
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
|
|
598
692
|
isDestructive: false,
|
|
693
|
+
isReversible: true,
|
|
599
694
|
description: `Add foreign key on "${name}" (${rel.relation.fields.join(", ")}) → "${rel.relatedModel}"`,
|
|
600
695
|
});
|
|
601
696
|
}
|
|
602
697
|
|
|
603
698
|
for (const [key, rel] of prevFKs) {
|
|
604
699
|
if (currFKs.has(key)) continue;
|
|
700
|
+
const fkCols = rel.relation.fields.map((f: string) => q({ name: resolveFieldDbName({ model: prevModel, fieldName: f }) })).join(", ");
|
|
701
|
+
const relatedModel = previous.models.find((m: Model) => m.name === rel.relatedModel);
|
|
702
|
+
const relatedTableName = relatedModel?.dbName ?? rel.relatedModel;
|
|
703
|
+
const refCols = rel.relation.references.map((f: string) => {
|
|
704
|
+
if (relatedModel) return q({ name: resolveFieldDbName({ model: relatedModel, fieldName: f }) });
|
|
705
|
+
return q({ name: f });
|
|
706
|
+
}).join(", ");
|
|
605
707
|
const resolvedFkNames = rel.relation.fields.map((f: string) => resolveFieldDbName({ model: prevModel, fieldName: f }));
|
|
606
708
|
const constraintName = `${tableName}_${resolvedFkNames.join("_")}_fkey`;
|
|
607
709
|
ops.push({
|
|
608
710
|
type: "dropForeignKey",
|
|
609
711
|
sql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
|
|
712
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ON DELETE RESTRICT ON UPDATE CASCADE;`,
|
|
610
713
|
isDestructive: true,
|
|
714
|
+
isReversible: true,
|
|
611
715
|
description: `Drop foreign key on "${name}" (${rel.relation.fields.join(", ")}) → "${rel.relatedModel}"`,
|
|
612
716
|
});
|
|
613
717
|
}
|
|
@@ -654,17 +758,50 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
654
758
|
`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;`,
|
|
655
759
|
`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;`,
|
|
656
760
|
].join("\n"),
|
|
761
|
+
downSql: `DROP TABLE IF EXISTS ${q({ name: jt.tableName })} CASCADE;`,
|
|
657
762
|
isDestructive: false,
|
|
763
|
+
isReversible: true,
|
|
658
764
|
description: `Create join table "${jt.tableName}" for ${jt.modelA} ↔ ${jt.modelB}`,
|
|
659
765
|
});
|
|
660
766
|
}
|
|
661
767
|
|
|
662
768
|
for (const jt of prevJoinTables) {
|
|
663
769
|
if (currJoinSet.has(jt.tableName)) continue;
|
|
770
|
+
|
|
771
|
+
// For the down migration, reconstruct the join table from the previous schema
|
|
772
|
+
const prevModelADef = prevModelMap.get(jt.modelA);
|
|
773
|
+
const prevModelBDef = prevModelMap.get(jt.modelB);
|
|
774
|
+
let downJoinSql = `-- WARNING: Cannot fully reconstruct join table "${jt.tableName}". Manual intervention required.`;
|
|
775
|
+
|
|
776
|
+
if (prevModelADef && prevModelBDef) {
|
|
777
|
+
const prevPkFieldA = prevModelADef.primaryKey.fields[0]!;
|
|
778
|
+
const prevPkFieldB = prevModelBDef.primaryKey.fields[0]!;
|
|
779
|
+
const prevPkFieldDefA = prevModelADef.fields.find((f: Field) => f.name === prevPkFieldA);
|
|
780
|
+
const prevPkFieldDefB = prevModelBDef.fields.find((f: Field) => f.name === prevPkFieldB);
|
|
781
|
+
const prevPkTypeA = prevPkFieldDefA && (prevPkFieldDefA.kind === "scalar" || prevPkFieldDefA.kind === "enum")
|
|
782
|
+
? resolveBaseColumnType({ field: prevPkFieldDefA, enums: previous.enums })
|
|
783
|
+
: "INTEGER";
|
|
784
|
+
const prevPkTypeB = prevPkFieldDefB && (prevPkFieldDefB.kind === "scalar" || prevPkFieldDefB.kind === "enum")
|
|
785
|
+
? resolveBaseColumnType({ field: prevPkFieldDefB, enums: previous.enums })
|
|
786
|
+
: "INTEGER";
|
|
787
|
+
const prevTableA = prevModelADef.dbName;
|
|
788
|
+
const prevTableB = prevModelBDef.dbName;
|
|
789
|
+
|
|
790
|
+
downJoinSql = [
|
|
791
|
+
`CREATE TABLE ${q({ name: jt.tableName })} (\n "A" ${prevPkTypeA} NOT NULL,\n "B" ${prevPkTypeB} NOT NULL\n);`,
|
|
792
|
+
`CREATE UNIQUE INDEX ${q({ name: `${jt.tableName}_AB_unique` })} ON ${q({ name: jt.tableName })} ("A", "B");`,
|
|
793
|
+
`CREATE INDEX ${q({ name: `${jt.tableName}_B_index` })} ON ${q({ name: jt.tableName })} ("B");`,
|
|
794
|
+
`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;`,
|
|
795
|
+
`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;`,
|
|
796
|
+
].join("\n");
|
|
797
|
+
}
|
|
798
|
+
|
|
664
799
|
ops.push({
|
|
665
800
|
type: "dropJoinTable",
|
|
666
801
|
sql: `DROP TABLE IF EXISTS ${q({ name: jt.tableName })} CASCADE;`,
|
|
802
|
+
downSql: downJoinSql,
|
|
667
803
|
isDestructive: true,
|
|
804
|
+
isReversible: prevModelADef !== undefined && prevModelBDef !== undefined,
|
|
668
805
|
description: `Drop join table "${jt.tableName}" for ${jt.modelA} ↔ ${jt.modelB}`,
|
|
669
806
|
});
|
|
670
807
|
}
|