@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.
- package/README.md +257 -257
- package/dist/src/common/errors.d.ts +10 -0
- package/dist/src/common/errors.d.ts.map +1 -1
- package/dist/src/common/errors.js +14 -0
- package/dist/src/common/errors.js.map +1 -1
- package/dist/src/common/types.d.ts +16 -4
- package/dist/src/common/types.d.ts.map +1 -1
- package/dist/src/common/types.js.map +1 -1
- package/dist/src/core/database-events.d.ts.map +1 -1
- package/dist/src/core/database-events.js +12 -9
- package/dist/src/core/database-events.js.map +1 -1
- package/dist/src/core/database-transaction.d.ts.map +1 -1
- package/dist/src/core/database-transaction.js +28 -19
- package/dist/src/core/database-transaction.js.map +1 -1
- package/dist/src/core/database.d.ts +58 -6
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +185 -54
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/core/param.d.ts +46 -1
- package/dist/src/core/param.d.ts.map +1 -1
- package/dist/src/core/param.js +87 -7
- package/dist/src/core/param.js.map +1 -1
- package/dist/src/core/statement.d.ts +66 -1
- package/dist/src/core/statement.d.ts.map +1 -1
- package/dist/src/core/statement.js +156 -91
- package/dist/src/core/statement.js.map +1 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/parser/lexer.d.ts.map +1 -1
- package/dist/src/parser/lexer.js +9 -3
- package/dist/src/parser/lexer.js.map +1 -1
- package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
- package/dist/src/planner/analysis/change-scope.js +7 -20
- package/dist/src/planner/analysis/change-scope.js.map +1 -1
- package/dist/src/planner/analysis/check-extraction.d.ts +62 -0
- package/dist/src/planner/analysis/check-extraction.d.ts.map +1 -1
- package/dist/src/planner/analysis/check-extraction.js +19 -5
- package/dist/src/planner/analysis/check-extraction.js.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.js +7 -0
- package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
- package/dist/src/planner/analysis/scalar-param-usage.d.ts +6 -3
- package/dist/src/planner/analysis/scalar-param-usage.d.ts.map +1 -1
- package/dist/src/planner/analysis/scalar-param-usage.js +32 -4
- package/dist/src/planner/analysis/scalar-param-usage.js.map +1 -1
- package/dist/src/planner/building/coercion.js +1 -1
- package/dist/src/planner/building/coercion.js.map +1 -1
- package/dist/src/planner/building/schema-authored-context.d.ts +7 -7
- package/dist/src/planner/building/schema-authored-context.js +7 -7
- package/dist/src/planner/nodes/scalar.d.ts +19 -1
- package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
- package/dist/src/planner/nodes/scalar.js +13 -2
- package/dist/src/planner/nodes/scalar.js.map +1 -1
- package/dist/src/planner/nodes/set-operation-node.js +4 -0
- package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
- package/dist/src/runtime/emit/add-constraint.js +44 -1
- package/dist/src/runtime/emit/add-constraint.js.map +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/alter-table.js +20 -32
- package/dist/src/runtime/emit/alter-table.js.map +1 -1
- package/dist/src/runtime/emit/ddl-event-scope.d.ts +20 -13
- package/dist/src/runtime/emit/ddl-event-scope.d.ts.map +1 -1
- package/dist/src/runtime/emit/ddl-event-scope.js +20 -13
- package/dist/src/runtime/emit/ddl-event-scope.js.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.js +9 -4
- package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
- package/dist/src/runtime/emit/parameter.d.ts.map +1 -1
- package/dist/src/runtime/emit/parameter.js +13 -8
- package/dist/src/runtime/emit/parameter.js.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.d.ts +17 -6
- package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.js +169 -31
- package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
- package/dist/src/schema/catalog-rendering.d.ts +26 -0
- package/dist/src/schema/catalog-rendering.d.ts.map +1 -1
- package/dist/src/schema/catalog-rendering.js +54 -5
- package/dist/src/schema/catalog-rendering.js.map +1 -1
- package/dist/src/schema/catalog.d.ts +58 -2
- package/dist/src/schema/catalog.d.ts.map +1 -1
- package/dist/src/schema/catalog.js +45 -34
- package/dist/src/schema/catalog.js.map +1 -1
- package/dist/src/schema/constraint-builder.d.ts +67 -11
- package/dist/src/schema/constraint-builder.d.ts.map +1 -1
- package/dist/src/schema/constraint-builder.js +115 -12
- package/dist/src/schema/constraint-builder.js.map +1 -1
- package/dist/src/schema/ddl-generator.d.ts +21 -0
- package/dist/src/schema/ddl-generator.d.ts.map +1 -1
- package/dist/src/schema/ddl-generator.js +7 -1
- package/dist/src/schema/ddl-generator.js.map +1 -1
- package/dist/src/schema/rename/self-qualifier-strip.d.ts +27 -0
- package/dist/src/schema/rename/self-qualifier-strip.d.ts.map +1 -1
- package/dist/src/schema/rename/self-qualifier-strip.js +73 -0
- package/dist/src/schema/rename/self-qualifier-strip.js.map +1 -1
- package/dist/src/schema/rename-rewriter.d.ts +5 -5
- package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
- package/dist/src/schema/rename-rewriter.js +5 -5
- package/dist/src/schema/rename-rewriter.js.map +1 -1
- package/dist/src/schema/schema-differ.d.ts +42 -2
- package/dist/src/schema/schema-differ.d.ts.map +1 -1
- package/dist/src/schema/schema-differ.js +722 -67
- package/dist/src/schema/schema-differ.js.map +1 -1
- package/dist/src/schema/unique-enforcement.d.ts +23 -1
- package/dist/src/schema/unique-enforcement.d.ts.map +1 -1
- package/dist/src/schema/unique-enforcement.js +43 -1
- package/dist/src/schema/unique-enforcement.js.map +1 -1
- package/dist/src/types/temporal-ops.d.ts.map +1 -1
- package/dist/src/types/temporal-ops.js +16 -10
- package/dist/src/types/temporal-ops.js.map +1 -1
- package/dist/src/vtab/capabilities.d.ts +5 -2
- package/dist/src/vtab/capabilities.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.d.ts +30 -23
- package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.js +102 -111
- package/dist/src/vtab/memory/layer/manager.js.map +1 -1
- package/dist/src/vtab/module.d.ts +16 -0
- package/dist/src/vtab/module.d.ts.map +1 -1
- 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
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1404
|
-
* Returns undefined for constraint kinds that are not
|
|
1405
|
-
* constraints (NOT NULL / NULL / DEFAULT / COLLATE /
|
|
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
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
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
|
|
1439
|
-
const
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
1454
|
+
}
|
|
1455
|
+
const lower = tc.name.toLowerCase();
|
|
1456
|
+
if (named.has(lower))
|
|
1447
1457
|
return;
|
|
1448
|
-
|
|
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
|
|
1452
|
-
|
|
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(
|
|
1470
|
+
add(tc, col.name);
|
|
1462
1471
|
}
|
|
1463
1472
|
}
|
|
1464
|
-
return
|
|
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`)
|
|
1788
|
-
//
|
|
1789
|
-
//
|
|
1790
|
-
const
|
|
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
|
-
|
|
2407
|
-
|
|
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
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
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(
|
|
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(
|
|
2678
|
+
pushText(`${alterColumn} DROP DEFAULT`, u => u.undoDropDefault(alter.tableName, colAlter.columnName, alterColumn));
|
|
2492
2679
|
}
|
|
2493
2680
|
}
|
|
2494
2681
|
else {
|
|
2495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|