@quereus/quereus 4.13.1 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/src/func/builtins/explain.js +1 -1
  2. package/dist/src/func/builtins/explain.js.map +1 -1
  3. package/dist/src/planner/mutation/capture-correlation.d.ts +57 -0
  4. package/dist/src/planner/mutation/capture-correlation.d.ts.map +1 -0
  5. package/dist/src/planner/mutation/capture-correlation.js +57 -0
  6. package/dist/src/planner/mutation/capture-correlation.js.map +1 -0
  7. package/dist/src/planner/mutation/decomposition.d.ts.map +1 -1
  8. package/dist/src/planner/mutation/decomposition.js +86 -23
  9. package/dist/src/planner/mutation/decomposition.js.map +1 -1
  10. package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -1
  11. package/dist/src/planner/mutation/lens-enforcement.js +15 -20
  12. package/dist/src/planner/mutation/lens-enforcement.js.map +1 -1
  13. package/dist/src/planner/mutation/multi-source.d.ts +52 -11
  14. package/dist/src/planner/mutation/multi-source.d.ts.map +1 -1
  15. package/dist/src/planner/mutation/multi-source.js +273 -86
  16. package/dist/src/planner/mutation/multi-source.js.map +1 -1
  17. package/dist/src/planner/mutation/set-op.d.ts.map +1 -1
  18. package/dist/src/planner/mutation/set-op.js +4 -12
  19. package/dist/src/planner/mutation/set-op.js.map +1 -1
  20. package/dist/src/planner/mutation/single-source.js +1 -1
  21. package/dist/src/planner/mutation/single-source.js.map +1 -1
  22. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  23. package/dist/src/runtime/emit/alter-table.js +8 -10
  24. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  25. package/dist/src/runtime/emit/materialized-view-helpers.d.ts +16 -4
  26. package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
  27. package/dist/src/runtime/emit/materialized-view-helpers.js +182 -25
  28. package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
  29. package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -1
  30. package/dist/src/runtime/emit/materialized-view.js +1 -1
  31. package/dist/src/runtime/emit/materialized-view.js.map +1 -1
  32. package/dist/src/schema/ddl-generator.d.ts.map +1 -1
  33. package/dist/src/schema/ddl-generator.js +65 -24
  34. package/dist/src/schema/ddl-generator.js.map +1 -1
  35. package/dist/src/schema/lens-ack.d.ts +1 -1
  36. package/dist/src/schema/lens-ack.js +2 -2
  37. package/dist/src/schema/lens-compiler.d.ts.map +1 -1
  38. package/dist/src/schema/lens-compiler.js +63 -34
  39. package/dist/src/schema/lens-compiler.js.map +1 -1
  40. package/dist/src/schema/lens-prover.d.ts +4 -4
  41. package/dist/src/schema/lens-prover.d.ts.map +1 -1
  42. package/dist/src/schema/lens-prover.js +13 -5
  43. package/dist/src/schema/lens-prover.js.map +1 -1
  44. package/dist/src/schema/lens.d.ts +1 -1
  45. package/dist/src/schema/manager.d.ts.map +1 -1
  46. package/dist/src/schema/manager.js +25 -13
  47. package/dist/src/schema/manager.js.map +1 -1
  48. package/dist/src/schema/reserved-tags.js +1 -1
  49. package/dist/src/schema/reserved-tags.js.map +1 -1
  50. package/dist/src/schema/schema-differ.d.ts.map +1 -1
  51. package/dist/src/schema/schema-differ.js +4 -3
  52. package/dist/src/schema/schema-differ.js.map +1 -1
  53. package/dist/src/schema/table.d.ts +41 -34
  54. package/dist/src/schema/table.d.ts.map +1 -1
  55. package/dist/src/schema/table.js +24 -43
  56. package/dist/src/schema/table.js.map +1 -1
  57. package/dist/src/vtab/memory/layer/alter-column.d.ts +8 -1
  58. package/dist/src/vtab/memory/layer/alter-column.d.ts.map +1 -1
  59. package/dist/src/vtab/memory/layer/alter-column.js +20 -10
  60. package/dist/src/vtab/memory/layer/alter-column.js.map +1 -1
  61. package/dist/src/vtab/memory/layer/base.d.ts +9 -1
  62. package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
  63. package/dist/src/vtab/memory/layer/base.js +12 -1
  64. package/dist/src/vtab/memory/layer/base.js.map +1 -1
  65. package/dist/src/vtab/memory/layer/manager.d.ts +23 -18
  66. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  67. package/dist/src/vtab/memory/layer/manager.js +44 -33
  68. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  69. package/dist/src/vtab/memory/layer/row-convert.d.ts +13 -0
  70. package/dist/src/vtab/memory/layer/row-convert.d.ts.map +1 -1
  71. package/dist/src/vtab/memory/layer/row-convert.js +24 -0
  72. package/dist/src/vtab/memory/layer/row-convert.js.map +1 -1
  73. package/dist/src/vtab/memory/layer/transaction.d.ts +21 -7
  74. package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
  75. package/dist/src/vtab/memory/layer/transaction.js +30 -14
  76. package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
  77. package/dist/src/vtab/module.d.ts +20 -2
  78. package/dist/src/vtab/module.d.ts.map +1 -1
  79. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"multi-source.d.ts","sourceRoot":"","sources":["../../../../src/planner/mutation/multi-source.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"multi-source.d.ts","sourceRoot":"","sources":["../../../../src/planner/mutation/multi-source.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@ import { buildExpression } from '../building/expression.js';
8
8
  import { columnSchemaToScalarType } from '../type-utils.js';
9
9
  import { JoinNode } from '../nodes/join-node.js';
10
10
  import { EXISTENCE_FLAG_TYPE } from '../nodes/join-utils.js';
11
+ import { captureKeyEquality } from './capture-correlation.js';
11
12
  import { FilterNode } from '../nodes/filter.js';
12
13
  import { ProjectNode } from '../nodes/project-node.js';
13
14
  import { raiseMutationDiagnostic } from './mutation-diagnostic.js';
@@ -104,6 +105,21 @@ import { requireValidatedNewRefIndex } from '../analysis/authored-inverse.js';
104
105
  * `k<sideIndex>_<pkColumnOrdinal>` ({@link keyColumnName}) — so a composite-PK side
105
106
  * contributes `k<side>_0, k<side>_1, …`. This flattened per-side-per-column shape is
106
107
  * what generalizes the substrate past the retired single-column `(k0, k1)` tuple.
108
+ *
109
+ * Two invariants keep the capture sound now that a key column may be nullable
110
+ * (`docs/schema.md` § Primary-key nullability — NULL is an ordinary, self-equal key value):
111
+ *
112
+ * 1. **Correlation is NULL-safe per nullable key column.** Every reader correlates a
113
+ * captured key column through {@link captureKeyEquality}: plain `=` for a column
114
+ * declared NOT NULL (index-friendly), `left = right or (left is null and right is
115
+ * null)` for a nullable one — so a row keyed NULL stays addressable.
116
+ * 2. **"Had no join partner" is carried explicitly.** For each captured non-preserved
117
+ * (outer-join) side the capture projects a boolean **match marker**
118
+ * (`m<side>` — {@link matchFlagName}), an {@link ExistenceColumnSpec} flag set by the
119
+ * join runtime from the actual null-extension decision. The outer-join branches read
120
+ * the marker — never "all of the side's captured key columns are NULL", which a real
121
+ * partner keyed NULL would satisfy too. A NULL marker (the flag itself null-extended
122
+ * by an enclosing outer join) reads as false.
107
123
  */
108
124
  export const MS_UPDATE_KEYS_CTE = '__vmupd_keys';
109
125
  /**
@@ -113,6 +129,33 @@ export const MS_UPDATE_KEYS_CTE = '__vmupd_keys';
113
129
  export function keyColumnName(sideIndex, j) {
114
130
  return `k${sideIndex}_${j}`;
115
131
  }
132
+ /**
133
+ * The capture column name of side `sideIndex`'s **match marker** — the boolean
134
+ * "did this non-preserved side have a join partner" flag the capture projects
135
+ * alongside the side's `k<side>_<j>` key columns (invariant 2 of
136
+ * {@link MS_UPDATE_KEYS_CTE}). Minted only for captured non-preserved sides;
137
+ * readers and the capture projection both derive the name here so they cannot
138
+ * drift. Namespace-disjoint from `k<side>_<j>` and the `srcN` value aliases.
139
+ */
140
+ export function matchFlagName(sideIndex) {
141
+ return `m${sideIndex}`;
142
+ }
143
+ /**
144
+ * The AST predicate reading side `sideIndex`'s capture match marker as "this side had
145
+ * NO join partner": `k.m<side> is null or not k.m<side>`. A NULL marker — the flag
146
+ * itself null-extended by an enclosing outer join in an n-way body — must read as
147
+ * false ("no partner"), so the declared NOT NULL of {@link EXISTENCE_FLAG_TYPE} is
148
+ * deliberately not assumed here.
149
+ */
150
+ function matchFlagIsFalse(sideIndex) {
151
+ const flag = () => ({ type: 'column', name: matchFlagName(sideIndex), table: 'k' });
152
+ return {
153
+ type: 'binary',
154
+ operator: 'OR',
155
+ left: { type: 'unary', operator: 'IS NULL', expr: flag() },
156
+ right: { type: 'unary', operator: 'NOT', expr: flag() },
157
+ };
158
+ }
116
159
  // --- entry ----------------------------------------------------------------
117
160
  /**
118
161
  * True when the view body is a join (and so routes to this multi-source path
@@ -1351,12 +1394,14 @@ export function decomposeUpdate(ctx, view, analysis, stmt, sourceValues, capture
1351
1394
  // Joins — Updates on a non-preserved-side column): where the non-preserved side
1352
1395
  // matched it is an ordinary base update; where the row is null-extended (no match) it
1353
1396
  // is rewritten as an insert on that side. Both ride the up-front `__vmupd_keys`
1354
- // capture, materialized pre-mutation over the join body: the matched op reads its
1355
- // captured PK (non-null for a matched row); the null-extended op fires for the rows
1356
- // whose captured PK is null. The assigned value is captured ONCE (so both branches
1357
- // read the identical pre-mutation value), and the matched op reads it back keyed on
1358
- // the non-preserved PK. Needs the capture carrier; the legacy `propagateMultiSource`
1359
- // path (no carrier) keeps deferring with `unsupported-outer-join-update`.
1397
+ // capture, materialized pre-mutation over the join body, and both partition on the
1398
+ // capture's explicit MATCH MARKER (invariant 2 of MS_UPDATE_KEYS_CTE): the matched
1399
+ // op correlates only marker-true capture rows; the null-extended op fires for the
1400
+ // marker-false rows. The assigned value is captured ONCE (so both branches read the
1401
+ // identical pre-mutation value), and the matched op reads it back keyed NULL-safely
1402
+ // on the non-preserved PK. Needs the capture carrier; the legacy
1403
+ // `propagateMultiSource` path (no carrier) keeps deferring with
1404
+ // `unsupported-outer-join-update`.
1360
1405
  if (out.nullExtended && out.sideIndex !== undefined && out.baseColumn) {
1361
1406
  if (!registerCapturedExpr) {
1362
1407
  raiseMutationDiagnostic({
@@ -1382,13 +1427,15 @@ export function decomposeUpdate(ctx, view, analysis, stmt, sourceValues, capture
1382
1427
  const baseValue = substituteViewColumns(ctx, asg.value, analysis.viewColToBaseRef, view, analysis.sides);
1383
1428
  const valAlias = registerCapturedExpr(`neval:${out.sideIndex}:${out.baseColumn.toLowerCase()}`, baseValue);
1384
1429
  // Matched rows: a per-side UPDATE reading the captured value back, correlated by
1385
- // the non-preserved side's PK (`buildCapturedKeyPredicate` already filters to
1386
- // matched rows — a null captured PK never equals a real one). The read-back is
1387
- // `min`-de-duped per non-preserved partner: when N preserved rows share one
1388
- // existing partner, that partner's PK matches all N capture rows, so a bare scalar
1389
- // read would error `Scalar subquery returned more than one row` — `min` collapses
1390
- // the shared-partner group to one value (a no-op for a constant / np-only SET).
1391
- perSide[out.sideIndex].push({ column: out.baseColumn, value: capturedValueSubquery(valAlias, out.sideIndex, requireKeyColumns(view, npSide), 'min', SELF_ALIAS, captureRelationName) });
1430
+ // the non-preserved side's PK (`buildCapturedKeyPredicate` restricts the op to
1431
+ // matched rows via the capture's match marker; `matchedOnly` gates this read-back
1432
+ // the same way, so a null-extended capture row — key columns NULL — cannot feed a
1433
+ // target row whose key genuinely holds NULL). The read-back is `min`-de-duped per
1434
+ // non-preserved partner: when N preserved rows share one existing partner, that
1435
+ // partner's PK matches all N capture rows, so a bare scalar read would error
1436
+ // `Scalar subquery returned more than one row` — `min` collapses the
1437
+ // shared-partner group to one value (a no-op for a constant / np-only SET).
1438
+ perSide[out.sideIndex].push({ column: out.baseColumn, value: capturedValueSubquery(valAlias, out.sideIndex, requireKeyColumns(view, npSide), { dedupAggregate: 'min', correlationAlias: SELF_ALIAS, captureRelationName, matchedOnly: true }) });
1392
1439
  // Null-extended rows: accumulate the (column, captured value) for this side's
1393
1440
  // single materialization insert, built after the loop.
1394
1441
  let list = nullExtendedBySide.get(out.sideIndex);
@@ -1565,9 +1612,11 @@ export function decomposeUpdate(ctx, view, analysis, stmt, sourceValues, capture
1565
1612
  ops.push(buildNullExtendedInsert(ctx, view, analysis, sideIndex, cols, registerCapturedExpr, stmt, captureRelationName));
1566
1613
  }
1567
1614
  // Existence-flip deletes (§ Existence columns): `set hasB = false` removes the matched
1568
- // non-preserved rows (their captured PK is non-null; a null-extended row's captured PK
1569
- // is null, so the same captured-key EXISTS naturally excludes it). The preserved side is
1570
- // untouched, so a deleted row reads back null-extended (`hasB` now false).
1615
+ // non-preserved rows — the captured-key EXISTS carries the capture's match-marker
1616
+ // conjunct (`buildCapturedKeyPredicate` on a non-preserved side), so a null-extended
1617
+ // capture row is excluded explicitly, not by its NULL key columns failing `=`. The
1618
+ // preserved side is untouched, so a deleted row reads back null-extended (`hasB` now
1619
+ // false).
1571
1620
  for (const sideIndex of existenceDeleteSides) {
1572
1621
  const side = analysis.sides[sideIndex];
1573
1622
  const where = buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName);
@@ -1595,12 +1644,14 @@ export function decomposeUpdate(ctx, view, analysis, stmt, sourceValues, capture
1595
1644
  /**
1596
1645
  * Build the null-extended materialization INSERT for a non-preserved outer-join side:
1597
1646
  * `insert into <np> (<joinKey>, <set cols…>) select k.<jk>, min(k.<val…>) from __vmupd_keys k
1598
- * where <every np PK k col> is null and k.<jk> is not null group by k.<jk>` (§ Outer Joins —
1599
- * Updates). The `group by k.<jk>` de-dups per dangling join key so a shared missing partner
1600
- * materializes exactly once (a fan-out of N preserved rows would otherwise double-insert the
1601
- * partner PK); the value projections are `min` so each is single-valued per group. It
1602
- * fires only for the affected rows the join null-extended (the non-preserved PK captured
1603
- * null) whose preserved-side join key is non-null (a null key cannot seed a joinable row).
1647
+ * where (k.m<np> is null or not k.m<np>) and k.<jk> is not null group by k.<jk>` (§ Outer
1648
+ * Joins — Updates). The `group by k.<jk>` de-dups per dangling join key so a shared missing
1649
+ * partner materializes exactly once (a fan-out of N preserved rows would otherwise
1650
+ * double-insert the partner PK); the value projections are `min` so each is single-valued
1651
+ * per group. It fires only for the affected rows the join null-extended — the capture's
1652
+ * match marker says "no partner" (invariant 2 of {@link MS_UPDATE_KEYS_CTE}; a real matched
1653
+ * partner keyed NULL keeps its marker true and is never re-materialized) — whose
1654
+ * preserved-side join key is non-null (a null key cannot seed a joinable row).
1604
1655
  * The new row carries the EC join key (so the preserved row joins it), the assigned
1605
1656
  * value(s) read from the same pre-mutation `__vmupd_keys` capture the matched UPDATE reads,
1606
1657
  * and base defaults for everything else; a NOT NULL base column without a default that no
@@ -1636,10 +1687,12 @@ function buildNullExtendedInsert(_ctx, view, analysis, npSideIndex, cols, regist
1636
1687
  projections.push({ type: 'column', expr: { type: 'function', name: 'min', args: [{ type: 'column', name: c.valAlias, table: 'k' }] }, alias: c.baseColumn });
1637
1688
  }
1638
1689
  assertNullExtendedInsertCovered(view, npSide.schema, targetColumns);
1639
- // Restrict to the null-extended partition: every captured PK column of the non-preserved
1640
- // side is null (no join match), and the preserved join key is non-null (a null key has
1641
- // no joinable row to create).
1642
- const conds = requireKeyColumns(view, npSide).map((_pk, j) => ({ type: 'unary', operator: 'IS NULL', expr: { type: 'column', name: keyColumnName(npSideIndex, j), table: 'k' } }));
1690
+ // Restrict to the null-extended partition: the capture's match marker says this side
1691
+ // had NO join partner (invariant 2 of MS_UPDATE_KEYS_CTE — never "every captured PK
1692
+ // column is null", which a real matched partner keyed NULL would satisfy too; a NULL
1693
+ // marker reads as false), and the preserved join key is non-null (a null key has no
1694
+ // joinable row to create).
1695
+ const conds = [matchFlagIsFalse(npSideIndex)];
1643
1696
  conds.push({ type: 'unary', operator: 'IS NOT NULL', expr: { type: 'column', name: jkAlias, table: 'k' } });
1644
1697
  const where = conds.reduce((acc, c) => combineAnd(acc, c));
1645
1698
  const select = {
@@ -1755,7 +1808,10 @@ function preservedSideIndices(sides) {
1755
1808
  * <pk1> …])` — matching ALL of the side's PK columns (composite keys included) against
1756
1809
  * the up-front materialized key set (the both-sides-assigned UPDATE path, every
1757
1810
  * single-side update/delete, and the multi-side DELETE fan-out; see
1758
- * {@link MS_UPDATE_KEYS_CTE}). The right-hand `<pk_j>` are unqualified, so they bind to
1811
+ * {@link MS_UPDATE_KEYS_CTE}). Each per-column equality is NULL-safe when the key column
1812
+ * is declared nullable ({@link captureKeyEquality} — invariant 1), and a non-preserved
1813
+ * side additionally conjoins its match marker (`k.m<side>` — invariant 2) so only
1814
+ * MATCHED capture rows correlate. The right-hand `<pk_j>` are unqualified, so they bind to
1759
1815
  * the base op's own target table; `k.k<side>_<j>` reads the captured column. The builder
1760
1816
  * injects `__vmupd_keys` into the base op's planning `cteNodes` (resolving to the
1761
1817
  * context-backed key relation), so this is read by descriptor rather than re-querying
@@ -1764,12 +1820,15 @@ function preservedSideIndices(sides) {
1764
1820
  */
1765
1821
  function buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName = MS_UPDATE_KEYS_CTE) {
1766
1822
  const keyCols = requireKeyColumns(view, side);
1767
- const conds = keyCols.map((pkCol, j) => ({
1768
- type: 'binary',
1769
- operator: '=',
1770
- left: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' },
1771
- right: { type: 'column', name: pkCol },
1772
- }));
1823
+ const conds = keyCols.map((pk, j) => captureKeyEquality({ type: 'column', name: keyColumnName(sideIndex, j), table: 'k' }, { type: 'column', name: pk.name }, pk.nullable));
1824
+ // A non-preserved (outer-join) side correlates only its MATCHED capture rows: the
1825
+ // capture's match marker (invariant 2 of MS_UPDATE_KEYS_CTE) is AND'd in, so a
1826
+ // null-extended capture row — whose captured key columns are NULL — cannot
1827
+ // NULL-safe-match a real partner row whose key genuinely holds NULL. A NULL marker
1828
+ // reads as false (the bare-column conjunct is not-true for NULL).
1829
+ if (!side.preserved) {
1830
+ conds.push({ type: 'column', name: matchFlagName(sideIndex), table: 'k' });
1831
+ }
1773
1832
  return {
1774
1833
  type: 'exists',
1775
1834
  subquery: {
@@ -1847,6 +1906,76 @@ export function capturedSideIndices(baseOps, analysis) {
1847
1906
  }
1848
1907
  return [...set].sort((a, b) => a - b);
1849
1908
  }
1909
+ /** True when the `TableReferenceNode` with plan-node id `tableId` sits anywhere beneath `node`. */
1910
+ function subtreeContainsTable(node, tableId) {
1911
+ if (node instanceof TableReferenceNode)
1912
+ return Number(node.id) === tableId;
1913
+ return node.getRelations().some(child => subtreeContainsTable(child, tableId));
1914
+ }
1915
+ /**
1916
+ * Rebuild the capture's join tree with one synthetic {@link ExistenceColumnSpec} match
1917
+ * flag per requested non-preserved side (invariant 2 of {@link MS_UPDATE_KEYS_CTE}).
1918
+ * Each flag is appended at the **nearest enclosing join that null-extends its side** —
1919
+ * the join whose non-preserved child subtree contains the side's table — so the flag's
1920
+ * value is the actual null-extension decision for that side (`runtime/emit/join.ts`
1921
+ * sets it from the match bit, never from a re-evaluation of the ON predicate). For an
1922
+ * n-way body the ancestors are rebuilt around the flagged join; a flag minted below an
1923
+ * enclosing outer join is itself nullable-widened by the parent's attribute rebuild
1924
+ * (readers treat NULL as false). Attribute identity is preserved throughout: the
1925
+ * children are reused verbatim and only the `JoinNode` wrappers are re-created, with
1926
+ * each flag carrying a freshly minted stable attribute id.
1927
+ *
1928
+ * NOTE: a flag-bearing join is pinned to the nested-loop emitter (the existence-flag
1929
+ * guards disable physical join selection), so an outer-join write's capture forgoes
1930
+ * hash/merge join. The capture materializes once per statement; if outer-join view
1931
+ * writes over large bodies ever show up as slow, teach the physical joins to emit
1932
+ * match flags rather than weakening the guards.
1933
+ */
1934
+ function rebuildJoinWithMatchFlags(view, root, sides, npSideIndices) {
1935
+ const flagsBySide = new Map();
1936
+ // Post-order: inner joins rebuild first, so each side is claimed at the NEAREST
1937
+ // join that null-extends it (a nested outer join claims before its ancestors).
1938
+ const rebuild = (node) => {
1939
+ if (!(node instanceof JoinNode))
1940
+ return node;
1941
+ const newLeft = rebuild(node.left);
1942
+ const newRight = rebuild(node.right);
1943
+ const nullExtends = (child) => (node.joinType === 'left' && child === 'right')
1944
+ || (node.joinType === 'right' && child === 'left')
1945
+ || node.joinType === 'full';
1946
+ const newSpecs = [];
1947
+ for (const i of npSideIndices) {
1948
+ if (flagsBySide.has(i))
1949
+ continue;
1950
+ const tableId = Number(sides[i].table.id);
1951
+ const child = subtreeContainsTable(node.left, tableId) ? 'left'
1952
+ : subtreeContainsTable(node.right, tableId) ? 'right'
1953
+ : undefined;
1954
+ if (!child || !nullExtends(child))
1955
+ continue;
1956
+ const spec = { attrId: PlanNode.nextAttrId(), name: matchFlagName(i), side: child };
1957
+ flagsBySide.set(i, spec);
1958
+ newSpecs.push(spec);
1959
+ }
1960
+ if (newLeft === node.left && newRight === node.right && newSpecs.length === 0)
1961
+ return node;
1962
+ return new JoinNode(node.scope, newLeft, newRight, node.joinType, node.condition, node.usingColumns, [...(node.existence ?? []), ...newSpecs]);
1963
+ };
1964
+ const rebuilt = rebuild(root);
1965
+ // A non-preserved side is, by collectJoinSources' classification, null-extended by
1966
+ // SOME enclosing outer join — a missing claim means the planned tree and the AST
1967
+ // classification disagree.
1968
+ for (const i of npSideIndices) {
1969
+ if (!flagsBySide.has(i)) {
1970
+ raiseMutationDiagnostic({
1971
+ reason: 'unsupported-join',
1972
+ table: view.name,
1973
+ message: `internal: cannot write through view '${view.name}': non-preserved side '${sides[i].schema.name}' did not resolve to a null-extending join for its match marker`,
1974
+ });
1975
+ }
1976
+ }
1977
+ return { joinNode: rebuilt, flagsBySide };
1978
+ }
1850
1979
  /**
1851
1980
  * Build the up-front identity capture: each affected view row's base-PK identities,
1852
1981
  * by the same identifying predicate the base ops route on (user WHERE → base ∧ body
@@ -1885,9 +2014,23 @@ export function buildMultiSourceKeyCapture(ctx, view, where, analysis, sideIndic
1885
2014
  const predicate = idPredicateAst
1886
2015
  ? buildExpression({ ...ctx, scope: analysis.joinScope }, idPredicateAst)
1887
2016
  : undefined;
2017
+ // Match markers (invariant 2 of MS_UPDATE_KEYS_CTE): the capture — and ONLY the
2018
+ // capture — reads the join through a rebuilt copy carrying one synthetic
2019
+ // `ExistenceColumnSpec` per captured non-preserved side, so "did this side have a
2020
+ // join partner" is materialized explicitly (set by the join runtime from the actual
2021
+ // null-extension decision) instead of being inferred from NULL key columns. The
2022
+ // rebuild preserves every pre-existing attribute id (`buildJoinAttributes` copies
2023
+ // the child attributes verbatim), so `analysis.joinScope` still resolves the
2024
+ // identifying predicate and PK projections against it; the RETURNING re-query keeps
2025
+ // reading `analysis.joinNode` unchanged and reads `k.m<side>` out of the capture.
2026
+ // An all-preserved (inner-join) capture rebuilds nothing — byte-identical shape.
2027
+ const npSideIndices = sideIndices.filter(i => !analysis.sides[i].preserved);
2028
+ const { joinNode: captureJoin, flagsBySide } = npSideIndices.length > 0
2029
+ ? rebuildJoinWithMatchFlags(view, analysis.joinNode, analysis.sides, npSideIndices)
2030
+ : { joinNode: analysis.joinNode, flagsBySide: new Map() };
1888
2031
  const filtered = predicate
1889
- ? new FilterNode(analysis.joinScope, analysis.joinNode, predicate)
1890
- : analysis.joinNode;
2032
+ ? new FilterNode(analysis.joinScope, captureJoin, predicate)
2033
+ : captureJoin;
1891
2034
  // One capture column per requested side per PK column: `k<side>_<j>`. A composite-PK
1892
2035
  // side projects all its PK columns; the readers' EXISTS correlate on the same set.
1893
2036
  const keyColumns = [];
@@ -1897,13 +2040,43 @@ export function buildMultiSourceKeyCapture(ctx, view, where, analysis, sideIndic
1897
2040
  const pkCols = requireKeyColumns(view, side);
1898
2041
  pkCols.forEach((pk, j) => {
1899
2042
  const name = keyColumnName(i, j);
1900
- keyColumns.push({ name, type: columnSchemaToScalarType(columnByName(side.schema, pk)) });
2043
+ keyColumns.push({ name, type: columnSchemaToScalarType(columnByName(side.schema, pk.name)) });
1901
2044
  projections.push({
1902
- node: buildExpression({ ...ctx, scope: analysis.joinScope }, { type: 'column', name: pk, table: side.alias }),
2045
+ node: buildExpression({ ...ctx, scope: analysis.joinScope }, { type: 'column', name: pk.name, table: side.alias }),
1903
2046
  alias: name,
1904
2047
  });
1905
2048
  });
1906
2049
  }
2050
+ // Project each non-preserved side's match marker as a capture column (`m<side>`),
2051
+ // referenced directly by the flag's own attribute id — the scope knows nothing of the
2052
+ // synthetic flags. The attribute's type is read off the rebuilt join (nullable-widened
2053
+ // when an enclosing outer join can null-extend the flag itself; readers treat NULL as
2054
+ // false).
2055
+ if (npSideIndices.length > 0) {
2056
+ const captureAttrs = captureJoin.getAttributes();
2057
+ for (const i of npSideIndices) {
2058
+ const spec = flagsBySide.get(i);
2059
+ const columnIndex = captureAttrs.findIndex(a => a.id === spec.attrId);
2060
+ if (columnIndex < 0) {
2061
+ // Unreachable by construction (the flag is minted on a join inside the rebuilt
2062
+ // spine, and `buildJoinAttributes` propagates every child attribute to the root);
2063
+ // named explicitly so a future rebuild regression fails legibly instead of as an
2064
+ // undefined-property TypeError.
2065
+ raiseMutationDiagnostic({
2066
+ reason: 'unsupported-join',
2067
+ table: view.name,
2068
+ message: `internal: cannot write through view '${view.name}': match marker '${matchFlagName(i)}' is missing from the rebuilt capture join's attributes`,
2069
+ });
2070
+ }
2071
+ const flagType = captureAttrs[columnIndex].type;
2072
+ const name = matchFlagName(i);
2073
+ keyColumns.push({ name, type: flagType });
2074
+ projections.push({
2075
+ node: new ColumnReferenceNode(analysis.joinScope, { type: 'column', name }, flagType, spec.attrId, columnIndex),
2076
+ alias: name,
2077
+ });
2078
+ }
2079
+ }
1907
2080
  // Cross-source SET read values: project each partner base column the SET reads under
1908
2081
  // its stable `srcN` alias (over the same join-body scope), so every per-side base op's
1909
2082
  // correlated `select srcN from __vmupd_keys k where …` reads the captured pre-mutation
@@ -1929,15 +2102,16 @@ export function buildMultiSourceKeyCapture(ctx, view, where, analysis, sideIndic
1929
2102
  * still returned (single-source NEW semantics). It keeps only the structural join
1930
2103
  * ON-condition; the body/user WHERE is intentionally NOT re-applied.
1931
2104
  *
1932
- * The per-side identity is **preserved-keyed**: a preserved side matches by exact
1933
- * per-PK-column equality (`k.k<p>_<j> = s<p>.pk<j>`), while a non-preserved (outer-join
1934
- * null-extended) side uses a matched-OR-null disjunction `(AND_j k.k<np>_<j> =
1935
- * s<np>.pk<j>) OR (AND_j k.k<np>_<j> is null)`. This re-keys the re-query off the
1936
- * **stable preserved-side identity** so a freshly-materialized null-extended row (whose
1937
- * non-preserved PK was captured NULL) surfaces via its preserved-side equalities alone,
1938
- * rather than being silently dropped by a `NULL = <minted pk>` match. For an all-
1939
- * preserved (inner) join every side is exact equality — byte-identical to the prior
1940
- * behavior, so inner-join RETURNING is unchanged.
2105
+ * The per-side identity is **preserved-keyed**: a preserved side matches by per-PK-column
2106
+ * equality (`k.k<p>_<j> = s<p>.pk<j>`, NULL-safe per nullable key column —
2107
+ * {@link captureKeyEquality}), while a non-preserved (outer-join null-extended) side uses
2108
+ * a matched-OR-no-partner disjunction `(AND_j k.k<np>_<j> ≈ s<np>.pk<j>) OR (k.m<np> is
2109
+ * false-or-null)` keyed off the capture's **match marker** ({@link matchFlagName}). This
2110
+ * re-keys the re-query off the **stable preserved-side identity** so a freshly-materialized
2111
+ * null-extended row (whose marker was captured false) surfaces via its preserved-side
2112
+ * equalities alone, rather than being silently dropped by a `NULL = <minted pk>` match.
2113
+ * For an all-preserved (inner) join every side is plain equality over its NOT NULL key
2114
+ * columns — byte-identical to the prior behavior, so inner-join RETURNING is unchanged.
1941
2115
  *
1942
2116
  * Reads the shared {@link MultiSourceKeyCapture} the builder materializes
1943
2117
  * before the base ops fire (via its own freshly-minted key ref over the same
@@ -1954,36 +2128,33 @@ export function buildMultiSourceUpdateReturning(ctx, view, stmt, capture, analys
1954
2128
  // join row through `joinScope`.
1955
2129
  //
1956
2130
  // The per-side identity predicate is AND'd over all sides:
1957
- // - a **preserved** side keys by exact per-PK-column equality (`AND_j k.k<side>_<j>
1958
- // = s<side>.pk<j>`) — its PK is stable across the mutation and uniquely identifies
1959
- // the view row (the premise that makes a non-preserved column updatable at all);
1960
- // - a **non-preserved** (outer-join null-extended) side keys by a matched-OR-null
1961
- // disjunction `(AND_j k.k<np>_<j> = s<np>.pk<j>) OR (AND_j k.k<np>_<j> is null)`.
2131
+ // - a **preserved** side keys by per-PK-column equality (`AND_j k.k<side>_<j> =
2132
+ // s<side>.pk<j>`, NULL-safe when the column is declared nullable) — its PK is
2133
+ // stable across the mutation and uniquely identifies the view row (the premise
2134
+ // that makes a non-preserved column updatable at all);
2135
+ // - a **non-preserved** (outer-join null-extended) side keys by a matched-OR-no-
2136
+ // partner disjunction `(AND_j k.k<np>_<j> ≈ s<np>.pk<j>) OR (k.m<np> is null or
2137
+ // not k.m<np>)`.
1962
2138
  //
1963
- // A *matched* capture row (np PK non-null) takes the matched branch and finds the
1964
- // stable np row; the null branch is false (the np PK is non-null). A *materialized
1965
- // null-extended* capture row (np PK captured NULL — it had no pre-mutation partner)
1966
- // fails the matched branch (`null = …` is not-true) and takes the null branch, so it
1967
- // is identified by the preserved-side equalities ALONE — surfacing the freshly-minted
1968
- // partner row (and a preserved-side update touching a still-null-extended row, the
1969
- // latent partial-set bug #2). SQL three-valued comparison keeps the two branches
1970
- // disjoint, so no explicit `is not null` guard is needed.
2139
+ // A *matched* capture row (marker true) takes the matched branch and finds the
2140
+ // stable np row (NULL-safe per nullable key column, so a partner genuinely keyed
2141
+ // NULL is still found); its no-partner branch is false. A *materialized
2142
+ // null-extended* capture row (marker false — it had no pre-mutation partner) takes
2143
+ // the no-partner branch, so it is identified by the preserved-side equalities ALONE
2144
+ // — surfacing the freshly-minted partner row (and a preserved-side update touching
2145
+ // a still-null-extended row, the latent partial-set bug #2). The marker — not "the
2146
+ // captured np PK is all NULL", which a real partner keyed NULL would satisfy too —
2147
+ // is what partitions the two branches (invariant 2 of MS_UPDATE_KEYS_CTE); a NULL
2148
+ // marker reads as false.
1971
2149
  const sideConds = analysis.sides.map((side, sideIndex) => {
1972
2150
  const pkCols = requireKeyColumns(view, side);
1973
- const exact = pkCols.map((pk, j) => ({
1974
- type: 'binary',
1975
- operator: '=',
1976
- left: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' },
1977
- right: { type: 'column', name: pk, table: side.alias },
1978
- })).reduce((acc, c) => combineAnd(acc, c));
2151
+ const exact = pkCols.map((pk, j) => captureKeyEquality({ type: 'column', name: keyColumnName(sideIndex, j), table: 'k' }, { type: 'column', name: pk.name, table: side.alias }, pk.nullable)).reduce((acc, c) => combineAnd(acc, c));
1979
2152
  if (side.preserved)
1980
2153
  return exact;
1981
- // Null-extended branch: every captured PK column of this non-preserved side is null
1982
- // (no pre-mutation join partner), so the row is identified by the preserved sides'
1983
- // exact equalities alone.
1984
- const allNull = pkCols.map((_pk, j) => ({ type: 'unary', operator: 'IS NULL', expr: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' } }))
1985
- .reduce((acc, c) => combineAnd(acc, c));
1986
- return { type: 'binary', operator: 'OR', left: exact, right: allNull };
2154
+ // No-partner branch: the capture's match marker says this non-preserved side had
2155
+ // no pre-mutation join partner, so the row is identified by the preserved sides'
2156
+ // equalities alone.
2157
+ return { type: 'binary', operator: 'OR', left: exact, right: matchFlagIsFalse(sideIndex) };
1987
2158
  });
1988
2159
  // Read the capture's own relation name so a fresh-named capture's RETURNING re-query
1989
2160
  // reads back from (and injects under) the same relation its base ops do — the constant
@@ -2405,7 +2576,7 @@ function stripSideQualifier(expr, view, owning, owningSideIndex, allSides, regis
2405
2576
  // Qualify the owning-PK operands with the per-side UPDATE's collision-proof alias so
2406
2577
  // the read-back correlates to the target row even when this subquery nests inside a
2407
2578
  // user value subquery whose FROM has a same-named column (the bug-1 site).
2408
- return capturedValueSubquery(srcAlias, owningSideIndex, owningPk, undefined, SELF_ALIAS);
2579
+ return capturedValueSubquery(srcAlias, owningSideIndex, owningPk, { correlationAlias: SELF_ALIAS });
2409
2580
  };
2410
2581
  // QUALIFIED-only substitution: an owning-alias ref is re-qualified to the lowered
2411
2582
  // target's `__vm_self` correlation alias; a partner-alias ref
@@ -2467,14 +2638,23 @@ function stripSideQualifier(expr, view, owning, owningSideIndex, allSides, regis
2467
2638
  * `captureRelationName` is the relation the correlated read scans (`from <name> k`); it
2468
2639
  * defaults to {@link MS_UPDATE_KEYS_CTE} so the standalone multi-source and decomposition
2469
2640
  * callers are byte-identical, while a nested multi-source capture threads its fresh name.
2641
+ *
2642
+ * Correlation is NULL-safe per nullable owning-PK column ({@link captureKeyEquality} —
2643
+ * invariant 1 of {@link MS_UPDATE_KEYS_CTE}); `matchedOnly` additionally restricts the
2644
+ * read to matched capture rows via the owning side's match marker (invariant 2) — the
2645
+ * non-preserved matched read-back passes it so a null-extended capture row cannot
2646
+ * pollute the aggregate for a target row keyed NULL.
2470
2647
  */
2471
- export function capturedValueSubquery(srcAlias, owningSideIndex, owningPk, dedupAggregate, correlationAlias, captureRelationName = MS_UPDATE_KEYS_CTE) {
2472
- const conds = owningPk.map((pk, j) => ({
2473
- type: 'binary',
2474
- operator: '=',
2475
- left: { type: 'column', name: keyColumnName(owningSideIndex, j), table: 'k' },
2476
- right: correlationAlias ? { type: 'column', name: pk, table: correlationAlias } : { type: 'column', name: pk },
2477
- }));
2648
+ export function capturedValueSubquery(srcAlias, owningSideIndex, owningPk, options = {}) {
2649
+ const { dedupAggregate, correlationAlias, captureRelationName = MS_UPDATE_KEYS_CTE, matchedOnly = false } = options;
2650
+ const conds = owningPk.map((pk, j) => captureKeyEquality({ type: 'column', name: keyColumnName(owningSideIndex, j), table: 'k' }, correlationAlias ? { type: 'column', name: pk.name, table: correlationAlias } : { type: 'column', name: pk.name }, pk.nullable));
2651
+ // `matchedOnly`: restrict the read to MATCHED capture rows via the owning side's
2652
+ // match marker (the non-preserved matched read-back). Without it, a null-extended
2653
+ // capture row — key columns all NULL — would NULL-safe-match a target row whose key
2654
+ // genuinely holds NULL and pollute the aggregate with the null-extended branch's value.
2655
+ if (matchedOnly) {
2656
+ conds.push({ type: 'column', name: matchFlagName(owningSideIndex), table: 'k' });
2657
+ }
2478
2658
  const colRef = { type: 'column', name: srcAlias, table: 'k' };
2479
2659
  const projection = dedupAggregate
2480
2660
  ? { type: 'function', name: dedupAggregate, args: [colRef] }
@@ -2577,10 +2757,12 @@ function asBooleanLiteral(expr) {
2577
2757
  return undefined;
2578
2758
  }
2579
2759
  /**
2580
- * The side's primary-key column names (≥1), in declaration order — the per-side
2581
- * identifying key the capture projects and the base ops' EXISTS correlates on.
2582
- * Composite keys are admitted (each PK column contributes a `k<side>_<j>` capture
2583
- * column); a keyless table is the only reject (`unsupported-join`).
2760
+ * The side's primary-key columns (≥1), in declaration order, each carrying its
2761
+ * **declared nullability** — the per-side identifying key the capture projects and
2762
+ * the base ops' EXISTS correlates on, NULL-safely per nullable column
2763
+ * ({@link captureKeyEquality}). Composite keys are admitted (each PK column
2764
+ * contributes a `k<side>_<j>` capture column); a keyless table is the only reject
2765
+ * (`unsupported-join`).
2584
2766
  */
2585
2767
  function requireKeyColumns(view, side) {
2586
2768
  const pk = side.schema.primaryKeyDefinition;
@@ -2591,7 +2773,10 @@ function requireKeyColumns(view, side) {
2591
2773
  message: `cannot write through view '${view.name}': base table '${side.schema.name}' has no primary key; multi-source identifying predicates need a key`,
2592
2774
  });
2593
2775
  }
2594
- return pk.map(def => side.schema.columns[def.index].name);
2776
+ return pk.map(def => {
2777
+ const col = side.schema.columns[def.index];
2778
+ return { name: col.name, nullable: !col.notNull };
2779
+ });
2595
2780
  }
2596
2781
  /**
2597
2782
  * True when `fk` (declared on `child`) targets `parent` — the shared FK-match
@@ -2784,8 +2969,10 @@ function resolveColumnSide(col, sides) {
2784
2969
  * **non-partial** UNIQUE index. A **partial** unique key (one carrying a `predicate`) does
2785
2970
  * not bound the rows outside its predicate scope, so it does not prove global at-most-one
2786
2971
  * and is NOT counted. NULL semantics need no special handling — a `=` join only matches
2787
- * non-null equal values and a unique key bounds each non-null value to ≤1 row (PK columns
2788
- * are NOT NULL regardless).
2972
+ * non-null equal values and a unique key bounds each non-null value to ≤1 row. That holds
2973
+ * even for a nullable key column (key membership does not imply NOT NULL — `docs/schema.md`
2974
+ * § Primary-key nullability): key equality is NULL-self-equal, so uniqueness over the pinned
2975
+ * non-null values is if anything stronger.
2789
2976
  *
2790
2977
  * This is the inverse of the FK-correlation reasoning {@link edgeCorrelated} the delete
2791
2978
  * path uses, but **FK is not required** — the proof is purely partner-side uniqueness (the