@quereus/quereus 4.19.2 → 4.20.0

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 (116) hide show
  1. package/README.md +257 -257
  2. package/dist/src/common/errors.d.ts +10 -0
  3. package/dist/src/common/errors.d.ts.map +1 -1
  4. package/dist/src/common/errors.js +14 -0
  5. package/dist/src/common/errors.js.map +1 -1
  6. package/dist/src/common/types.d.ts +12 -0
  7. package/dist/src/common/types.d.ts.map +1 -1
  8. package/dist/src/common/types.js.map +1 -1
  9. package/dist/src/core/database-events.d.ts.map +1 -1
  10. package/dist/src/core/database-events.js +12 -9
  11. package/dist/src/core/database-events.js.map +1 -1
  12. package/dist/src/core/database-transaction.d.ts.map +1 -1
  13. package/dist/src/core/database-transaction.js +28 -19
  14. package/dist/src/core/database-transaction.js.map +1 -1
  15. package/dist/src/core/database.d.ts +58 -6
  16. package/dist/src/core/database.d.ts.map +1 -1
  17. package/dist/src/core/database.js +185 -54
  18. package/dist/src/core/database.js.map +1 -1
  19. package/dist/src/core/param.d.ts +46 -1
  20. package/dist/src/core/param.d.ts.map +1 -1
  21. package/dist/src/core/param.js +87 -7
  22. package/dist/src/core/param.js.map +1 -1
  23. package/dist/src/core/statement.d.ts +66 -1
  24. package/dist/src/core/statement.d.ts.map +1 -1
  25. package/dist/src/core/statement.js +156 -91
  26. package/dist/src/core/statement.js.map +1 -1
  27. package/dist/src/index.d.ts +3 -3
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +2 -2
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/parser/lexer.d.ts.map +1 -1
  32. package/dist/src/parser/lexer.js +9 -3
  33. package/dist/src/parser/lexer.js.map +1 -1
  34. package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
  35. package/dist/src/planner/analysis/change-scope.js +7 -20
  36. package/dist/src/planner/analysis/change-scope.js.map +1 -1
  37. package/dist/src/planner/analysis/check-extraction.d.ts +62 -0
  38. package/dist/src/planner/analysis/check-extraction.d.ts.map +1 -1
  39. package/dist/src/planner/analysis/check-extraction.js +19 -5
  40. package/dist/src/planner/analysis/check-extraction.js.map +1 -1
  41. package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
  42. package/dist/src/planner/analysis/constraint-extractor.js +7 -0
  43. package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
  44. package/dist/src/planner/analysis/scalar-param-usage.d.ts +6 -3
  45. package/dist/src/planner/analysis/scalar-param-usage.d.ts.map +1 -1
  46. package/dist/src/planner/analysis/scalar-param-usage.js +32 -4
  47. package/dist/src/planner/analysis/scalar-param-usage.js.map +1 -1
  48. package/dist/src/planner/building/coercion.js +1 -1
  49. package/dist/src/planner/building/coercion.js.map +1 -1
  50. package/dist/src/planner/building/schema-authored-context.d.ts +7 -7
  51. package/dist/src/planner/building/schema-authored-context.js +7 -7
  52. package/dist/src/planner/nodes/scalar.d.ts +19 -1
  53. package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
  54. package/dist/src/planner/nodes/scalar.js +13 -2
  55. package/dist/src/planner/nodes/scalar.js.map +1 -1
  56. package/dist/src/planner/nodes/set-operation-node.js +4 -0
  57. package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
  58. package/dist/src/runtime/emit/add-constraint.js +44 -1
  59. package/dist/src/runtime/emit/add-constraint.js.map +1 -1
  60. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  61. package/dist/src/runtime/emit/alter-table.js +20 -32
  62. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  63. package/dist/src/runtime/emit/ddl-event-scope.d.ts +20 -13
  64. package/dist/src/runtime/emit/ddl-event-scope.d.ts.map +1 -1
  65. package/dist/src/runtime/emit/ddl-event-scope.js +20 -13
  66. package/dist/src/runtime/emit/ddl-event-scope.js.map +1 -1
  67. package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
  68. package/dist/src/runtime/emit/materialized-view-helpers.js +9 -4
  69. package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
  70. package/dist/src/runtime/emit/parameter.d.ts.map +1 -1
  71. package/dist/src/runtime/emit/parameter.js +13 -8
  72. package/dist/src/runtime/emit/parameter.js.map +1 -1
  73. package/dist/src/runtime/emit/schema-declarative.d.ts +17 -6
  74. package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
  75. package/dist/src/runtime/emit/schema-declarative.js +169 -31
  76. package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
  77. package/dist/src/schema/catalog-rendering.d.ts +26 -0
  78. package/dist/src/schema/catalog-rendering.d.ts.map +1 -1
  79. package/dist/src/schema/catalog-rendering.js +41 -4
  80. package/dist/src/schema/catalog-rendering.js.map +1 -1
  81. package/dist/src/schema/catalog.d.ts +23 -0
  82. package/dist/src/schema/catalog.d.ts.map +1 -1
  83. package/dist/src/schema/catalog.js +6 -4
  84. package/dist/src/schema/catalog.js.map +1 -1
  85. package/dist/src/schema/constraint-builder.d.ts +67 -11
  86. package/dist/src/schema/constraint-builder.d.ts.map +1 -1
  87. package/dist/src/schema/constraint-builder.js +115 -12
  88. package/dist/src/schema/constraint-builder.js.map +1 -1
  89. package/dist/src/schema/ddl-generator.d.ts +21 -0
  90. package/dist/src/schema/ddl-generator.d.ts.map +1 -1
  91. package/dist/src/schema/ddl-generator.js +7 -1
  92. package/dist/src/schema/ddl-generator.js.map +1 -1
  93. package/dist/src/schema/rename/self-qualifier-strip.d.ts +27 -0
  94. package/dist/src/schema/rename/self-qualifier-strip.d.ts.map +1 -1
  95. package/dist/src/schema/rename/self-qualifier-strip.js +73 -0
  96. package/dist/src/schema/rename/self-qualifier-strip.js.map +1 -1
  97. package/dist/src/schema/rename-rewriter.d.ts +5 -5
  98. package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
  99. package/dist/src/schema/rename-rewriter.js +5 -5
  100. package/dist/src/schema/rename-rewriter.js.map +1 -1
  101. package/dist/src/schema/schema-differ.d.ts +41 -1
  102. package/dist/src/schema/schema-differ.d.ts.map +1 -1
  103. package/dist/src/schema/schema-differ.js +529 -36
  104. package/dist/src/schema/schema-differ.js.map +1 -1
  105. package/dist/src/types/temporal-ops.d.ts.map +1 -1
  106. package/dist/src/types/temporal-ops.js +16 -10
  107. package/dist/src/types/temporal-ops.js.map +1 -1
  108. package/dist/src/vtab/capabilities.d.ts +5 -2
  109. package/dist/src/vtab/capabilities.d.ts.map +1 -1
  110. package/dist/src/vtab/memory/layer/manager.d.ts +15 -11
  111. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  112. package/dist/src/vtab/memory/layer/manager.js +15 -11
  113. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  114. package/dist/src/vtab/module.d.ts +16 -0
  115. package/dist/src/vtab/module.d.ts.map +1 -1
  116. package/package.json +3 -3
