@quereus/quereus 4.19.4 → 4.20.1

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 (120) 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 +16 -4
  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 +4 -4
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +5 -4
  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 +54 -5
  80. package/dist/src/schema/catalog-rendering.js.map +1 -1
  81. package/dist/src/schema/catalog.d.ts +58 -2
  82. package/dist/src/schema/catalog.d.ts.map +1 -1
  83. package/dist/src/schema/catalog.js +45 -34
  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 +42 -2
  102. package/dist/src/schema/schema-differ.d.ts.map +1 -1
  103. package/dist/src/schema/schema-differ.js +722 -67
  104. package/dist/src/schema/schema-differ.js.map +1 -1
  105. package/dist/src/schema/unique-enforcement.d.ts +23 -1
  106. package/dist/src/schema/unique-enforcement.d.ts.map +1 -1
  107. package/dist/src/schema/unique-enforcement.js +43 -1
  108. package/dist/src/schema/unique-enforcement.js.map +1 -1
  109. package/dist/src/types/temporal-ops.d.ts.map +1 -1
  110. package/dist/src/types/temporal-ops.js +16 -10
  111. package/dist/src/types/temporal-ops.js.map +1 -1
  112. package/dist/src/vtab/capabilities.d.ts +5 -2
  113. package/dist/src/vtab/capabilities.d.ts.map +1 -1
  114. package/dist/src/vtab/memory/layer/manager.d.ts +30 -23
  115. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  116. package/dist/src/vtab/memory/layer/manager.js +102 -111
  117. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  118. package/dist/src/vtab/module.d.ts +16 -0
  119. package/dist/src/vtab/module.d.ts.map +1 -1
  120. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import { implicitIndexNameForColumns } from './catalog.js';
1
2
  import { createTableToString, createViewToString, createMaterializedViewToString, createIndexToString, createAssertionToString, columnDefToString, quoteIdentifier, expressionToString, tagsBodyToString, tableConstraintsToString, constraintBodyToCanonicalString, createIndexBodyToCanonicalString, indexedColumnBareName, viewDefinitionToCanonicalString, astToString } from '../emit/ast-stringify.js';
2
3
  import { computeBodyHash, normalizeBackingModuleName, canonicalBackingModuleArgs } from './view.js';
3
4
  import { QuereusError } from '../common/errors.js';
@@ -6,10 +7,11 @@ import { createLogger } from '../common/logger.js';
6
7
  import { validateReservedTags } from './reserved-tags.js';
7
8
  import { raiseReservedTagDiagnostics } from './reserved-tags-policy.js';
8
9
  import { renameColumnInAst, renameColumnInCheckExpression, renameTableInAst, tableReferencedInAst, objectRefKey, singleSchemaObjectRefResolver } from './rename-rewriter.js';
10
+ import { Parser } from '../parser/parser.js';
9
11
  import { cloneExpr, cloneQueryExpr } from '../planner/mutation/scope-transform.js';
10
12
  import { normalizeCollationName } from '../util/comparison.js';
11
13
  import { inferType } from '../types/registry.js';
12
- import { resolveDefaultCollation } from './table.js';
14
+ import { resolveDefaultCollation, disambiguateAutoConstraintName } from './table.js';
13
15
  const log = createLogger('schema:differ');
14
16
  const warnLog = log.extend('warn');
15
17
  /** Reserved tag namespace prefix used for differ-recognized hints. */
@@ -1398,11 +1400,12 @@ columnRenamesByTable, schemaName) {
1398
1400
  return { ...stmt, columns, where };
1399
1401
  }
1400
1402
  /**
1401
- * Converts a column-level constraint carrying a name into the equivalent
1402
- * table-level {@link AST.TableConstraint}, so it can be stringified into an
1403
- * `ADD CONSTRAINT` fragment and diffed by name alongside table-level constraints.
1404
- * Returns undefined for constraint kinds that are not lifecycle-managed named
1405
- * constraints (NOT NULL / NULL / DEFAULT / COLLATE / GENERATED / PRIMARY KEY).
1403
+ * Converts a column-level constraint into the equivalent table-level
1404
+ * {@link AST.TableConstraint}, so it can be stringified into an `ADD CONSTRAINT`
1405
+ * fragment and diffed (by name, or by body when unnamed) alongside table-level
1406
+ * constraints. Returns undefined for constraint kinds that are not
1407
+ * lifecycle-managed table constraints (NOT NULL / NULL / DEFAULT / COLLATE /
1408
+ * GENERATED / PRIMARY KEY).
1406
1409
  */
