@quereus/quereus 4.20.0 → 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.
@@ -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';
@@ -10,7 +11,7 @@ import { Parser } from '../parser/parser.js';
10
11
  import { cloneExpr, cloneQueryExpr } from '../planner/mutation/scope-transform.js';
11
12
  import { normalizeCollationName } from '../util/comparison.js';
12
13
  import { inferType } from '../types/registry.js';
13
- import { resolveDefaultCollation } from './table.js';
14
+ import { resolveDefaultCollation, disambiguateAutoConstraintName } from './table.js';
14
15
  const log = createLogger('schema:differ');
15
16
  const warnLog = log.extend('warn');
16
17
  /** Reserved tag namespace prefix used for differ-recognized hints. */
@@ -1399,11 +1400,12 @@ columnRenamesByTable, schemaName) {
1399
1400
  return { ...stmt, columns, where };
1400
1401
  }
1401
1402
  /**
1402
- * Converts a column-level constraint carrying a name into the equivalent
1403
- * table-level {@link AST.TableConstraint}, so it can be stringified into an
1404
- * `ADD CONSTRAINT` fragment and diffed by name alongside table-level constraints.
1405
- * Returns undefined for constraint kinds that are not lifecycle-managed named
1406
- * 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).
1407
1409
  */
1408
1410
  function columnConstraintToTableConstraint(columnName, cc) {
1409
1411
  switch (cc.type) {
@@ -1422,13 +1424,18 @@ function columnConstraintToTableConstraint(columnName, cc) {
1422
1424
  }
1423
1425
  }
1424
1426
  /**
1425
- * Gathers declared *user-named* CHECK / UNIQUE / FOREIGN KEY constraints from a
1426
- * declared table — both table-level and column-level (carrying an explicit name)
1427
- * — keyed by lowercased name. PRIMARY KEY is excluded (handled by
1428
- * `primaryKeyChange`); engine-synthesized `_`-prefixed names are excluded to stay
1429
- * 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
1430
1432
  * wins (a duplicate user constraint name is a separate validation concern).
1431
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
+ *
1432
1439
  * `schemaName` is the schema this table is being diffed under (the differ runs
1433
1440
  * per schema — it is the CHILD schema for any FK declared here). It is threaded
1434
1441
  * into the canonical-body render so an FK's explicit own-schema qualifier folds
@@ -1436,33 +1443,34 @@ function columnConstraintToTableConstraint(columnName, cc) {
1436
1443
  * {@link constraintBodyToCanonicalString}); a genuine cross-schema parent stays a
1437
1444
  * body-change channel.
1438
1445
  */
1439
- function collectDeclaredNamedConstraints(declaredTable, schemaName) {
1440
- const out = new Map();
1441
- const add = (name, tags, tc) => {
1442
- if (!name)
1443
- return;
1444
- const lower = name.toLowerCase();
1445
- 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 });
1446
1453
  return;
1447
- if (out.has(lower))
1454
+ }
1455
+ const lower = tc.name.toLowerCase();
1456
+ if (named.has(lower))
1448
1457
  return;
1449
- 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 });
1450
1459
  };
1451
1460
  for (const c of declaredTable.tableStmt.constraints ?? []) {
1452
- if (c.type === 'primaryKey')
1453
- continue;
1454
- add(c.name, c.tags, c);
1461
+ if (c.type !== 'primaryKey')
1462
+ add(c);
1455
1463
  }
1456
1464
  for (const col of declaredTable.tableStmt.columns) {
1465
+ if (isAddedColumn(col.name.toLowerCase()))
1466
+ continue;
1457
1467
  for (const cc of col.constraints ?? []) {
1458
- if (!cc.name)
1459
- continue;
1460
1468
  const tc = columnConstraintToTableConstraint(col.name, cc);
1461
1469
  if (tc)
1462
- add(cc.name, cc.tags, tc);
1470
+ add(tc, col.name);
1463
1471
  }
1464
1472
  }
1465
- return out;
1473
+ return { named, unnamed };
1466
1474
  }
1467
1475
  /**
1468
1476
  * Inverse-applies the in-diff column renames to a constraint's column list: maps
@@ -1655,6 +1663,141 @@ resolveDeclaredColumn) {
1655
1663
  return d.definition;
1656
1664
  }
1657
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
+ }
1658
1801
  /**
1659
1802
  * Resolves a declared table's column renames against its matched actual catalog
1660
1803
  * table — the map-building + {@link resolveRenames} step shared by both the
@@ -1785,10 +1928,11 @@ defaultCollation) {
1785
1928
  // `constraints` list AND column-level constraints carrying an explicit name
1786
1929
  // (e.g. `qty int constraint chk_qty check (qty > 0)`) — the actual catalog's
1787
1930
  // `namedConstraints` already merges both. PRIMARY KEY constraints are excluded
1788
- // (PK changes flow through `primaryKeyChange`); auto-prefixed (`_`) names are
1789
- // excluded to stay symmetric with the catalog (see catalog.ts) so an unnamed
1790
- // declared constraint never churns add/drop against its synthesized actual name.
1791
- 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;
1792
1936
  const actualNamedConstraints = new Map();
1793
1937
  for (const c of actualTable.namedConstraints ?? []) {
1794
1938
  actualNamedConstraints.set(c.name.toLowerCase(), c);
@@ -1863,6 +2007,21 @@ defaultCollation) {
1863
2007
  pureDropCount++;
1864
2008
  }
1865
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);
1866
2025
  if (constraintsToAdd.length > 0)
1867
2026
  diff.constraintsToAdd = constraintsToAdd;
1868
2027
  if (constraintsToDrop.length > 0)
@@ -2824,8 +2983,11 @@ class UndoRenderer {
2824
2983
  }
2825
2984
  readdConstraint(table, name, quotedTable) {
2826
2985
  // `constraintsToDrop` carries pre-apply names (a renamed constraint is never
2827
- // also dropped), so the lookup is direct rather than through the renames.
2828
- const constraint = this.tableNow(table)?.namedConstraints?.find(c => sameName(c.name, name));
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));
2829
2991
  if (!constraint)
2830
2992
  return NOTHING_TO_UNDO;
2831
2993
  if (!constraint.bodyAst)