@@ -6,6 +6,7 @@ import { createLogger } from '../common/logger.js';
6
6
  import { validateReservedTags } from './reserved-tags.js';
7
7
  import { raiseReservedTagDiagnostics } from './reserved-tags-policy.js';
8
8
  import { renameColumnInAst, renameColumnInCheckExpression, renameTableInAst, tableReferencedInAst, objectRefKey, singleSchemaObjectRefResolver } from './rename-rewriter.js';
9
+ import { Parser } from '../parser/parser.js';
9
10
  import { cloneExpr, cloneQueryExpr } from '../planner/mutation/scope-transform.js';
10
11
  import { normalizeCollationName } from '../util/comparison.js';
11
12
  import { inferType } from '../types/registry.js';
@@ -2374,8 +2375,8 @@ function orderDropsByFKDependency(dropSet, actualTables) {
2374
2375
  */
2375
2376
  function comparisonDomainAlters(quotedTable, quotedCol, change) {
2376
2377
  const prefix = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol}`;
2377
- const retype = change.dataType !== undefined ? [`${prefix} SET DATA TYPE ${change.dataType}`] : [];
2378
- const recollate = change.collation !== undefined ? [`${prefix} SET COLLATE ${change.collation}`] : [];
2378
+ const retype = change.dataType !== undefined ? [{ sql: `${prefix} SET DATA TYPE ${change.dataType}`, attribute: 'dataType' }] : [];
2379
+ const recollate = change.collation !== undefined ? [{ sql: `${prefix} SET COLLATE ${change.collation}`, attribute: 'collation' }] : [];
2379
2380
  return normalizeCollationName(change.collation ?? 'BINARY') === 'BINARY'
2380
2381
  ? [...recollate, ...retype]
2381
2382
  : [...retype, ...recollate];
@@ -2399,12 +2400,31 @@ export function generateMigrationDDL(diff, schemaName) {
2399
2400
  * them without re-lexing the text this function just produced. The remaining steps
2400
2401
  * are short template-built strings (renames, drops, column/constraint alters,
2401
2402
  * `SET TAGS`) and stay text-only.
2403
+ *
2404
+ * With `actual` — the pre-apply catalog `computeSchemaDiff` was handed — every step
2405
+ * also carries its reversal (`undo` / `irreversible`, see {@link MigrationStep}),
2406
+ * rendered by {@link UndoRenderer} at the step's own push site so a forward
2407
+ * statement and its undo read together. Without it nothing about the undo is
2408
+ * computed: `generateMigrationDDL` never passes a catalog, so `diff schema` output
2409
+ * and cost are untouched.
2402
2410
  */
2403
- export function generateMigrationPlan(diff, schemaName) {
2411
+ export function generateMigrationPlan(diff, schemaName, actual) {
2404
2412
  const statements = [];
2405
2413
  const schemaPrefix = (schemaName && schemaName !== 'main') ? `${quoteIdentifier(schemaName)}.` : '';
2406
- /** Appends a template-built step — text-only, so the apply path parses it as before. */
2407
- const pushText = (sql) => { statements.push({ sql }); };
2414
+ const undo = actual ? new UndoRenderer(schemaName ?? 'main', schemaPrefix, actual) : undefined;
2415
+ /**
2416
+ * Appends a template-built step — text-only, so the apply path parses it as
2417
+ * before. `undoOf` runs only when undo was requested, and renders the reversal
2418
+ * for the catalog state THIS step runs in (Rule 1 of {@link MigrationStep.undo}).
2419
+ */
2420
+ const pushText = (sql, undoOf) => {
2421
+ statements.push(undo ? { sql, ...undoOf(undo) } : { sql });
2422
+ };
2423
+ /** Appends a create bucket; each create undoes to the `DROP … IF EXISTS` of what it made. */
2424
+ const pushCreates = (bucket, kind) => {
2425
+ for (const create of bucket)
2426
+ statements.push(undo ? { ...create, ...undo.dropCreated(create, kind) } : create);
2427
+ };
2408
2428
  // Renames first — they free old names for subsequent creates and re-target
2409
2429
  // dependents (handled inside ALTER TABLE ... RENAME by the rename rewriter).
2410
2430
  // Only tables have a rename primitive. The other RenameOp kinds are metadata
@@ -2415,13 +2435,16 @@ export function generateMigrationPlan(diff, schemaName) {
2415
2435
  // the table-alter channel (RENAME CONSTRAINT below).
2416
2436
  for (const r of diff.renames) {
2417
2437
  if (r.kind === 'table') {
2418
- pushText(`ALTER TABLE ${schemaPrefix}${quoteIdentifier(r.oldName)} RENAME TO ${quoteIdentifier(r.newName)}`);
2438
+ pushText(`ALTER TABLE ${schemaPrefix}${quoteIdentifier(r.oldName)} RENAME TO ${quoteIdentifier(r.newName)}`, () => ({ undo: [`ALTER TABLE ${schemaPrefix}${quoteIdentifier(r.newName)} RENAME TO ${quoteIdentifier(r.oldName)}`] }));
2419
2439
  }
2420
2440
  // Non-table rename ops emit no DDL here — see the note above.
2421
2441
  }
2442
+ // Every later step runs — and is undone — with the table renames in force, so
2443
+ // from here on the undo renderer reads the pre-apply catalog through them.
2444
+ undo?.tableRenamesLanded(diff.renames);
2422
2445
  // Drop assertions first (they may reference tables)
2423
2446
  for (const name of diff.assertionsToDrop) {
2424
- pushText(`DROP ASSERTION IF EXISTS ${schemaPrefix}${quoteIdentifier(name)}`);
2447
+ pushText(`DROP ASSERTION IF EXISTS ${schemaPrefix}${quoteIdentifier(name)}`, u => u.recreateAssertion(name));
2425
2448
  }
2426
2449
  // Detach maintained tables (`drop maintained`) EARLY — where MV drops ran
2427
2450
  // before — so a detach precedes any column op on that table AND any drop of its
@@ -2430,25 +2453,25 @@ export function generateMigrationPlan(diff, schemaName) {
2430
2453
  // flip's cross-table detach-v2 / attach-v1 pair falls out of this ordering.
2431
2454
  for (const alter of diff.tablesToAlter) {
2432
2455
  if (alter.dropMaintained) {
2433
- pushText(`ALTER TABLE ${schemaPrefix}${quoteIdentifier(alter.tableName)} DROP MAINTAINED`);
2456
+ pushText(`ALTER TABLE ${schemaPrefix}${quoteIdentifier(alter.tableName)} DROP MAINTAINED`, u => u.reattachAsBefore(alter.tableName));
2434
2457
  }
2435
2458
  }
2436
2459
  // Drop items (reverse order)
2437
2460
  for (const tableName of diff.tablesToDrop) {
2438
- pushText(`DROP TABLE IF EXISTS ${schemaPrefix}${quoteIdentifier(tableName)}`);
2461
+ pushText(`DROP TABLE IF EXISTS ${schemaPrefix}${quoteIdentifier(tableName)}`, u => u.droppedTable(tableName));
2439
2462
  }
2440
2463
  for (const viewName of diff.viewsToDrop) {
2441
- pushText(`DROP VIEW IF EXISTS ${schemaPrefix}${quoteIdentifier(viewName)}`);
2464
+ pushText(`DROP VIEW IF EXISTS ${schemaPrefix}${quoteIdentifier(viewName)}`, u => u.recreateView(viewName));
2442
2465
  }
2443
2466
  for (const indexName of diff.indexesToDrop) {
2444
- pushText(`DROP INDEX IF EXISTS ${schemaPrefix}${quoteIdentifier(indexName)}`);
2467
+ pushText(`DROP INDEX IF EXISTS ${schemaPrefix}${quoteIdentifier(indexName)}`, u => u.recreateIndex(indexName));
2445
2468
  }
2446
2469
  // Create new items. A fresh maintained table rides `tablesToCreate` (rendered
2447
2470
  // as the `create materialized view` sugar) and re-materializes as part of its
2448
2471
  // create.
2449
- statements.push(...diff.tablesToCreate);
2450
- statements.push(...diff.viewsToCreate);
2451
- statements.push(...diff.indexesToCreate);
2472
+ pushCreates(diff.tablesToCreate, 'table');
2473
+ pushCreates(diff.viewsToCreate, 'view');
2474
+ pushCreates(diff.indexesToCreate, 'index');
2452
2475
  // Assertion creates do NOT go here — they run last, after the table alters and
2453
2476
  // the maintained re-attaches. See the push at the end of this function.
2454
2477
  // Alter existing tables.
@@ -2468,47 +2491,56 @@ export function generateMigrationPlan(diff, schemaName) {
2468
2491
  for (const alter of diff.tablesToAlter) {
2469
2492
  const quotedTable = `${schemaPrefix}${quoteIdentifier(alter.tableName)}`;
2470
2493
  for (const r of alter.columnsToRename) {
2471
- pushText(`ALTER TABLE ${quotedTable} RENAME COLUMN ${quoteIdentifier(r.oldName)} TO ${quoteIdentifier(r.newName)}`);
2494
+ pushText(`ALTER TABLE ${quotedTable} RENAME COLUMN ${quoteIdentifier(r.oldName)} TO ${quoteIdentifier(r.newName)}`, () => ({ undo: [`ALTER TABLE ${quotedTable} RENAME COLUMN ${quoteIdentifier(r.newName)} TO ${quoteIdentifier(r.oldName)}`] }));
2495
+ undo?.columnRenameLanded(alter.tableName, r);
2472
2496
  }
2473
2497
  for (const colDef of alter.columnsToAdd) {
2474
- pushText(`ALTER TABLE ${quotedTable} ADD COLUMN ${colDef}`);
2498
+ pushText(`ALTER TABLE ${quotedTable} ADD COLUMN ${colDef}`, u => u.dropAddedColumn(quotedTable, colDef));
2475
2499
  }
2476
2500
  for (const colAlter of alter.columnsToAlter) {
2477
2501
  const quotedCol = quoteIdentifier(colAlter.columnName);
2502
+ const alterColumn = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol}`;
2478
2503
  // A default the retype would choke on and this migration replaces anyway is
2479
2504
  // cleared BEFORE the comparison-domain phase (see `dropStaleDefaultFirst`).
2480
2505
  if (colAlter.dropStaleDefaultFirst) {
2481
- pushText(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} DROP DEFAULT`);
2506
+ pushText(`${alterColumn} DROP DEFAULT`, u => u.undoDropDefault(alter.tableName, colAlter.columnName, alterColumn));
2482
2507
  }
2483
2508
  // SET DATA TYPE / SET COLLATE lead the per-column phase (both are
2484
2509
  // comparison-domain changes), before DEFAULT / NOT NULL.
2485
- for (const s of comparisonDomainAlters(quotedTable, quotedCol, colAlter))
2486
- pushText(s);
2510
+ for (const s of comparisonDomainAlters(quotedTable, quotedCol, colAlter)) {
2511
+ pushText(s.sql, u => s.attribute === 'dataType'
2512
+ ? u.retypedColumn(alter.tableName, colAlter.columnName)
2513
+ : u.undoSetCollation(alter.tableName, colAlter.columnName, alterColumn));
2514
+ }
2487
2515
  if (colAlter.defaultValue !== undefined) {
2488
2516
  if (colAlter.defaultValue === null) {
2489
2517
  // Already dropped above when it preceded the retype.
2490
2518
  if (!colAlter.dropStaleDefaultFirst) {
2491
- pushText(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} DROP DEFAULT`);
2519
+ pushText(`${alterColumn} DROP DEFAULT`, u => u.undoDropDefault(alter.tableName, colAlter.columnName, alterColumn));
2492
2520
  }