1407
1410
  function columnConstraintToTableConstraint(columnName, cc) {
1408
1411
  switch (cc.type) {
@@ -1421,13 +1424,18 @@ function columnConstraintToTableConstraint(columnName, cc) {
1421
1424
  }
1422
1425
  }
1423
1426
  /**
1424
- * Gathers declared *user-named* CHECK / UNIQUE / FOREIGN KEY constraints from a
1425
- * declared table — both table-level and column-level (carrying an explicit name)
1426
- * — keyed by lowercased name. PRIMARY KEY is excluded (handled by
1427
- * `primaryKeyChange`); engine-synthesized `_`-prefixed names are excluded to stay
1428
- * symmetric with the catalog's `namedConstraints`. On a name collision the first
1427
+ * Gathers a declared table's CHECK / UNIQUE / FOREIGN KEY constraints — both
1428
+ * table-level and column-level — split the way the catalog splits them:
1429
+ * *user-named* ones keyed by lowercased name, and the rest (no name, or a
1430
+ * reserved `_`-prefixed one) as a list in declaration order. PRIMARY KEY is
1431
+ * excluded (handled by `primaryKeyChange`). On a user-name collision the first
1429
1432
  * wins (a duplicate user constraint name is a separate validation concern).
1430
1433
  *
1434
+ * A column-level clause on a column `isAddedColumn` reports is skipped: `ADD
1435
+ * COLUMN` renders the column's clauses inline (`columnDefToString`), so collecting
1436
+ * it here would ADD the constraint a second time. A TABLE-level constraint over a
1437
+ * new column is not carried by `ADD COLUMN` and is collected as usual.
1438
+ *
1431
1439
  * `schemaName` is the schema this table is being diffed under (the differ runs
1432
1440
  * per schema — it is the CHILD schema for any FK declared here). It is threaded
1433
1441
  * into the canonical-body render so an FK's explicit own-schema qualifier folds
@@ -1435,33 +1443,34 @@ function columnConstraintToTableConstraint(columnName, cc) {
1435
1443
  * {@link constraintBodyToCanonicalString}); a genuine cross-schema parent stays a
1436
1444
  * body-change channel.
1437
1445
  */
1438
- function collectDeclaredNamedConstraints(declaredTable, schemaName) {
1439
- const out = new Map();
1440
- const add = (name, tags, tc) => {
1441
- if (!name)
1442
- return;
1443
- const lower = name.toLowerCase();
1444
- if (lower.startsWith('_'))
1446
+ function collectDeclaredConstraints(declaredTable, schemaName, isAddedColumn) {
1447
+ const named = new Map();
1448
+ const unnamed = [];
1449
+ const add = (tc, column) => {
1450
+ const definition = constraintBodyToCanonicalString(tc, schemaName);
1451
+ if (!tc.name || tc.name.startsWith('_')) {
1452
+ unnamed.push({ name: tc.name, column, definition, bodyAst: tc });
1445
1453
  return;
1446
- if (out.has(lower))
1454
+ }
1455
+ const lower = tc.name.toLowerCase();
1456
+ if (named.has(lower))
1447
1457
  return;
1448
- out.set(lower, { name, tags, ddl: tableConstraintsToString([tc]), definition: constraintBodyToCanonicalString(tc, schemaName), bodyAst: tc });
1458
+ named.set(lower, { name: tc.name, tags: tc.tags, ddl: tableConstraintsToString([tc]), definition, bodyAst: tc });
1449
1459
  };
1450
1460
  for (const c of declaredTable.tableStmt.constraints ?? []) {
1451
- if (c.type === 'primaryKey')
1452
- continue;
1453
- add(c.name, c.tags, c);
1461
+ if (c.type !== 'primaryKey')
1462
+ add(c);
1454
1463
  }
1455
1464
  for (const col of declaredTable.tableStmt.columns) {
1465
+ if (isAddedColumn(col.name.toLowerCase()))
1466
+ continue;
1456
1467
  for (const cc of col.constraints ?? []) {
1457
- if (!cc.name)
1458
- continue;
1459
1468
  const tc = columnConstraintToTableConstraint(col.name, cc);
1460
1469
  if (tc)
1461
- add(cc.name, cc.tags, tc);
1470
+ add(tc, col.name);
1462
1471
  }
1463
1472
  }
1464
- return out;
1473
+ return { named, unnamed };
1465
1474
  }
1466
1475
  /**
1467
1476
  * Inverse-applies the in-diff column renames to a constraint's column list: maps
@@ -1654,6 +1663,141 @@ resolveDeclaredColumn) {
1654
1663
  return d.definition;
1655
1664
  }
1656
1665
  }
1666
+ /**
1667
+ * Constraint lifecycle for CHECK / UNIQUE / FOREIGN KEY constraints with no
1668
+ * user-addressable name (see `CatalogTable.unnamedConstraints`). With no name to
1669
+ * key on, identity is the canonical body: declared and actual are matched as a
1670
+ * multiset — each declared constraint consumes one actual with the same
1671
+ * (rename-reconciled) body, so two identical unnamed CHECKs are two entries.
1672
+ *
1673
+ * - declared, unmatched → ADD under a minted reserved name (see
1674
+ * {@link mintUnnamedConstraintName}), so the result is unnamed-class on the
1675
+ * next diff and droppable by name;
1676
+ * - actual, unmatched → DROP by its stored auto-name. One stored with no name
1677
+ * (a CREATE-time table-level CHECK, any UNIQUE added unnamed) has no statement
1678
+ * that removes it: skipped when a dropped column takes it along (DROP COLUMN
1679
+ * prunes a UNIQUE / FK over the column), otherwise refused here — the
1680
+ * alternative is the silent no-op that leaves an upgraded database enforcing a
1681
+ * rule a fresh one does not.
1682
+ *
1683
+ * NOTE: tags on an unnamed constraint are not diffed — with no name there is
1684
+ * nothing for `ALTER CONSTRAINT … SET TAGS` to address. A tag-only edit to an
1685
+ * unnamed constraint is a no-op; name the constraint to manage its tags.
1686
+ */
1687
+ function diffUnnamedConstraints(declared, actualTable, ctx) {
1688
+ const unmatchedActual = [...(actualTable.unnamedConstraints ?? [])];
1689
+ const unmatchedDeclared = [];
1690
+ for (const d of declared) {
1691
+ const body = ctx.reconcile(d);
1692
+ // Among same-body actuals, keep the one with no stored name: of a pair, it is
1693
+ // the one no DROP can reach, so leaving the other unmatched stays droppable.
1694
+ let at = unmatchedActual.findIndex(a => a.definition === body && a.name === undefined);
1695
+ if (at < 0)
1696
+ at = unmatchedActual.findIndex(a => a.definition === body);
1697
+ if (at >= 0)
1698
+ unmatchedActual.splice(at, 1);
1699
+ else
1700
+ unmatchedDeclared.push(d);
1701
+ }
1702
+ const drops = [];
1703
+ const undroppable = [];
1704
+ for (const a of unmatchedActual) {
1705
+ if (a.name !== undefined)
1706
+ drops.push(a.name);
1707
+ else if (!prunedByColumnDrop(a, ctx.droppedColumns))
1708
+ undroppable.push(a);
1709
+ }
1710
+ if (undroppable.length > 0 && ctx.refuseUndroppable) {
1711
+ throw new QuereusError(`Cannot drop unnamed constraint ${undroppable.map(a => `'${a.definition}'`).join(', ')} from table '${ctx.schemaName}.${actualTable.name}': `
1712
+ + `it is no longer declared, but it was created without a name, so no statement can drop it. `
1713
+ + `Keep declaring it, or rebuild the table; name constraints (constraint <name> …) to keep them droppable.`, StatusCode.ERROR);
1714
+ }
1715
+ const taken = constraintNamesAtAddTime(actualTable, ctx, drops);
1716
+ const checkOrdinal = countActualChecks(actualTable);
1717
+ const adds = unmatchedDeclared.map(d => tableConstraintsToString([{ ...d.bodyAst, name: mintUnnamedConstraintName(d, ctx.tableName, checkOrdinal, taken) }]));
1718
+ return { adds, drops };
1719
+ }
1720
+ /** True when DROP COLUMN removes `a` on its own: it prunes a UNIQUE / FK over a dropped column (a CHECK instead blocks the drop). */
1721
+ function prunedByColumnDrop(a, droppedColumns) {
1722
+ return a.kind !== 'check' && (a.bodyAst?.columns ?? []).some(c => droppedColumns.has(c.name.toLowerCase()));
1723
+ }
1724
+ /** CHECKs on the actual table — where an unnamed table-level CHECK's `_check_<n>` ordinal starts. */
1725
+ function countActualChecks(actualTable) {
1726
+ return (actualTable.namedConstraints ?? []).filter(c => c.bodyAst?.type === 'check').length
1727
+ + (actualTable.unnamedConstraints ?? []).filter(c => c.kind === 'check').length;
1728
+ }
1729
+ /**
1730
+ * The lowercased constraint names a minted name must avoid at the point the
1731
+ * `ADD CONSTRAINT` steps run (after every DROP / RENAME CONSTRAINT and ADD COLUMN
1732
+ * of the table — see the phase order in `generateMigrationPlan`): the live names
1733
+ * minus the dropped ones, plus every declared user name (each exists by then),
1734
+ * plus — over-approximated — what `ADD COLUMN` mints for an inline clause on a new
1735
+ * column. An unnamed UNIQUE's backing-structure name (`_uc_<cols>`) is included:
1736
+ * a UNIQUE added under the same name would collide with that structure.
1737
+ *
1738
+ * NOTE: that backing name is derived from the PRE-rename column names, but a RENAME
1739
+ * COLUMN in the same diff moves it to `_uc_<new cols>` before the ADDs run. A mint can
1740
+ * only hit the moved name through a `_`-joined spelling clash (`unique (a_b)` beside a
1741
+ * renamed `unique (a, b)`), and the engine refuses that ADD loudly; if it ever bites,
1742
+ * derive the name over the post-rename columns.
1743
+ */
1744
+ function constraintNamesAtAddTime(actualTable, ctx, unnamedDrops) {
1745
+ const taken = new Set();
1746
+ for (const c of actualTable.namedConstraints ?? [])
1747
+ taken.add(c.name.toLowerCase());
1748
+ for (const c of actualTable.unnamedConstraints ?? []) {
1749
+ if (c.name !== undefined)
1750
+ taken.add(c.name.toLowerCase());
1751
+ else if (c.kind === 'unique')
1752
+ taken.add(uniqueBackingName(c.bodyAst?.columns ?? []).toLowerCase());
1753
+ }
1754
+ for (const name of [...ctx.namedDrops, ...unnamedDrops])
1755
+ taken.delete(name.toLowerCase());
1756
+ for (const name of ctx.declaredNames)
1757
+ taken.add(name);
1758
+ for (const col of ctx.addedColumns) {
1759
+ taken.add(`_check_${col}`.toLowerCase());
1760
+ taken.add(`_fk_${ctx.tableName}_${col}`.toLowerCase());
1761
+ taken.add(uniqueBackingName([{ name: col }]).toLowerCase());
1762
+ }
1763
+ return taken;
1764
+ }
1765
+ /** The structure name an unnamed UNIQUE over `columns` is backed by. */
1766
+ function uniqueBackingName(columns) {
1767
+ return implicitIndexNameForColumns(undefined, columns.map(c => c.name));
1768
+ }
1769
+ /**
1770
+ * The reserved `_`-prefixed name an unnamed constraint is ADDed under — reserved so
1771
+ * the result stays in the unnamed class (body-matched, stable on re-diff), named so
1772
+ * it can be dropped and its ADD undone. Each spells what the engine itself names
1773
+ * the constraint where it names one:
1774
+ * - the `_`-name the declaration wrote, if any;
1775
+ * - column-level CHECK → `_check_<column>` (CREATE TABLE's mint);
1776
+ * - table-level CHECK → `_check_<n>`, `n` from the table's CHECK count, bumped
1777
+ * until free (the `_check_<index>` label an unnamed CHECK's violation reports;
1778
+ * each mint registers in `taken`, so the next one bumps past it).
1779
+ * NOT left unnamed: `ALTER … ADD` would mint a user-class `check_<n>`, which
1780
+ * the next diff would drop and re-add forever;
1781
+ * - FOREIGN KEY → `_fk_<table>_<cols>` (both engine paths' mint);
1782
+ * - UNIQUE → `_uc_<cols>` (the backing-structure name both paths use).
1783
+ * Then disambiguated (`_<N>` suffix) against — and registered into — `taken`.
1784
+ */
1785
+ function mintUnnamedConstraintName(d, tableName, checkOrdinal, taken) {
1786
+ const tc = d.bodyAst;
1787
+ if (d.name === undefined && tc.type === 'check' && d.column === undefined) {
1788
+ let n = checkOrdinal;
1789
+ while (taken.has(`_check_${n}`))
1790
+ n++;
1791
+ taken.add(`_check_${n}`);
1792
+ return `_check_${n}`;
1793
+ }
1794
+ const columns = tc.columns ?? [];
1795
+ const base = d.name
1796
+ ?? (tc.type === 'check' ? `_check_${d.column}`
1797
+ : tc.type === 'foreignKey' ? `_fk_${tableName}_${columns.map(c => c.name).join('_')}`
1798
+ : uniqueBackingName(columns));
1799
+ return disambiguateAutoConstraintName(base, taken);
1800
+ }
1657
1801
  /**
1658
1802
  * Resolves a declared table's column renames against its matched actual catalog
1659
1803
  * table — the map-building + {@link resolveRenames} step shared by both the
@@ -1784,10 +1928,11 @@ defaultCollation) {
1784
1928
  // `constraints` list AND column-level constraints carrying an explicit name
1785
1929
  // (e.g. `qty int constraint chk_qty check (qty > 0)`) — the actual catalog's
1786
1930
  // `namedConstraints` already merges both. PRIMARY KEY constraints are excluded
1787
- // (PK changes flow through `primaryKeyChange`); auto-prefixed (`_`) names are
1788
- // excluded to stay symmetric with the catalog (see catalog.ts) so an unnamed
1789
- // declared constraint never churns add/drop against its synthesized actual name.
1790
- const declaredNamedConstraints = collectDeclaredNamedConstraints(declaredTable, schemaName);
1931
+ // (PK changes flow through `primaryKeyChange`). Unnamed and auto-prefixed (`_`)
1932
+ // ones are matched by body instead, after the named lifecycle below (see
1933
+ // `diffUnnamedConstraints`), mirroring the catalog's named / unnamed split.
1934
+ const declaredConstraints = collectDeclaredConstraints(declaredTable, schemaName, lower => !colRenames.pairs.has(lower));
1935
+ const declaredNamedConstraints = declaredConstraints.named;
1791
1936
  const actualNamedConstraints = new Map();
1792
1937
  for (const c of actualTable.namedConstraints ?? []) {
1793
1938
  actualNamedConstraints.set(c.name.toLowerCase(), c);
@@ -1862,6 +2007,21 @@ defaultCollation) {
1862
2007
  pureDropCount++;
1863
2008
  }
1864
2009
  }
2010
+ // Unnamed constraints are not rename candidates, so they stay out of the
2011
+ // require-hint counts.
2012
+ const unnamed = diffUnnamedConstraints(declaredConstraints.unnamed, actualTable, {
2013
+ tableName: declaredTable.tableStmt.table.name,
2014
+ schemaName,
2015
+ reconcile: d => reconciledDeclaredBody(d, diff.columnsToRename, tableRenames, actualTable.name, schemaName, columnRenamesByTable, resolveDeclaredColumn),
2016
+ declaredNames: [...declaredNamedConstraints.keys()],
2017
+ namedDrops: constraintsToDrop,
2018
+ addedColumns: declaredTable.tableStmt.columns.filter(c => !colRenames.pairs.has(c.name.toLowerCase())).map(c => c.name),
2019
+ droppedColumns: new Set(diff.columnsToDrop.map(c => c.toLowerCase())),
2020
+ // A backing-module move recreates the table, so its alter diff is discarded.
2021
+ refuseUndroppable: !diff.maintainedModuleMigration,
2022
+ });
2023
+ constraintsToDrop.push(...unnamed.drops);
2024
+ constraintsToAdd.push(...unnamed.adds);
1865
2025
  if (constraintsToAdd.length > 0)
1866
2026
  diff.constraintsToAdd = constraintsToAdd;
1867
2027
  if (constraintsToDrop.length > 0)
@@ -2374,8 +2534,8 @@ function orderDropsByFKDependency(dropSet, actualTables) {
2374
2534
  */
2375
2535
  function comparisonDomainAlters(quotedTable, quotedCol, change) {
2376
2536
  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}`] : [];
2537
+ const retype = change.dataType !== undefined ? [{ sql: `${prefix} SET DATA TYPE ${change.dataType}`, attribute: 'dataType' }] : [];
2538
+ const recollate = change.collation !== undefined ? [{ sql: `${prefix} SET COLLATE ${change.collation}`, attribute: 'collation' }] : [];
2379
2539
  return normalizeCollationName(change.collation ?? 'BINARY') === 'BINARY'
2380
2540
  ? [...recollate, ...retype]
2381
2541
  : [...retype, ...recollate];
@@ -2399,12 +2559,31 @@ export function generateMigrationDDL(diff, schemaName) {
2399
2559
  * them without re-lexing the text this function just produced. The remaining steps
2400
2560
  * are short template-built strings (renames, drops, column/constraint alters,
2401
2561
  * `SET TAGS`) and stay text-only.
2562
+ *
2563
+ * With `actual` — the pre-apply catalog `computeSchemaDiff` was handed — every step
2564
+ * also carries its reversal (`undo` / `irreversible`, see {@link MigrationStep}),
2565
+ * rendered by {@link UndoRenderer} at the step's own push site so a forward
2566
+ * statement and its undo read together. Without it nothing about the undo is
2567
+ * computed: `generateMigrationDDL` never passes a catalog, so `diff schema` output
2568
+ * and cost are untouched.
2402
2569
  */
2403
- export function generateMigrationPlan(diff, schemaName) {
2570
+ export function generateMigrationPlan(diff, schemaName, actual) {
2404
2571
  const statements = [];
2405
2572
  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 }); };
2573
+ const undo = actual ? new UndoRenderer(schemaName ?? 'main', schemaPrefix, actual) : undefined;
2574
+ /**
2575
+ * Appends a template-built step — text-only, so the apply path parses it as
2576
+ * before. `undoOf` runs only when undo was requested, and renders the reversal
2577
+ * for the catalog state THIS step runs in (Rule 1 of {@link MigrationStep.undo}).
2578
+ */
2579
+ const pushText = (sql, undoOf) => {
2580
+ statements.push(undo ? { sql, ...undoOf(undo) } : { sql });
2581
+ };
2582
+ /** Appends a create bucket; each create undoes to the `DROP … IF EXISTS` of what it made. */
2583
+ const pushCreates = (bucket, kind) => {
2584
+ for (const create of bucket)
2585
+ statements.push(undo ? { ...create, ...undo.dropCreated(create, kind) } : create);
2586
+ };
2408
2587
  // Renames first — they free old names for subsequent creates and re-target
2409
2588
  // dependents (handled inside ALTER TABLE ... RENAME by the rename rewriter).
2410
2589
  // Only tables have a rename primitive. The other RenameOp kinds are metadata
@@ -2415,13 +2594,16 @@ export function generateMigrationPlan(diff, schemaName) {
2415
2594
  // the table-alter channel (RENAME CONSTRAINT below).
2416
2595
  for (const r of diff.renames) {
2417
2596
  if (r.kind === 'table') {
2418
- pushText(`ALTER TABLE ${schemaPrefix}${quoteIdentifier(r.oldName)} RENAME TO ${quoteIdentifier(r.newName)}`);
2597
+ 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
2598
  }
2420
2599
  // Non-table rename ops emit no DDL here — see the note above.
2421
2600
  }
2601
+ // Every later step runs — and is undone — with the table renames in force, so
2602
+ // from here on the undo renderer reads the pre-apply catalog through them.
2603
+ undo?.tableRenamesLanded(diff.renames);
2422
2604
  // Drop assertions first (they may reference tables)
2423
2605
  for (const name of diff.assertionsToDrop) {
2424
- pushText(`DROP ASSERTION IF EXISTS ${schemaPrefix}${quoteIdentifier(name)}`);
2606
+ pushText(`DROP ASSERTION IF EXISTS ${schemaPrefix}${quoteIdentifier(name)}`, u => u.recreateAssertion(name));
2425
2607
  }
2426
2608
  // Detach maintained tables (`drop maintained`) EARLY — where MV drops ran
2427
2609
  // before — so a detach precedes any column op on that table AND any drop of its
@@ -2430,25 +2612,25 @@ export function generateMigrationPlan(diff, schemaName) {
2430
2612
  // flip's cross-table detach-v2 / attach-v1 pair falls out of this ordering.
2431
2613
  for (const alter of diff.tablesToAlter) {
2432
2614
  if (alter.dropMaintained) {
2433
- pushText(`ALTER TABLE ${schemaPrefix}${quoteIdentifier(alter.tableName)} DROP MAINTAINED`);
2615
+ pushText(`ALTER TABLE ${schemaPrefix}${quoteIdentifier(alter.tableName)} DROP MAINTAINED`, u => u.reattachAsBefore(alter.tableName));
2434
2616
  }
2435
2617
  }
2436
2618
  // Drop items (reverse order)
2437
2619
  for (const tableName of diff.tablesToDrop) {
2438
- pushText(`DROP TABLE IF EXISTS ${schemaPrefix}${quoteIdentifier(tableName)}`);
2620
+ pushText(`DROP TABLE IF EXISTS ${schemaPrefix}${quoteIdentifier(tableName)}`, u => u.droppedTable(tableName));
2439
2621
  }
2440
2622
  for (const viewName of diff.viewsToDrop) {
2441
- pushText(`DROP VIEW IF EXISTS ${schemaPrefix}${quoteIdentifier(viewName)}`);
2623
+ pushText(`DROP VIEW IF EXISTS ${schemaPrefix}${quoteIdentifier(viewName)}`, u => u.recreateView(viewName));
2442
2624
  }
2443
2625
  for (const indexName of diff.indexesToDrop) {
2444
- pushText(`DROP INDEX IF EXISTS ${schemaPrefix}${quoteIdentifier(indexName)}`);
2626
+ pushText(`DROP INDEX IF EXISTS ${schemaPrefix}${quoteIdentifier(indexName)}`, u => u.recreateIndex(indexName));
2445
2627
  }
2446
2628
  // Create new items. A fresh maintained table rides `tablesToCreate` (rendered
2447
2629
  // as the `create materialized view` sugar) and re-materializes as part of its
2448
2630
  // create.
2449
- statements.push(...diff.tablesToCreate);
2450
- statements.push(...diff.viewsToCreate);
2451
- statements.push(...diff.indexesToCreate);
2631
+ pushCreates(diff.tablesToCreate, 'table');
2632
+ pushCreates(diff.viewsToCreate, 'view');
2633
+ pushCreates(diff.indexesToCreate, 'index');
2452
2634
  // Assertion creates do NOT go here — they run last, after the table alters and
2453
2635
  // the maintained re-attaches. See the push at the end of this function.
2454
2636
  // Alter existing tables.
@@ -2468,47 +2650,56 @@ export function generateMigrationPlan(diff, schemaName) {
2468
2650
  for (const alter of diff.tablesToAlter) {
2469
2651
  const quotedTable = `${schemaPrefix}${quoteIdentifier(alter.tableName)}`;
2470
2652
  for (const r of alter.columnsToRename) {
2471
- pushText(`ALTER TABLE ${quotedTable} RENAME COLUMN ${quoteIdentifier(r.oldName)} TO ${quoteIdentifier(r.newName)}`);
2653
+ 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)}`] }));
2654
+ undo?.columnRenameLanded(alter.tableName, r);
2472
2655
  }
2473
2656
  for (const colDef of alter.columnsToAdd) {
2474
- pushText(`ALTER TABLE ${quotedTable} ADD COLUMN ${colDef}`);
2657
+ pushText(`ALTER TABLE ${quotedTable} ADD COLUMN ${colDef}`, u => u.dropAddedColumn(quotedTable, colDef));
2475
2658
  }
2476
2659
  for (const colAlter of alter.columnsToAlter) {
2477
2660
  const quotedCol = quoteIdentifier(colAlter.columnName);
2661
+ const alterColumn = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol}`;
2478
2662
  // A default the retype would choke on and this migration replaces anyway is
2479
2663
  // cleared BEFORE the comparison-domain phase (see `dropStaleDefaultFirst`).
2480
2664
  if (colAlter.dropStaleDefaultFirst) {
2481
- pushText(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} DROP DEFAULT`);
2665
+ pushText(`${alterColumn} DROP DEFAULT`, u => u.undoDropDefault(alter.tableName, colAlter.columnName, alterColumn));
2482
2666
  }
2483
2667
  // SET DATA TYPE / SET COLLATE lead the per-column phase (both are
2484
2668
  // comparison-domain changes), before DEFAULT / NOT NULL.
2485
- for (const s of comparisonDomainAlters(quotedTable, quotedCol, colAlter))
2486
- pushText(s);
2669
+ for (const s of comparisonDomainAlters(quotedTable, quotedCol, colAlter)) {
2670
+ pushText(s.sql, u => s.attribute === 'dataType'
2671
+ ? u.retypedColumn(alter.tableName, colAlter.columnName)
2672
+ : u.undoSetCollation(alter.tableName, colAlter.columnName, alterColumn));
2673
+ }
2487
2674
  if (colAlter.defaultValue !== undefined) {
2488
2675
  if (colAlter.defaultValue === null) {
2489
2676
  // Already dropped above when it preceded the retype.
2490
2677
  if (!colAlter.dropStaleDefaultFirst) {
2491
- pushText(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} DROP DEFAULT`);
2678
+ pushText(`${alterColumn} DROP DEFAULT`, u => u.undoDropDefault(alter.tableName, colAlter.columnName, alterColumn));
2492
2679
  }
2493
2680
  }
2494
2681
  else {
2495
- pushText(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET DEFAULT ${expressionToString(colAlter.defaultValue)}`);
2682
+ // After `dropStaleDefaultFirst` the column reaches this step with no
2683
+ // default — the stale one was cleared earlier in this phase — so the
2684
+ // reversal is a plain DROP DEFAULT, not the pre-apply default (Rule 1).
2685
+ pushText(`${alterColumn} SET DEFAULT ${expressionToString(colAlter.defaultValue)}`, u => colAlter.dropStaleDefaultFirst
2686
+ ? { undo: [`${alterColumn} DROP DEFAULT`] }
2687
+ : u.undoSetDefault(alter.tableName, colAlter.columnName, alterColumn));
2496
2688
  }
2497
2689
  }