2493
2521
  }
2494
2522
  else {
2495
- pushText(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET DEFAULT ${expressionToString(colAlter.defaultValue)}`);
2523
+ // After `dropStaleDefaultFirst` the column reaches this step with no
2524
+ // default — the stale one was cleared earlier in this phase — so the
2525
+ // reversal is a plain DROP DEFAULT, not the pre-apply default (Rule 1).
2526
+ pushText(`${alterColumn} SET DEFAULT ${expressionToString(colAlter.defaultValue)}`, u => colAlter.dropStaleDefaultFirst
2527
+ ? { undo: [`${alterColumn} DROP DEFAULT`] }
2528
+ : u.undoSetDefault(alter.tableName, colAlter.columnName, alterColumn));
2496
2529
  }
2497
2530
  }
2498
2531
  if (colAlter.notNull !== undefined) {
2499
- pushText(colAlter.notNull
2500
- ? `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET NOT NULL`
2501
- : `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} DROP NOT NULL`);
2532
+ pushText(colAlter.notNull ? `${alterColumn} SET NOT NULL` : `${alterColumn} DROP NOT NULL`, () => ({ undo: [colAlter.notNull ? `${alterColumn} DROP NOT NULL` : `${alterColumn} SET NOT NULL`] }));
2502
2533
  }
2503
2534
  }
2504
2535
  // Constraint lifecycle: RENAME (free a name) then DROP (remove a stale /
2505
2536
  // conflicting constraint), both BEFORE re-adds and before the PK change so a
2506
2537
  // dropped UNIQUE can't strand a PK dependency.
2507
2538
  for (const r of alter.constraintsToRename ?? []) {
2508
- pushText(`ALTER TABLE ${quotedTable} RENAME CONSTRAINT ${quoteIdentifier(r.oldName)} TO ${quoteIdentifier(r.newName)}`);
2539
+ pushText(`ALTER TABLE ${quotedTable} RENAME CONSTRAINT ${quoteIdentifier(r.oldName)} TO ${quoteIdentifier(r.newName)}`, () => ({ undo: [`ALTER TABLE ${quotedTable} RENAME CONSTRAINT ${quoteIdentifier(r.newName)} TO ${quoteIdentifier(r.oldName)}`] }));
2540
+ undo?.constraintRenameLanded(alter.tableName, r);
2509
2541
  }
2510
2542
  for (const name of alter.constraintsToDrop ?? []) {
2511
- pushText(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quoteIdentifier(name)}`);
2543
+ pushText(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quoteIdentifier(name)}`, u => u.readdConstraint(alter.tableName, name, quotedTable));
2512
2544
  }
2513
2545
  if (alter.primaryKeyChange) {
2514
2546
  const pkCols = alter.primaryKeyChange.newPkColumns
@@ -2519,16 +2551,16 @@ export function generateMigrationPlan(diff, schemaName) {
2519
2551
  return s;
2520
2552
  })
2521
2553
  .join(', ');
2522
- pushText(`ALTER TABLE ${quotedTable} ALTER PRIMARY KEY (${pkCols})`);
2554
+ pushText(`ALTER TABLE ${quotedTable} ALTER PRIMARY KEY (${pkCols})`, u => u.restorePrimaryKey(alter.tableName, quotedTable));
2523
2555
  }
2524
2556
  // ADD CONSTRAINT after the PK change (a new UNIQUE / FK may align with the new
2525
2557
  // key) and after the column adds it may reference. CHECK adds apply in-place;
2526
2558
  // UNIQUE / FK adds depend on module ADD CONSTRAINT support (see constraintsToAdd).
2527
2559
  for (const frag of alter.constraintsToAdd ?? []) {
2528
- pushText(`ALTER TABLE ${quotedTable} ADD ${frag}`);
2560
+ pushText(`ALTER TABLE ${quotedTable} ADD ${frag}`, u => u.dropAddedConstraint(quotedTable, frag));
2529
2561
  }
2530
2562
  for (const colName of alter.columnsToDrop) {
2531
- pushText(`ALTER TABLE ${quotedTable} DROP COLUMN ${quoteIdentifier(colName)}`);
2563
+ pushText(`ALTER TABLE ${quotedTable} DROP COLUMN ${quoteIdentifier(colName)}`, u => u.droppedColumn(alter.tableName, colName));
2532
2564
  }
2533
2565
  // Tags phase — last, so a SET TAGS lands on the post-structural column /
2534
2566
  // constraint set (a tag set emitted alongside a RENAME COLUMN targets the
@@ -2539,15 +2571,17 @@ export function generateMigrationPlan(diff, schemaName) {
2539
2571
  // TABLE handler rejects a tag action on a maintained table). See
2540
2572
  // `TableAlterDiff.maintainedTags`.
2541
2573
  const tagVerb = alter.maintainedTags ? 'ALTER MATERIALIZED VIEW' : 'ALTER TABLE';
2542
- pushText(`${tagVerb} ${quotedTable} SET TAGS ${tagsBodyToString(alter.tableTagsChange)}`);
2574
+ pushText(`${tagVerb} ${quotedTable} SET TAGS ${tagsBodyToString(alter.tableTagsChange)}`, u => u.restoreTableTags(alter.tableName, `${tagVerb} ${quotedTable}`));
2543
2575
  }
2544
2576
  for (const colAlter of alter.columnsToAlter) {
2545
2577
  if (colAlter.tags === undefined)
2546
2578
  continue;
2547
- pushText(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quoteIdentifier(colAlter.columnName)} SET TAGS ${tagsBodyToString(colAlter.tags)}`);
2579
+ const alterColumn = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quoteIdentifier(colAlter.columnName)}`;
2580
+ pushText(`${alterColumn} SET TAGS ${tagsBodyToString(colAlter.tags)}`, u => u.restoreColumnTags(alter.tableName, colAlter.columnName, alterColumn));
2548
2581
  }
2549
2582
  for (const ctc of alter.constraintTagsChanges ?? []) {
2550
- pushText(`ALTER TABLE ${quotedTable} ALTER CONSTRAINT ${quoteIdentifier(ctc.constraintName)} SET TAGS ${tagsBodyToString(ctc.tags)}`);
2583
+ const alterConstraint = `ALTER TABLE ${quotedTable} ALTER CONSTRAINT ${quoteIdentifier(ctc.constraintName)}`;
2584
+ pushText(`${alterConstraint} SET TAGS ${tagsBodyToString(ctc.tags)}`, u => u.restoreConstraintTags(alter.tableName, ctc.constraintName, alterConstraint));
2551
2585
  }
2552
2586
  }
2553
2587
  // Attach / re-attach maintained tables (`set maintained as`) LATE — where MV
@@ -2575,7 +2609,8 @@ export function generateMigrationPlan(diff, schemaName) {
2575
2609
  select: alter.setMaintained.select,
2576
2610
  },
2577
2611
  };
2578
- statements.push({ sql: astToString(stmt), ast: stmt });
2612
+ const step = { sql: astToString(stmt), ast: stmt };
2613
+ statements.push(undo ? { ...step, ...undo.undoSetMaintained(alter) } : step);
2579
2614
  }
2580
2615
  // Assertion creates LAST — after every table alter and maintained re-attach, so
2581
2616
  // a body sees the final shape of every object. `CREATE ASSERTION` plans its
@@ -2584,7 +2619,7 @@ export function generateMigrationPlan(diff, schemaName) {
2584
2619
  // create ran in the create block, before `ADD COLUMN`. Nothing in a migration
2585
2620
  // depends on an assertion existing, so last is strictly safer. Assertion DROPs
2586
2621
  // stay first (see the top of this function).
2587
- statements.push(...diff.assertionsToCreate);
2622
+ pushCreates(diff.assertionsToCreate, 'assertion');
2588
2623
  // In-place tag changes on views / indexes. These are leaf metadata writes (no
2589
2624
  // dependency ordering vs the table-alter block). The `?? []` keeps
2590
2625
  // generateMigrationDDL robust against hand-built diffs (some tests construct
@@ -2592,11 +2627,469 @@ export function generateMigrationPlan(diff, schemaName) {
2592
2627
  // table's tag-only change rides the table-alter block above (`SET TAGS`), not a
2593
2628
  // separate bucket.
2594
2629
  for (const vtc of diff.viewTagsChanges ?? []) {
2595
- pushText(`ALTER VIEW ${schemaPrefix}${quoteIdentifier(vtc.name)} SET TAGS ${tagsBodyToString(vtc.tags)}`);
2630
+ const alterView = `ALTER VIEW ${schemaPrefix}${quoteIdentifier(vtc.name)}`;
2631
+ pushText(`${alterView} SET TAGS ${tagsBodyToString(vtc.tags)}`, u => u.restoreViewTags(vtc.name, alterView));
2596
2632
  }
2597
2633
  for (const itc of diff.indexTagsChanges ?? []) {
2598
- pushText(`ALTER INDEX ${schemaPrefix}${quoteIdentifier(itc.name)} SET TAGS ${tagsBodyToString(itc.tags)}`);
2634
+ const alterIndex = `ALTER INDEX ${schemaPrefix}${quoteIdentifier(itc.name)}`;
2635
+ pushText(`${alterIndex} SET TAGS ${tagsBodyToString(itc.tags)}`, u => u.restoreIndexTags(itc.name, alterIndex));
2599
2636
  }
2600
2637
  return statements;
2601
2638
  }
2639
+ /** A step that changed nothing, so nothing has to be put back. */
2640
+ const NOTHING_TO_UNDO = { undo: [] };
2641
+ /** Case-insensitive identifier equality — how the catalog and the rename walkers compare names. */
2642
+ const sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
2643
+ /**
2644
+ * The pre-apply spelling of a name as spelled NOW, given the renames in force:
2645
+ * the old name when a rename landed on it, the name itself when none touched it,
2646
+ * and `undefined` when a rename vacated it — nothing pre-apply answers to it any more.
2647
+ */
2648
+ function preRenameName(renames, currentName) {
2649
+ const landed = renames.find(r => sameName(r.newName, currentName));
2650
+ if (landed)
2651
+ return landed.oldName;
2652
+ return renames.some(r => sameName(r.oldName, currentName)) ? undefined : currentName;
2653
+ }
2654
+ /**
2655
+ * Renders each migration step's undo from the pre-apply catalog. The two rules
2656
+ * {@link MigrationStep.undo} states are what every arm below honours:
2657
+ *
2658
+ * **Rule 1 — spell the target the way the forward step did.** The unwind runs in
2659
+ * reverse, so when a step's undo runs every later step has already been taken back
2660
+ * and the catalog is in the state the step itself left behind. The plan is pushed in
2661
+ * execution order and this renderer is told about each rename as its step lands
2662
+ * (`tableRenamesLanded`, `columnRenameLanded`, `constraintRenameLanded`), so every
2663
+ * catalog lookup goes through exactly the renames in force at that step — a later
2664
+ * `ALTER TABLE t …` step's undo names the post-rename `t`, and finds the pre-apply
2665
+ * table under its old name.
2666
+ *
2667
+ * **Rule 2 — forward-apply the renames in force to anything taken from the catalog.**
2668
+ * A restored constraint / view / assertion / derivation body, a column default and
2669
+ * the old primary key are spelled with pre-rename names in the catalog; by Rule 1
2670
+ * those renames are still in force when the undo runs, so {@link replayRenames}
2671
+ * applies them forward — through the same `renameTableInAst` /
2672
+ * `renameColumnInAst` / `renameColumnInCheckExpression` walkers the live rename
2673
+ * propagation uses — before the body is emitted. This is the differ's
2674
+ * rename-reconciled comparison (`reconciledDeclaredBody`) pointed the other way.
2675
+ * Only renames whose steps PRECEDE this one count: a view dropped before the
2676
+ * column-rename phase is restored with the old column names, because by the time
2677
+ * its undo runs the column rename has already been reversed.
2678
+ *
2679
+ * Two policies keep the plan generator total over hand-built inputs:
2680
+ * - a target the pre-apply catalog does not mention undoes to nothing — the
2681
+ * forward step either is a no-op (`IF EXISTS`) or fails before changing anything;
2682
+ * - a catalog entry that lacks what its restore needs (no body AST, DDL that does
2683
+ * not parse) is reported as `irreversible` with the reason, never thrown.
2684
+ *
2685
+ * NOTE: the undo is rendered eagerly for every step at plan time, so once the apply
2686
+ * path passes its catalog every apply pays it; the only non-trivial per-step cost is
2687
+ * one small parse on the fragment arms (dropped index, added column / constraint).
2688
+ * If plan generation ever shows up in profiles, render lazily on the failure path.
2689
+ */
2690
+ class UndoRenderer {
2691
+ schemaName;
2692
+ schemaPrefix;
2693
+ tables = new Map();
2694
+ views = new Map();
2695
+ indexes = new Map();
2696
+ assertions = new Map();
2697
+ resolveRef;
2698
+ tableRenames = [];
2699
+ columnRenames = [];
2700
+ constraintRenames = [];
2701
+ constructor(schemaName, schemaPrefix, actual) {
2702
+ this.schemaName = schemaName;
2703
+ this.schemaPrefix = schemaPrefix;
2704
+ for (const t of actual.tables ?? [])
2705
+ this.tables.set(t.name.toLowerCase(), t);
2706
+ for (const v of actual.views ?? [])
2707
+ this.views.set(v.name.toLowerCase(), v);
2708
+ // NOTE: an index name is unique per table, not per schema, and the plan's
2709
+ // `DROP INDEX` names it bare — so the first entry wins here, as it does there.
2710
+ // If `indexesToDrop` ever carries the owning table, key this map by both.
2711
+ for (const i of actual.indexes ?? [])
2712
+ if (!this.indexes.has(i.name.toLowerCase()))
2713
+ this.indexes.set(i.name.toLowerCase(), i);
2714
+ for (const a of actual.assertions ?? [])
2715
+ this.assertions.set(a.name.toLowerCase(), a);
2716
+ this.resolveRef = singleSchemaObjectRefResolver(schemaName);
2717
+ }
2718
+ // --- Rename bookkeeping (Rule 1) ---
2719
+ tableRenamesLanded(renames) {
2720
+ this.tableRenames = renames.filter(r => r.kind === 'table');
2721
+ }
2722
+ columnRenameLanded(table, r) {
2723
+ this.columnRenames.push({ table, oldName: r.oldName, newName: r.newName });
2724
+ }
2725
+ constraintRenameLanded(table, r) {
2726
+ this.constraintRenames.push({ table, oldName: r.oldName, newName: r.newName });
2727
+ }
2728
+ // --- Arms ---
2729
+ dropCreated(create, kind) {
2730
+ const name = createdObjectName(create.ast, kind);
2731
+ if (name === undefined)
2732
+ return this.cannotUndo(create.sql, `its statement is not a create ${kind}`);
2733
+ const verb = { table: 'DROP TABLE', view: 'DROP VIEW', index: 'DROP INDEX', assertion: 'DROP ASSERTION' }[kind];
2734
+ return { undo: [`${verb} IF EXISTS ${this.schemaPrefix}${quoteIdentifier(name)}`] };
2735
+ }
2736
+ droppedTable(actualName) {
2737
+ if (!this.tables.has(actualName.toLowerCase()))
2738
+ return NOTHING_TO_UNDO;
2739
+ return { irreversible: `dropping table "${actualName}" also discards its rows, and no statement can bring them back` };
2740
+ }
2741
+ droppedColumn(table, column) {
2742
+ if (!this.columnNow(table, column))
2743
+ return NOTHING_TO_UNDO;
2744
+ return { irreversible: `dropping column "${column}" from table "${table}" discards the values stored in it, and no statement can bring them back` };
2745
+ }
2746
+ retypedColumn(table, column) {
2747
+ if (!this.columnNow(table, column))
2748
+ return NOTHING_TO_UNDO;
2749
+ return { irreversible: `changing the type of column "${column}" in table "${table}" converts the values stored in it, and a conversion can lose information that no statement can bring back` };
2750
+ }
2751
+ recreateAssertion(name) {
2752
+ const assertion = this.assertions.get(name.toLowerCase());
2753
+ if (!assertion)
2754
+ return NOTHING_TO_UNDO;
2755
+ if (!assertion.check)
2756
+ return this.cannotUndo(`DROP ASSERTION ${name}`, 'the pre-apply catalog carries no CHECK body for it');
2757
+ const check = cloneExpr(assertion.check);
2758
+ this.replayRenames(check, undefined, {});
2759
+ const stmt = { type: 'createAssertion', name: { type: 'identifier', name: assertion.name }, check };
2760
+ return { undo: [createAssertionToString(applyAssertionSchemaDefault(stmt, this.schemaName))] };
2761
+ }
2762
+ recreateView(name) {
2763
+ const view = this.views.get(name.toLowerCase());
2764
+ if (!view)
2765
+ return NOTHING_TO_UNDO;
2766
+ if (!view.select)
2767
+ return this.cannotUndo(`DROP VIEW ${name}`, 'the pre-apply catalog carries no body for it');
2768
+ const select = cloneQueryExpr(view.select);
2769
+ this.replayRenames(select, undefined, {});
2770
+ const stmt = {
2771
+ type: 'createView',
2772
+ view: { type: 'identifier', name: view.name },
2773
+ ifNotExists: false,
2774
+ columns: view.columns ? [...view.columns] : undefined,
2775
+ select,
2776
+ tags: view.tags ? { ...view.tags } : undefined,
2777
+ };
2778
+ return { undo: [createViewToString(applyViewSchemaDefault(stmt, this.schemaName))] };
2779
+ }
2780
+ recreateIndex(name) {
2781
+ const index = this.indexes.get(name.toLowerCase());
2782
+ if (!index)
2783
+ return NOTHING_TO_UNDO;
2784
+ // The catalog carries no structured index shape, only `generateIndexDDL`'s
2785
+ // text — re-parsed here with the real parser rather than sliced.
2786
+ const parsed = parseStatement(index.ddl);
2787
+ if (!parsed.ok)
2788
+ return this.cannotUndo(`DROP INDEX ${name}`, `its recorded DDL does not parse (${parsed.reason})`);
2789
+ if (parsed.stmt.type !== 'createIndex')
2790
+ return this.cannotUndo(`DROP INDEX ${name}`, 'its recorded DDL is not a create index statement');
2791
+ const recreate = {
2792
+ ...parsed.stmt,
2793
+ ifNotExists: false,
2794
+ index: { type: 'identifier', name: index.name },
2795
+ table: { type: 'identifier', name: this.tableNameNow(index.tableName) },
2796
+ };
2797
+ if (recreate.where)
2798
+ this.replayRenames(recreate.where, undefined, { schemaAuthoredBody: true });
2799
+ return { undo: [createIndexToString(applyIndexDefaults(recreate, this.schemaName))] };
2800
+ }
2801
+ /** Undo of `DROP MAINTAINED`: the derivation the table carried before the apply. */
2802
+ reattachAsBefore(table) {
2803
+ const maintained = this.tableNow(table)?.maintained;
2804
+ if (!maintained)
2805
+ return NOTHING_TO_UNDO;
2806
+ return this.setMaintainedFrom(maintained, table);
2807
+ }
2808
+ /**
2809
+ * Undo of `SET MAINTAINED`. The table is plain at this step when it never was
2810
+ * maintained OR when this same plan detached it earlier (the reshape leg:
2811
+ * `drop maintained` → column ops → re-attach); either way the attach undoes to
2812
+ * a detach, and the earlier detach's own undo restores the original derivation.
2813
+ * A same-shape re-attach (no detach) replaced the derivation in place, so its
2814
+ * undo re-attaches the pre-apply one.
2815
+ */
2816
+ undoSetMaintained(alter) {
2817
+ const actual = this.tableNow(alter.tableName);
2818
+ if (!actual)
2819
+ return NOTHING_TO_UNDO;
2820
+ if (!actual.maintained || alter.dropMaintained) {
2821
+ return { undo: [`ALTER TABLE ${this.schemaPrefix}${quoteIdentifier(alter.tableName)} DROP MAINTAINED`] };
2822
+ }
2823
+ return this.setMaintainedFrom(actual.maintained, alter.tableName);
2824
+ }
2825
+ readdConstraint(table, name, quotedTable) {
2826
+ // `constraintsToDrop` carries pre-apply names (a renamed constraint is never
2827
+ // also dropped), so the lookup is direct rather than through the renames.
2828
+ const constraint = this.tableNow(table)?.namedConstraints?.find(c => sameName(c.name, name));
2829
+ if (!constraint)
2830
+ return NOTHING_TO_UNDO;
2831
+ if (!constraint.bodyAst)
2832
+ return this.cannotUndo(`DROP CONSTRAINT ${name}`, 'the pre-apply catalog carries no body for it');
2833
+ const body = this.forwardRenamedConstraint(constraint.bodyAst, table);
2834
+ const fragment = tableConstraintsToString([{ ...body, name: constraint.name, tags: constraint.tags ? { ...constraint.tags } : undefined }]);
2835
+ return { undo: [`ALTER TABLE ${quotedTable} ADD ${fragment}`] };
2836
+ }
2837
+ restorePrimaryKey(table, quotedTable) {
2838
+ const actual = this.tableNow(table);
2839
+ if (!actual)
2840
+ return NOTHING_TO_UNDO;
2841
+ if (actual.primaryKey.length === 0)
2842
+ return this.cannotUndo(`ALTER PRIMARY KEY on ${table}`, 'the pre-apply catalog records no primary key for it');
2843
+ const cols = actual.primaryKey
2844
+ .map(pk => quoteIdentifier(this.columnNameNow(table, pk.columnName)) + (pk.desc ? ' desc' : ''))
2845
+ .join(', ');
2846
+ return { undo: [`ALTER TABLE ${quotedTable} ALTER PRIMARY KEY (${cols})`] };
2847
+ }
2848
+ dropAddedColumn(quotedTable, colDef) {
2849
+ const parsed = parseStatement(`alter table "x" add column ${colDef}`);
2850
+ if (!parsed.ok)
2851
+ return this.cannotUndo(`ADD COLUMN ${colDef}`, `the column definition does not parse (${parsed.reason})`);
2852
+ const action = parsed.stmt.type === 'alterTable' ? parsed.stmt.action : undefined;
2853
+ if (action?.type !== 'addColumn')
2854
+ return this.cannotUndo(`ADD COLUMN ${colDef}`, 'the column definition does not parse as one');
2855
+ return { undo: [`ALTER TABLE ${quotedTable} DROP COLUMN ${quoteIdentifier(action.column.name)}`] };
2856
+ }
2857
+ dropAddedConstraint(quotedTable, fragment) {
2858
+ const parsed = parseStatement(`alter table "x" add ${fragment}`);
2859
+ if (!parsed.ok)
2860
+ return this.cannotUndo(`ADD ${fragment}`, `the constraint fragment does not parse (${parsed.reason})`);
2861
+ const action = parsed.stmt.type === 'alterTable' ? parsed.stmt.action : undefined;
2862
+ if (action?.type !== 'addConstraint')
2863
+ return this.cannotUndo(`ADD ${fragment}`, 'the fragment does not parse as a constraint');
2864
+ if (!action.constraint.name)
2865
+ return this.cannotUndo(`ADD ${fragment}`, 'the constraint has no name to drop it by');
2866
+ return { undo: [`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quoteIdentifier(action.constraint.name)}`] };
2867
+ }
2868
+ /** Undo of `DROP DEFAULT`: put the pre-apply default back, or nothing when there was none. */
2869
+ undoDropDefault(table, column, alterColumn) {
2870
+ const prior = this.priorDefaultText(table, column);
2871
+ return { undo: prior ? [`${alterColumn} SET DEFAULT ${prior}`] : [] };
2872
+ }
2873
+ /** Undo of `SET DEFAULT e`: the pre-apply default, or `DROP DEFAULT` when the column had none. */
2874
+ undoSetDefault(table, column, alterColumn) {
2875
+ const prior = this.priorDefaultText(table, column);
2876
+ if (prior === undefined)
2877
+ return NOTHING_TO_UNDO;
2878
+ return { undo: [prior === null ? `${alterColumn} DROP DEFAULT` : `${alterColumn} SET DEFAULT ${prior}`] };
2879
+ }
2880
+ undoSetCollation(table, column, alterColumn) {
2881
+ const actual = this.columnNow(table, column);
2882
+ if (!actual)
2883
+ return NOTHING_TO_UNDO;
2884
+ return { undo: [`${alterColumn} SET COLLATE ${actual.collation || 'BINARY'}`] };
2885
+ }
2886
+ restoreTableTags(table, alterTable) {
2887
+ const actual = this.tableNow(table);
2888
+ return actual ? restoreTags(alterTable, actual.tags) : NOTHING_TO_UNDO;
2889
+ }
2890
+ restoreColumnTags(table, column, alterColumn) {
2891
+ const actual = this.columnNow(table, column);
2892
+ return actual ? restoreTags(alterColumn, actual.tags) : NOTHING_TO_UNDO;
2893
+ }
2894
+ restoreConstraintTags(table, constraint, alterConstraint) {
2895
+ const actual = this.constraintNow(table, constraint);
2896
+ return actual ? restoreTags(alterConstraint, actual.tags) : NOTHING_TO_UNDO;
2897
+ }
2898
+ restoreViewTags(view, alterView) {
2899
+ const actual = this.views.get(view.toLowerCase());
2900
+ return actual ? restoreTags(alterView, actual.tags) : NOTHING_TO_UNDO;
2901
+ }
2902
+ restoreIndexTags(index, alterIndex) {
2903
+ const actual = this.indexes.get(index.toLowerCase());
2904
+ return actual ? restoreTags(alterIndex, actual.tags) : NOTHING_TO_UNDO;
2905
+ }
2906
+ // --- Catalog lookups through the renames in force (Rule 1) ---
2907
+ /**
2908
+ * The pre-apply table a CURRENT table name denotes. A name a rename in force
2909
+ * vacated (and nothing was renamed onto) denotes no pre-apply table: whatever
2910
+ * sits under it now, this plan created.
2911
+ */
2912
+ tableNow(currentName) {
2913
+ const actualName = preRenameName(this.tableRenames, currentName);
2914
+ return actualName === undefined ? undefined : this.tables.get(actualName.toLowerCase());
2915
+ }
2916
+ /** The current spelling of a pre-apply table name. */
2917
+ tableNameNow(actualName) {
2918
+ return this.tableRenames.find(r => sameName(r.oldName, actualName))?.newName ?? actualName;
2919
+ }
2920
+ /** The pre-apply column a CURRENT column name denotes (both names as spelled now); see `tableNow` for a vacated name. */
2921
+ columnNow(currentTable, currentColumn) {
2922
+ const table = this.tableNow(currentTable);
2923
+ if (!table)
2924
+ return undefined;
2925
+ const actualName = preRenameName(this.columnRenames.filter(r => sameName(r.table, currentTable)), currentColumn);
2926
+ return actualName === undefined ? undefined : table.columns.find(c => sameName(c.name, actualName));
2927
+ }
2928
+ /** The current spelling of a pre-apply column name. */
2929
+ columnNameNow(currentTable, actualColumn) {
2930
+ return this.columnRenames.find(r => sameName(r.table, currentTable) && sameName(r.oldName, actualColumn))?.newName ?? actualColumn;
2931
+ }
2932
+ constraintNow(currentTable, currentName) {
2933
+ const table = this.tableNow(currentTable);
2934
+ if (!table)
2935
+ return undefined;
2936
+ const actualName = preRenameName(this.constraintRenames.filter(r => sameName(r.table, currentTable)), currentName);
2937
+ return actualName === undefined ? undefined : table.namedConstraints?.find(c => sameName(c.name, actualName));
2938
+ }
2939
+ // --- Catalog-sourced bodies with the renames in force applied forward (Rule 2) ---
2940
+ setMaintainedFrom(maintained, table) {
2941
+ if (!maintained.select)
2942
+ return this.cannotUndo(`the derivation of ${table}`, 'the pre-apply catalog carries no body for it');
2943
+ const select = cloneQueryExpr(maintained.select);
2944
+ this.replayRenames(select, undefined, {});
2945
+ const stmt = {
2946
+ type: 'alterTable',
2947
+ table: this.schemaName !== 'main'
2948
+ ? { type: 'identifier', name: table, schema: this.schemaName }
2949
+ : { type: 'identifier', name: table },
2950
+ action: { type: 'setMaintained', columns: maintained.columns, select },
2951
+ };
2952
+ return { undo: [astToString(stmt)] };
2953
+ }
2954
+ /** The pre-apply DEFAULT of a column, rendered: `null` when it had none, `undefined` when the column is unknown. */
2955
+ priorDefaultText(table, column) {
2956
+ const actual = this.columnNow(table, column);
2957
+ if (!actual)
2958
+ return undefined;
2959
+ if (!actual.defaultValue)
2960
+ return null;
2961
+ const expr = cloneExpr(actual.defaultValue);
2962
+ this.replayRenames(expr, table, { rowImageContext: true, schemaAuthoredBody: true });
2963
+ return expressionToString(expr);
2964
+ }
2965
+ /**
2966
+ * A clone of a pre-apply constraint body with the renames in force applied
2967
+ * forward — the inverse of `reconciledDeclaredBody`, arm for arm: a CHECK's
2968
+ * expression through the walkers (owning-table column renames seeded, other
2969
+ * tables' by scope); a UNIQUE's column list; an FK's local list, its parent's
2970
+ * column list under the parent's CURRENT name, then the parent name itself.
2971
+ */
2972
+ forwardRenamedConstraint(tc, owner) {
2973
+ switch (tc.type) {
2974
+ case 'check': {
2975
+ const clone = { ...tc, expr: tc.expr ? cloneExpr(tc.expr) : undefined };
2976
+ if (clone.expr)
2977
+ this.replayRenames(clone.expr, owner, { rowImageContext: true, schemaAuthoredBody: true });
2978
+ return clone;
2979
+ }
2980
+ case 'unique': {
2981
+ const clone = { ...tc, columns: tc.columns?.map(c => ({ ...c })) };
2982
+ this.forwardRenameColumnList(clone.columns, owner);
2983
+ return clone;
2984
+ }
2985
+ case 'foreignKey': {
2986
+ const clone = {
2987
+ ...tc,
2988
+ columns: tc.columns?.map(c => ({ ...c })),
2989
+ foreignKey: tc.foreignKey
2990
+ ? { ...tc.foreignKey, columns: tc.foreignKey.columns ? [...tc.foreignKey.columns] : undefined }
2991
+ : tc.foreignKey,
2992
+ };
2993
+ this.forwardRenameColumnList(clone.columns, owner);
2994
+ const fk = clone.foreignKey;
2995
+ // Renames are within-schema, so a cross-schema parent is left alone.
2996
+ if (fk && (!fk.schema || sameName(fk.schema, this.schemaName))) {
2997
+ const parentNow = this.tableNameNow(fk.table);
2998
+ if (fk.columns) {
2999
+ for (let i = 0; i < fk.columns.length; i++)
3000
+ fk.columns[i] = this.columnNameNow(parentNow, fk.columns[i]);
3001
+ }
3002
+ fk.table = parentNow;
3003
+ }
3004
+ return clone;
3005
+ }
3006
+ default:
3007
+ return tc;
3008
+ }
3009
+ }
3010
+ forwardRenameColumnList(columns, table) {
3011
+ for (const col of columns ?? [])
3012
+ col.name = this.columnNameNow(table, col.name);
3013
+ }
3014
+ /**
3015
+ * Applies the renames in force to a body in place, in plan order: every table
3016
+ * rename first (they are the plan's first steps), then each column rename as its
3017
+ * step landed. Each call goes through the walker the live propagation uses for
3018
+ * that body kind: a body OWNED by a table (a CHECK, a column default — `owner`
3019
+ * set) takes the owning table's renames through the seeded CHECK walker in
3020
+ * `'own'` row-image mode and other tables' renames through the plain scope walk
3021
+ * in `'foreign'` mode; a free body (a view, assertion or derivation body, an
3022
+ * index predicate) takes every rename through the plain walk in `'none'` mode.
3023
+ * `tableOpts` is the body kind's `renameTableInAst` options, exactly as the
3024
+ * matching `renameTableIn*` helper passes them.
3025
+ */
3026
+ replayRenames(node, owner, tableOpts) {
3027
+ for (const r of this.tableRenames) {
3028
+ renameTableInAst(node, {
3029
+ oldName: r.oldName, newName: r.newName, schemaName: this.schemaName,
3030
+ resolve: this.resolveRef, resolveAfter: this.resolveRef,
3031
+ }, tableOpts);
3032
+ }
3033
+ this.columnRenames.forEach((r, replayedSoFar) => {
3034
+ const targetKey = objectRefKey(this.schemaName, r.table);
3035
+ const resolveColumn = this.columnResolverAfter(replayedSoFar);
3036
+ if (owner !== undefined && sameName(r.table, owner)) {
3037
+ renameColumnInCheckExpression(node, owner, r.oldName, r.newName, this.resolveRef, targetKey, 'own', resolveColumn);
3038
+ }
3039
+ else {
3040
+ renameColumnInAst(node, r.table, r.oldName, r.newName, this.resolveRef, targetKey, owner !== undefined ? 'foreign' : 'none', resolveColumn);
3041
+ }
3042
+ });
3043
+ }
3044
+ /**
3045
+ * Column-existence resolver for the scope-aware walks in {@link replayRenames}:
3046
+ * "does this FROM source expose this column?" answered against the pre-apply
3047
+ * catalog with the first `applied` column renames in force — the world the
3048
+ * matching live rename statement ran in. Cross-schema sources and views answer
3049
+ * false, as the differ's declared-side resolver does (conservative: worst case a
3050
+ * ref that would have bound to an inner source is rewritten too).
3051
+ */
3052
+ columnResolverAfter(applied) {
3053
+ const inForce = this.columnRenames.slice(0, applied);
3054
+ return (schema, table, column) => {
3055
+ if (schema !== this.schemaName.toLowerCase())
3056
+ return false;
3057
+ const actual = this.tableNow(table);
3058
+ if (!actual)
3059
+ return false;
3060
+ const onTable = inForce.filter(r => sameName(r.table, table));
3061
+ return actual.columns.some(c => sameName(onTable.find(r => sameName(r.oldName, c.name))?.newName ?? c.name, column));
3062
+ };
3063
+ }
3064
+ cannotUndo(what, why) {
3065
+ warnLog(`No undo for migration step ${what}: ${why}`);
3066
+ return { irreversible: `${what} cannot be undone: ${why}` };
3067
+ }
3068
+ }
3069
+ /** `<prefix> SET TAGS (<pre-apply tags>)`; an absent or empty set restores "no tags" as the explicit clear form. */
3070
+ function restoreTags(prefix, tags) {
3071
+ return { undo: [`${prefix} SET TAGS ${tagsBodyToString(tags ? { ...tags } : undefined)}`] };
3072
+ }
3073
+ /** The name a create statement gives the object it makes, when it is a create of `kind`. */
3074
+ function createdObjectName(ast, kind) {
3075
+ if (!ast)
3076
+ return undefined;
3077
+ switch (ast.type) {
3078
+ case 'createTable': return kind === 'table' ? ast.table.name : undefined;
3079
+ case 'createMaterializedView': return kind === 'table' ? ast.view.name : undefined;
3080
+ case 'createView': return kind === 'view' ? ast.view.name : undefined;
3081
+ case 'createIndex': return kind === 'index' ? ast.index.name : undefined;
3082
+ case 'createAssertion': return kind === 'assertion' ? ast.name.name : undefined;
3083
+ default: return undefined;
3084
+ }
3085
+ }
3086
+ /** A parse that reports failure as data: the undo renderer must stay total over hand-built input. */
3087
+ function parseStatement(sql) {
3088
+ try {
3089
+ return { ok: true, stmt: new Parser().parse(sql) };
3090
+ }
3091
+ catch (e) {
3092
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
3093
+ }
3094
+ }
2602
3095
  //# sourceMappingURL=schema-differ.js.map