2498
2690
  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`);
2691
+ pushText(colAlter.notNull ? `${alterColumn} SET NOT NULL` : `${alterColumn} DROP NOT NULL`, () => ({ undo: [colAlter.notNull ? `${alterColumn} DROP NOT NULL` : `${alterColumn} SET NOT NULL`] }));
2502
2692
  }
2503
2693
  }
2504
2694
  // Constraint lifecycle: RENAME (free a name) then DROP (remove a stale /
2505
2695
  // conflicting constraint), both BEFORE re-adds and before the PK change so a
2506
2696
  // dropped UNIQUE can't strand a PK dependency.
2507
2697
  for (const r of alter.constraintsToRename ?? []) {
2508
- pushText(`ALTER TABLE ${quotedTable} RENAME CONSTRAINT ${quoteIdentifier(r.oldName)} TO ${quoteIdentifier(r.newName)}`);
2698
+ 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)}`] }));
2699
+ undo?.constraintRenameLanded(alter.tableName, r);
2509
2700
  }
2510
2701
  for (const name of alter.constraintsToDrop ?? []) {
2511
- pushText(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quoteIdentifier(name)}`);
2702
+ pushText(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quoteIdentifier(name)}`, u => u.readdConstraint(alter.tableName, name, quotedTable));
2512
2703
  }
2513
2704
  if (alter.primaryKeyChange) {
2514
2705
  const pkCols = alter.primaryKeyChange.newPkColumns
@@ -2519,16 +2710,16 @@ export function generateMigrationPlan(diff, schemaName) {
2519
2710
  return s;
2520
2711
  })
2521
2712
  .join(', ');
2522
- pushText(`ALTER TABLE ${quotedTable} ALTER PRIMARY KEY (${pkCols})`);
2713
+ pushText(`ALTER TABLE ${quotedTable} ALTER PRIMARY KEY (${pkCols})`, u => u.restorePrimaryKey(alter.tableName, quotedTable));
2523
2714
  }
2524
2715
  // ADD CONSTRAINT after the PK change (a new UNIQUE / FK may align with the new
2525
2716
  // key) and after the column adds it may reference. CHECK adds apply in-place;
2526
2717
  // UNIQUE / FK adds depend on module ADD CONSTRAINT support (see constraintsToAdd).
2527
2718
  for (const frag of alter.constraintsToAdd ?? []) {
2528
- pushText(`ALTER TABLE ${quotedTable} ADD ${frag}`);
2719
+ pushText(`ALTER TABLE ${quotedTable} ADD ${frag}`, u => u.dropAddedConstraint(quotedTable, frag));
2529
2720
  }
2530
2721
  for (const colName of alter.columnsToDrop) {
2531
- pushText(`ALTER TABLE ${quotedTable} DROP COLUMN ${quoteIdentifier(colName)}`);
2722
+ pushText(`ALTER TABLE ${quotedTable} DROP COLUMN ${quoteIdentifier(colName)}`, u => u.droppedColumn(alter.tableName, colName));
2532
2723
  }
2533
2724
  // Tags phase — last, so a SET TAGS lands on the post-structural column /
2534
2725
  // constraint set (a tag set emitted alongside a RENAME COLUMN targets the
@@ -2539,15 +2730,17 @@ export function generateMigrationPlan(diff, schemaName) {
2539
2730
  // TABLE handler rejects a tag action on a maintained table). See
2540
2731
  // `TableAlterDiff.maintainedTags`.
2541
2732
  const tagVerb = alter.maintainedTags ? 'ALTER MATERIALIZED VIEW' : 'ALTER TABLE';
2542
- pushText(`${tagVerb} ${quotedTable} SET TAGS ${tagsBodyToString(alter.tableTagsChange)}`);
2733
+ pushText(`${tagVerb} ${quotedTable} SET TAGS ${tagsBodyToString(alter.tableTagsChange)}`, u => u.restoreTableTags(alter.tableName, `${tagVerb} ${quotedTable}`));
2543
2734
  }
2544
2735
  for (const colAlter of alter.columnsToAlter) {
2545
2736
  if (colAlter.tags === undefined)
2546
2737
  continue;
2547
- pushText(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quoteIdentifier(colAlter.columnName)} SET TAGS ${tagsBodyToString(colAlter.tags)}`);
2738
+ const alterColumn = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quoteIdentifier(colAlter.columnName)}`;
2739
+ pushText(`${alterColumn} SET TAGS ${tagsBodyToString(colAlter.tags)}`, u => u.restoreColumnTags(alter.tableName, colAlter.columnName, alterColumn));
2548
2740
  }
2549
2741
  for (const ctc of alter.constraintTagsChanges ?? []) {
2550
- pushText(`ALTER TABLE ${quotedTable} ALTER CONSTRAINT ${quoteIdentifier(ctc.constraintName)} SET TAGS ${tagsBodyToString(ctc.tags)}`);
2742
+ const alterConstraint = `ALTER TABLE ${quotedTable} ALTER CONSTRAINT ${quoteIdentifier(ctc.constraintName)}`;
2743
+ pushText(`${alterConstraint} SET TAGS ${tagsBodyToString(ctc.tags)}`, u => u.restoreConstraintTags(alter.tableName, ctc.constraintName, alterConstraint));
2551
2744
  }
2552
2745
  }
2553
2746
  // Attach / re-attach maintained tables (`set maintained as`) LATE — where MV
@@ -2575,7 +2768,8 @@ export function generateMigrationPlan(diff, schemaName) {
2575
2768
  select: alter.setMaintained.select,
2576
2769
  },
2577
2770
  };
2578
- statements.push({ sql: astToString(stmt), ast: stmt });
2771
+ const step = { sql: astToString(stmt), ast: stmt };
2772
+ statements.push(undo ? { ...step, ...undo.undoSetMaintained(alter) } : step);
2579
2773
  }
2580
2774
  // Assertion creates LAST — after every table alter and maintained re-attach, so
2581
2775
  // a body sees the final shape of every object. `CREATE ASSERTION` plans its
@@ -2584,7 +2778,7 @@ export function generateMigrationPlan(diff, schemaName) {
2584
2778
  // create ran in the create block, before `ADD COLUMN`. Nothing in a migration
2585
2779
  // depends on an assertion existing, so last is strictly safer. Assertion DROPs
2586
2780
  // stay first (see the top of this function).
2587
- statements.push(...diff.assertionsToCreate);
2781
+ pushCreates(diff.assertionsToCreate, 'assertion');
2588
2782
  // In-place tag changes on views / indexes. These are leaf metadata writes (no
2589
2783
  // dependency ordering vs the table-alter block). The `?? []` keeps
2590
2784
  // generateMigrationDDL robust against hand-built diffs (some tests construct
@@ -2592,11 +2786,472 @@ export function generateMigrationPlan(diff, schemaName) {
2592
2786
  // table's tag-only change rides the table-alter block above (`SET TAGS`), not a
2593
2787
  // separate bucket.
2594
2788
  for (const vtc of diff.viewTagsChanges ?? []) {
2595
- pushText(`ALTER VIEW ${schemaPrefix}${quoteIdentifier(vtc.name)} SET TAGS ${tagsBodyToString(vtc.tags)}`);
2789
+ const alterView = `ALTER VIEW ${schemaPrefix}${quoteIdentifier(vtc.name)}`;
2790
+ pushText(`${alterView} SET TAGS ${tagsBodyToString(vtc.tags)}`, u => u.restoreViewTags(vtc.name, alterView));
2596
2791
  }
2597
2792
  for (const itc of diff.indexTagsChanges ?? []) {
2598
- pushText(`ALTER INDEX ${schemaPrefix}${quoteIdentifier(itc.name)} SET TAGS ${tagsBodyToString(itc.tags)}`);
2793
+ const alterIndex = `ALTER INDEX ${schemaPrefix}${quoteIdentifier(itc.name)}`;
2794
+ pushText(`${alterIndex} SET TAGS ${tagsBodyToString(itc.tags)}`, u => u.restoreIndexTags(itc.name, alterIndex));
2599
2795
  }
2600
2796
  return statements;
2601
2797
  }
2798
+ /** A step that changed nothing, so nothing has to be put back. */
2799
+ const NOTHING_TO_UNDO = { undo: [] };
2800
+ /** Case-insensitive identifier equality — how the catalog and the rename walkers compare names. */
2801
+ const sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
2802
+ /**
2803
+ * The pre-apply spelling of a name as spelled NOW, given the renames in force:
2804
+ * the old name when a rename landed on it, the name itself when none touched it,
2805
+ * and `undefined` when a rename vacated it — nothing pre-apply answers to it any more.
2806
+ */
2807
+ function preRenameName(renames, currentName) {
2808
+ const landed = renames.find(r => sameName(r.newName, currentName));
2809
+ if (landed)
2810
+ return landed.oldName;
2811
+ return renames.some(r => sameName(r.oldName, currentName)) ? undefined : currentName;
2812
+ }
2813
+ /**
2814
+ * Renders each migration step's undo from the pre-apply catalog. The two rules
2815
+ * {@link MigrationStep.undo} states are what every arm below honours:
2816
+ *
2817
+ * **Rule 1 — spell the target the way the forward step did.** The unwind runs in
2818
+ * reverse, so when a step's undo runs every later step has already been taken back
2819
+ * and the catalog is in the state the step itself left behind. The plan is pushed in
2820
+ * execution order and this renderer is told about each rename as its step lands
2821
+ * (`tableRenamesLanded`, `columnRenameLanded`, `constraintRenameLanded`), so every
2822
+ * catalog lookup goes through exactly the renames in force at that step — a later
2823
+ * `ALTER TABLE t …` step's undo names the post-rename `t`, and finds the pre-apply
2824
+ * table under its old name.
2825
+ *
2826
+ * **Rule 2 — forward-apply the renames in force to anything taken from the catalog.**
2827
+ * A restored constraint / view / assertion / derivation body, a column default and
2828
+ * the old primary key are spelled with pre-rename names in the catalog; by Rule 1
2829
+ * those renames are still in force when the undo runs, so {@link replayRenames}
2830
+ * applies them forward — through the same `renameTableInAst` /
2831
+ * `renameColumnInAst` / `renameColumnInCheckExpression` walkers the live rename
2832
+ * propagation uses — before the body is emitted. This is the differ's
2833
+ * rename-reconciled comparison (`reconciledDeclaredBody`) pointed the other way.
2834
+ * Only renames whose steps PRECEDE this one count: a view dropped before the
2835
+ * column-rename phase is restored with the old column names, because by the time
2836
+ * its undo runs the column rename has already been reversed.
2837
+ *
2838
+ * Two policies keep the plan generator total over hand-built inputs:
2839
+ * - a target the pre-apply catalog does not mention undoes to nothing — the
2840
+ * forward step either is a no-op (`IF EXISTS`) or fails before changing anything;
2841
+ * - a catalog entry that lacks what its restore needs (no body AST, DDL that does
2842
+ * not parse) is reported as `irreversible` with the reason, never thrown.
2843
+ *
2844
+ * NOTE: the undo is rendered eagerly for every step at plan time, so once the apply
2845
+ * path passes its catalog every apply pays it; the only non-trivial per-step cost is
2846
+ * one small parse on the fragment arms (dropped index, added column / constraint).
2847
+ * If plan generation ever shows up in profiles, render lazily on the failure path.
2848
+ */
2849
+ class UndoRenderer {
2850
+ schemaName;
2851
+ schemaPrefix;
2852
+ tables = new Map();
2853
+ views = new Map();
2854
+ indexes = new Map();
2855
+ assertions = new Map();
2856
+ resolveRef;
2857
+ tableRenames = [];
2858
+ columnRenames = [];
2859
+ constraintRenames = [];
2860
+ constructor(schemaName, schemaPrefix, actual) {
2861
+ this.schemaName = schemaName;
2862
+ this.schemaPrefix = schemaPrefix;
2863
+ for (const t of actual.tables ?? [])
2864
+ this.tables.set(t.name.toLowerCase(), t);
2865
+ for (const v of actual.views ?? [])
2866
+ this.views.set(v.name.toLowerCase(), v);
2867
+ // NOTE: an index name is unique per table, not per schema, and the plan's
2868
+ // `DROP INDEX` names it bare — so the first entry wins here, as it does there.
2869
+ // If `indexesToDrop` ever carries the owning table, key this map by both.
2870
+ for (const i of actual.indexes ?? [])
2871
+ if (!this.indexes.has(i.name.toLowerCase()))
2872
+ this.indexes.set(i.name.toLowerCase(), i);
2873
+ for (const a of actual.assertions ?? [])
2874
+ this.assertions.set(a.name.toLowerCase(), a);
2875
+ this.resolveRef = singleSchemaObjectRefResolver(schemaName);
2876
+ }
2877
+ // --- Rename bookkeeping (Rule 1) ---
2878
+ tableRenamesLanded(renames) {
2879
+ this.tableRenames = renames.filter(r => r.kind === 'table');
2880
+ }
2881
+ columnRenameLanded(table, r) {
2882
+ this.columnRenames.push({ table, oldName: r.oldName, newName: r.newName });
2883
+ }
2884
+ constraintRenameLanded(table, r) {
2885
+ this.constraintRenames.push({ table, oldName: r.oldName, newName: r.newName });
2886
+ }
2887
+ // --- Arms ---
2888
+ dropCreated(create, kind) {
2889
+ const name = createdObjectName(create.ast, kind);
2890
+ if (name === undefined)
2891
+ return this.cannotUndo(create.sql, `its statement is not a create ${kind}`);
2892
+ const verb = { table: 'DROP TABLE', view: 'DROP VIEW', index: 'DROP INDEX', assertion: 'DROP ASSERTION' }[kind];
2893
+ return { undo: [`${verb} IF EXISTS ${this.schemaPrefix}${quoteIdentifier(name)}`] };
2894
+ }
2895
+ droppedTable(actualName) {
2896
+ if (!this.tables.has(actualName.toLowerCase()))
2897
+ return NOTHING_TO_UNDO;
2898
+ return { irreversible: `dropping table "${actualName}" also discards its rows, and no statement can bring them back` };
2899
+ }
2900
+ droppedColumn(table, column) {
2901
+ if (!this.columnNow(table, column))
2902
+ return NOTHING_TO_UNDO;
2903
+ return { irreversible: `dropping column "${column}" from table "${table}" discards the values stored in it, and no statement can bring them back` };
2904
+ }
2905
+ retypedColumn(table, column) {
2906
+ if (!this.columnNow(table, column))
2907
+ return NOTHING_TO_UNDO;
2908
+ 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` };
2909
+ }
2910
+ recreateAssertion(name) {
2911
+ const assertion = this.assertions.get(name.toLowerCase());
2912
+ if (!assertion)
2913
+ return NOTHING_TO_UNDO;
2914
+ if (!assertion.check)
2915
+ return this.cannotUndo(`DROP ASSERTION ${name}`, 'the pre-apply catalog carries no CHECK body for it');
2916
+ const check = cloneExpr(assertion.check);
2917
+ this.replayRenames(check, undefined, {});
2918
+ const stmt = { type: 'createAssertion', name: { type: 'identifier', name: assertion.name }, check };
2919
+ return { undo: [createAssertionToString(applyAssertionSchemaDefault(stmt, this.schemaName))] };
2920
+ }
2921
+ recreateView(name) {
2922
+ const view = this.views.get(name.toLowerCase());
2923
+ if (!view)
2924
+ return NOTHING_TO_UNDO;
2925
+ if (!view.select)
2926
+ return this.cannotUndo(`DROP VIEW ${name}`, 'the pre-apply catalog carries no body for it');
2927
+ const select = cloneQueryExpr(view.select);
2928
+ this.replayRenames(select, undefined, {});
2929
+ const stmt = {
2930
+ type: 'createView',
2931
+ view: { type: 'identifier', name: view.name },
2932
+ ifNotExists: false,
2933
+ columns: view.columns ? [...view.columns] : undefined,
2934
+ select,
2935
+ tags: view.tags ? { ...view.tags } : undefined,
2936
+ };
2937
+ return { undo: [createViewToString(applyViewSchemaDefault(stmt, this.schemaName))] };
2938
+ }
2939
+ recreateIndex(name) {
2940
+ const index = this.indexes.get(name.toLowerCase());
2941
+ if (!index)
2942
+ return NOTHING_TO_UNDO;
2943
+ // The catalog carries no structured index shape, only `generateIndexDDL`'s
2944
+ // text — re-parsed here with the real parser rather than sliced.
2945
+ const parsed = parseStatement(index.ddl);
2946
+ if (!parsed.ok)
2947
+ return this.cannotUndo(`DROP INDEX ${name}`, `its recorded DDL does not parse (${parsed.reason})`);
2948
+ if (parsed.stmt.type !== 'createIndex')
2949
+ return this.cannotUndo(`DROP INDEX ${name}`, 'its recorded DDL is not a create index statement');
2950
+ const recreate = {
2951
+ ...parsed.stmt,
2952
+ ifNotExists: false,
2953
+ index: { type: 'identifier', name: index.name },
2954
+ table: { type: 'identifier', name: this.tableNameNow(index.tableName) },
2955
+ };
2956
+ if (recreate.where)
2957
+ this.replayRenames(recreate.where, undefined, { schemaAuthoredBody: true });
2958
+ return { undo: [createIndexToString(applyIndexDefaults(recreate, this.schemaName))] };
2959
+ }
2960
+ /** Undo of `DROP MAINTAINED`: the derivation the table carried before the apply. */
2961
+ reattachAsBefore(table) {
2962
+ const maintained = this.tableNow(table)?.maintained;
2963
+ if (!maintained)
2964
+ return NOTHING_TO_UNDO;
2965
+ return this.setMaintainedFrom(maintained, table);
2966
+ }
2967
+ /**
2968
+ * Undo of `SET MAINTAINED`. The table is plain at this step when it never was
2969
+ * maintained OR when this same plan detached it earlier (the reshape leg:
2970
+ * `drop maintained` → column ops → re-attach); either way the attach undoes to
2971
+ * a detach, and the earlier detach's own undo restores the original derivation.
2972
+ * A same-shape re-attach (no detach) replaced the derivation in place, so its
2973
+ * undo re-attaches the pre-apply one.
2974
+ */
2975
+ undoSetMaintained(alter) {
2976
+ const actual = this.tableNow(alter.tableName);
2977
+ if (!actual)
2978
+ return NOTHING_TO_UNDO;
2979
+ if (!actual.maintained || alter.dropMaintained) {
2980
+ return { undo: [`ALTER TABLE ${this.schemaPrefix}${quoteIdentifier(alter.tableName)} DROP MAINTAINED`] };
2981
+ }
2982
+ return this.setMaintainedFrom(actual.maintained, alter.tableName);
2983
+ }
2984
+ readdConstraint(table, name, quotedTable) {
2985
+ // `constraintsToDrop` carries pre-apply names (a renamed constraint is never
2986
+ // also dropped), so the lookup is direct rather than through the renames. An
2987
+ // unnamed constraint is dropped by its stored auto-name, and re-added under it.
2988
+ const actual = this.tableNow(table);
2989
+ const constraint = actual?.namedConstraints?.find(c => sameName(c.name, name))
2990
+ ?? actual?.unnamedConstraints?.find(c => c.name !== undefined && sameName(c.name, name));
2991
+ if (!constraint)
2992
+ return NOTHING_TO_UNDO;
2993
+ if (!constraint.bodyAst)
2994
+ return this.cannotUndo(`DROP CONSTRAINT ${name}`, 'the pre-apply catalog carries no body for it');
2995
+ const body = this.forwardRenamedConstraint(constraint.bodyAst, table);
2996
+ const fragment = tableConstraintsToString([{ ...body, name: constraint.name, tags: constraint.tags ? { ...constraint.tags } : undefined }]);
2997
+ return { undo: [`ALTER TABLE ${quotedTable} ADD ${fragment}`] };
2998
+ }
2999
+ restorePrimaryKey(table, quotedTable) {
3000
+ const actual = this.tableNow(table);
3001
+ if (!actual)
3002
+ return NOTHING_TO_UNDO;
3003
+ if (actual.primaryKey.length === 0)
3004
+ return this.cannotUndo(`ALTER PRIMARY KEY on ${table}`, 'the pre-apply catalog records no primary key for it');
3005
+ const cols = actual.primaryKey
3006
+ .map(pk => quoteIdentifier(this.columnNameNow(table, pk.columnName)) + (pk.desc ? ' desc' : ''))
3007
+ .join(', ');
3008
+ return { undo: [`ALTER TABLE ${quotedTable} ALTER PRIMARY KEY (${cols})`] };
3009
+ }
3010
+ dropAddedColumn(quotedTable, colDef) {
3011
+ const parsed = parseStatement(`alter table "x" add column ${colDef}`);
3012
+ if (!parsed.ok)
3013
+ return this.cannotUndo(`ADD COLUMN ${colDef}`, `the column definition does not parse (${parsed.reason})`);
3014
+ const action = parsed.stmt.type === 'alterTable' ? parsed.stmt.action : undefined;
3015
+ if (action?.type !== 'addColumn')
3016
+ return this.cannotUndo(`ADD COLUMN ${colDef}`, 'the column definition does not parse as one');
3017
+ return { undo: [`ALTER TABLE ${quotedTable} DROP COLUMN ${quoteIdentifier(action.column.name)}`] };
3018
+ }
3019
+ dropAddedConstraint(quotedTable, fragment) {
3020
+ const parsed = parseStatement(`alter table "x" add ${fragment}`);
3021
+ if (!parsed.ok)
3022
+ return this.cannotUndo(`ADD ${fragment}`, `the constraint fragment does not parse (${parsed.reason})`);
3023
+ const action = parsed.stmt.type === 'alterTable' ? parsed.stmt.action : undefined;
3024
+ if (action?.type !== 'addConstraint')
3025
+ return this.cannotUndo(`ADD ${fragment}`, 'the fragment does not parse as a constraint');
3026
+ if (!action.constraint.name)
3027
+ return this.cannotUndo(`ADD ${fragment}`, 'the constraint has no name to drop it by');
3028
+ return { undo: [`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quoteIdentifier(action.constraint.name)}`] };
3029
+ }
3030
+ /** Undo of `DROP DEFAULT`: put the pre-apply default back, or nothing when there was none. */
3031
+ undoDropDefault(table, column, alterColumn) {
3032
+ const prior = this.priorDefaultText(table, column);
3033
+ return { undo: prior ? [`${alterColumn} SET DEFAULT ${prior}`] : [] };
3034
+ }
3035
+ /** Undo of `SET DEFAULT e`: the pre-apply default, or `DROP DEFAULT` when the column had none. */
3036
+ undoSetDefault(table, column, alterColumn) {
3037
+ const prior = this.priorDefaultText(table, column);
3038
+ if (prior === undefined)
3039
+ return NOTHING_TO_UNDO;
3040
+ return { undo: [prior === null ? `${alterColumn} DROP DEFAULT` : `${alterColumn} SET DEFAULT ${prior}`] };
3041
+ }
3042
+ undoSetCollation(table, column, alterColumn) {
3043
+ const actual = this.columnNow(table, column);
3044
+ if (!actual)
3045
+ return NOTHING_TO_UNDO;
3046
+ return { undo: [`${alterColumn} SET COLLATE ${actual.collation || 'BINARY'}`] };
3047
+ }
3048
+ restoreTableTags(table, alterTable) {
3049
+ const actual = this.tableNow(table);
3050
+ return actual ? restoreTags(alterTable, actual.tags) : NOTHING_TO_UNDO;
3051
+ }
3052
+ restoreColumnTags(table, column, alterColumn) {
3053
+ const actual = this.columnNow(table, column);
3054
+ return actual ? restoreTags(alterColumn, actual.tags) : NOTHING_TO_UNDO;
3055
+ }
3056
+ restoreConstraintTags(table, constraint, alterConstraint) {
3057
+ const actual = this.constraintNow(table, constraint);
3058
+ return actual ? restoreTags(alterConstraint, actual.tags) : NOTHING_TO_UNDO;
3059
+ }
3060
+ restoreViewTags(view, alterView) {
3061
+ const actual = this.views.get(view.toLowerCase());
3062
+ return actual ? restoreTags(alterView, actual.tags) : NOTHING_TO_UNDO;
3063
+ }
3064
+ restoreIndexTags(index, alterIndex) {
3065
+ const actual = this.indexes.get(index.toLowerCase());
3066
+ return actual ? restoreTags(alterIndex, actual.tags) : NOTHING_TO_UNDO;
3067
+ }
3068
+ // --- Catalog lookups through the renames in force (Rule 1) ---
3069
+ /**
3070
+ * The pre-apply table a CURRENT table name denotes. A name a rename in force
3071
+ * vacated (and nothing was renamed onto) denotes no pre-apply table: whatever
3072
+ * sits under it now, this plan created.
3073
+ */
3074
+ tableNow(currentName) {
3075
+ const actualName = preRenameName(this.tableRenames, currentName);
3076
+ return actualName === undefined ? undefined : this.tables.get(actualName.toLowerCase());
3077
+ }
3078
+ /** The current spelling of a pre-apply table name. */
3079
+ tableNameNow(actualName) {
3080
+ return this.tableRenames.find(r => sameName(r.oldName, actualName))?.newName ?? actualName;
3081
+ }
3082
+ /** The pre-apply column a CURRENT column name denotes (both names as spelled now); see `tableNow` for a vacated name. */
3083
+ columnNow(currentTable, currentColumn) {
3084
+ const table = this.tableNow(currentTable);
3085
+ if (!table)
3086
+ return undefined;
3087
+ const actualName = preRenameName(this.columnRenames.filter(r => sameName(r.table, currentTable)), currentColumn);
3088
+ return actualName === undefined ? undefined : table.columns.find(c => sameName(c.name, actualName));
3089
+ }
3090
+ /** The current spelling of a pre-apply column name. */
3091
+ columnNameNow(currentTable, actualColumn) {
3092
+ return this.columnRenames.find(r => sameName(r.table, currentTable) && sameName(r.oldName, actualColumn))?.newName ?? actualColumn;
3093
+ }
3094
+ constraintNow(currentTable, currentName) {
3095
+ const table = this.tableNow(currentTable);
3096
+ if (!table)
3097
+ return undefined;
3098
+ const actualName = preRenameName(this.constraintRenames.filter(r => sameName(r.table, currentTable)), currentName);
3099
+ return actualName === undefined ? undefined : table.namedConstraints?.find(c => sameName(c.name, actualName));
3100
+ }
3101
+ // --- Catalog-sourced bodies with the renames in force applied forward (Rule 2) ---
3102
+ setMaintainedFrom(maintained, table) {
3103
+ if (!maintained.select)
3104
+ return this.cannotUndo(`the derivation of ${table}`, 'the pre-apply catalog carries no body for it');
3105
+ const select = cloneQueryExpr(maintained.select);
3106
+ this.replayRenames(select, undefined, {});
3107
+ const stmt = {
3108
+ type: 'alterTable',
3109
+ table: this.schemaName !== 'main'
3110
+ ? { type: 'identifier', name: table, schema: this.schemaName }
3111
+ : { type: 'identifier', name: table },
3112
+ action: { type: 'setMaintained', columns: maintained.columns, select },
3113
+ };
3114
+ return { undo: [astToString(stmt)] };
3115
+ }
3116
+ /** The pre-apply DEFAULT of a column, rendered: `null` when it had none, `undefined` when the column is unknown. */
3117
+ priorDefaultText(table, column) {
3118
+ const actual = this.columnNow(table, column);
3119
+ if (!actual)
3120
+ return undefined;
3121
+ if (!actual.defaultValue)
3122
+ return null;
3123
+ const expr = cloneExpr(actual.defaultValue);
3124
+ this.replayRenames(expr, table, { rowImageContext: true, schemaAuthoredBody: true });
3125
+ return expressionToString(expr);
3126
+ }
3127
+ /**
3128
+ * A clone of a pre-apply constraint body with the renames in force applied
3129
+ * forward — the inverse of `reconciledDeclaredBody`, arm for arm: a CHECK's
3130
+ * expression through the walkers (owning-table column renames seeded, other
3131
+ * tables' by scope); a UNIQUE's column list; an FK's local list, its parent's
3132
+ * column list under the parent's CURRENT name, then the parent name itself.
3133
+ */
3134
+ forwardRenamedConstraint(tc, owner) {
3135
+ switch (tc.type) {
3136
+ case 'check': {
3137
+ const clone = { ...tc, expr: tc.expr ? cloneExpr(tc.expr) : undefined };
3138
+ if (clone.expr)
3139
+ this.replayRenames(clone.expr, owner, { rowImageContext: true, schemaAuthoredBody: true });
3140
+ return clone;
3141
+ }
3142
+ case 'unique': {
3143
+ const clone = { ...tc, columns: tc.columns?.map(c => ({ ...c })) };
3144
+ this.forwardRenameColumnList(clone.columns, owner);
3145
+ return clone;
3146
+ }
3147
+ case 'foreignKey': {
3148
+ const clone = {
3149
+ ...tc,
3150
+ columns: tc.columns?.map(c => ({ ...c })),
3151
+ foreignKey: tc.foreignKey
3152
+ ? { ...tc.foreignKey, columns: tc.foreignKey.columns ? [...tc.foreignKey.columns] : undefined }
3153
+ : tc.foreignKey,
3154
+ };
3155
+ this.forwardRenameColumnList(clone.columns, owner);
3156
+ const fk = clone.foreignKey;
3157
+ // Renames are within-schema, so a cross-schema parent is left alone.
3158
+ if (fk && (!fk.schema || sameName(fk.schema, this.schemaName))) {
3159
+ const parentNow = this.tableNameNow(fk.table);
3160
+ if (fk.columns) {
3161
+ for (let i = 0; i < fk.columns.length; i++)
3162
+ fk.columns[i] = this.columnNameNow(parentNow, fk.columns[i]);
3163
+ }
3164
+ fk.table = parentNow;
3165
+ }
3166
+ return clone;
3167
+ }
3168
+ default:
3169
+ return tc;
3170
+ }
3171
+ }
3172
+ forwardRenameColumnList(columns, table) {
3173
+ for (const col of columns ?? [])
3174
+ col.name = this.columnNameNow(table, col.name);
3175
+ }
3176
+ /**
3177
+ * Applies the renames in force to a body in place, in plan order: every table
3178
+ * rename first (they are the plan's first steps), then each column rename as its
3179
+ * step landed. Each call goes through the walker the live propagation uses for
3180
+ * that body kind: a body OWNED by a table (a CHECK, a column default — `owner`
3181
+ * set) takes the owning table's renames through the seeded CHECK walker in
3182
+ * `'own'` row-image mode and other tables' renames through the plain scope walk
3183
+ * in `'foreign'` mode; a free body (a view, assertion or derivation body, an
3184
+ * index predicate) takes every rename through the plain walk in `'none'` mode.
3185
+ * `tableOpts` is the body kind's `renameTableInAst` options, exactly as the
3186
+ * matching `renameTableIn*` helper passes them.
3187
+ */
3188
+ replayRenames(node, owner, tableOpts) {
3189
+ for (const r of this.tableRenames) {
3190
+ renameTableInAst(node, {
3191
+ oldName: r.oldName, newName: r.newName, schemaName: this.schemaName,
3192
+ resolve: this.resolveRef, resolveAfter: this.resolveRef,
3193
+ }, tableOpts);
3194
+ }
3195
+ this.columnRenames.forEach((r, replayedSoFar) => {
3196
+ const targetKey = objectRefKey(this.schemaName, r.table);
3197
+ const resolveColumn = this.columnResolverAfter(replayedSoFar);
3198
+ if (owner !== undefined && sameName(r.table, owner)) {
3199
+ renameColumnInCheckExpression(node, owner, r.oldName, r.newName, this.resolveRef, targetKey, 'own', resolveColumn);
3200
+ }
3201
+ else {
3202
+ renameColumnInAst(node, r.table, r.oldName, r.newName, this.resolveRef, targetKey, owner !== undefined ? 'foreign' : 'none', resolveColumn);
3203
+ }
3204
+ });
3205
+ }
3206
+ /**
3207
+ * Column-existence resolver for the scope-aware walks in {@link replayRenames}:
3208
+ * "does this FROM source expose this column?" answered against the pre-apply
3209
+ * catalog with the first `applied` column renames in force — the world the
3210
+ * matching live rename statement ran in. Cross-schema sources and views answer
3211
+ * false, as the differ's declared-side resolver does (conservative: worst case a
3212
+ * ref that would have bound to an inner source is rewritten too).
3213
+ */
3214
+ columnResolverAfter(applied) {
3215
+ const inForce = this.columnRenames.slice(0, applied);
3216
+ return (schema, table, column) => {
3217
+ if (schema !== this.schemaName.toLowerCase())
3218
+ return false;
3219
+ const actual = this.tableNow(table);
3220
+ if (!actual)
3221
+ return false;
3222
+ const onTable = inForce.filter(r => sameName(r.table, table));
3223
+ return actual.columns.some(c => sameName(onTable.find(r => sameName(r.oldName, c.name))?.newName ?? c.name, column));
3224
+ };
3225
+ }
3226
+ cannotUndo(what, why) {
3227
+ warnLog(`No undo for migration step ${what}: ${why}`);
3228
+ return { irreversible: `${what} cannot be undone: ${why}` };
3229
+ }
3230
+ }
3231
+ /** `<prefix> SET TAGS (<pre-apply tags>)`; an absent or empty set restores "no tags" as the explicit clear form. */
3232
+ function restoreTags(prefix, tags) {
3233
+ return { undo: [`${prefix} SET TAGS ${tagsBodyToString(tags ? { ...tags } : undefined)}`] };
3234
+ }
3235
+ /** The name a create statement gives the object it makes, when it is a create of `kind`. */
3236
+ function createdObjectName(ast, kind) {
3237
+ if (!ast)
3238
+ return undefined;
3239
+ switch (ast.type) {
3240
+ case 'createTable': return kind === 'table' ? ast.table.name : undefined;
3241
+ case 'createMaterializedView': return kind === 'table' ? ast.view.name : undefined;
3242
+ case 'createView': return kind === 'view' ? ast.view.name : undefined;
3243
+ case 'createIndex': return kind === 'index' ? ast.index.name : undefined;
3244
+ case 'createAssertion': return kind === 'assertion' ? ast.name.name : undefined;
3245
+ default: return undefined;
3246
+ }
3247
+ }
3248
+ /** A parse that reports failure as data: the undo renderer must stay total over hand-built input. */
3249
+ function parseStatement(sql) {
3250
+ try {
3251
+ return { ok: true, stmt: new Parser().parse(sql) };
3252
+ }
3253
+ catch (e) {
3254
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
3255
+ }
3256
+ }
2602
3257
  //# sourceMappingURL=schema-differ.js.map