@quereus/quereus 4.17.0 → 4.18.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 (88) hide show
  1. package/dist/src/core/database.d.ts.map +1 -1
  2. package/dist/src/core/database.js +18 -10
  3. package/dist/src/core/database.js.map +1 -1
  4. package/dist/src/func/builtins/schema.d.ts.map +1 -1
  5. package/dist/src/func/builtins/schema.js +39 -64
  6. package/dist/src/func/builtins/schema.js.map +1 -1
  7. package/dist/src/planner/analysis/constraint-extractor.d.ts +9 -1
  8. package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
  9. package/dist/src/planner/analysis/constraint-extractor.js +55 -26
  10. package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
  11. package/dist/src/planner/building/default-scope.d.ts +11 -1
  12. package/dist/src/planner/building/default-scope.d.ts.map +1 -1
  13. package/dist/src/planner/building/default-scope.js +13 -2
  14. package/dist/src/planner/building/default-scope.js.map +1 -1
  15. package/dist/src/planner/building/insert.d.ts.map +1 -1
  16. package/dist/src/planner/building/insert.js +161 -107
  17. package/dist/src/planner/building/insert.js.map +1 -1
  18. package/dist/src/planner/building/select-aggregates.d.ts +8 -0
  19. package/dist/src/planner/building/select-aggregates.d.ts.map +1 -1
  20. package/dist/src/planner/building/select-aggregates.js +30 -25
  21. package/dist/src/planner/building/select-aggregates.js.map +1 -1
  22. package/dist/src/planner/building/select.d.ts.map +1 -1
  23. package/dist/src/planner/building/select.js +15 -18
  24. package/dist/src/planner/building/select.js.map +1 -1
  25. package/dist/src/planner/cost/index.d.ts +14 -0
  26. package/dist/src/planner/cost/index.d.ts.map +1 -1
  27. package/dist/src/planner/cost/index.js +14 -0
  28. package/dist/src/planner/cost/index.js.map +1 -1
  29. package/dist/src/planner/nodes/plan-node-type.d.ts +1 -0
  30. package/dist/src/planner/nodes/plan-node-type.d.ts.map +1 -1
  31. package/dist/src/planner/nodes/plan-node-type.js +1 -0
  32. package/dist/src/planner/nodes/plan-node-type.js.map +1 -1
  33. package/dist/src/planner/nodes/scalar.d.ts +39 -0
  34. package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
  35. package/dist/src/planner/nodes/scalar.js +77 -0
  36. package/dist/src/planner/nodes/scalar.js.map +1 -1
  37. package/dist/src/planner/nodes/table-access-nodes.d.ts +21 -0
  38. package/dist/src/planner/nodes/table-access-nodes.d.ts.map +1 -1
  39. package/dist/src/planner/nodes/table-access-nodes.js +38 -0
  40. package/dist/src/planner/nodes/table-access-nodes.js.map +1 -1
  41. package/dist/src/planner/optimizer.d.ts.map +1 -1
  42. package/dist/src/planner/optimizer.js +25 -0
  43. package/dist/src/planner/optimizer.js.map +1 -1
  44. package/dist/src/planner/rules/access/rule-select-access-path.d.ts.map +1 -1
  45. package/dist/src/planner/rules/access/rule-select-access-path.js +17 -9
  46. package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
  47. package/dist/src/planner/rules/join/rule-quickpick-enumeration.d.ts.map +1 -1
  48. package/dist/src/planner/rules/join/rule-quickpick-enumeration.js +24 -4
  49. package/dist/src/planner/rules/join/rule-quickpick-enumeration.js.map +1 -1
  50. package/dist/src/planner/rules/join/rule-semi-join-pushdown.d.ts +107 -0
  51. package/dist/src/planner/rules/join/rule-semi-join-pushdown.d.ts.map +1 -0
  52. package/dist/src/planner/rules/join/rule-semi-join-pushdown.js +214 -0
  53. package/dist/src/planner/rules/join/rule-semi-join-pushdown.js.map +1 -0
  54. package/dist/src/planner/util/column-refs.d.ts +17 -0
  55. package/dist/src/planner/util/column-refs.d.ts.map +1 -0
  56. package/dist/src/planner/util/column-refs.js +30 -0
  57. package/dist/src/planner/util/column-refs.js.map +1 -0
  58. package/dist/src/runtime/emit/alter-table.d.ts +25 -5
  59. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  60. package/dist/src/runtime/emit/alter-table.js +144 -62
  61. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  62. package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
  63. package/dist/src/runtime/emit/dml-executor.js +4 -6
  64. package/dist/src/runtime/emit/dml-executor.js.map +1 -1
  65. package/dist/src/runtime/emit/insert.d.ts.map +1 -1
  66. package/dist/src/runtime/emit/insert.js +7 -0
  67. package/dist/src/runtime/emit/insert.js.map +1 -1
  68. package/dist/src/runtime/emit/update.d.ts.map +1 -1
  69. package/dist/src/runtime/emit/update.js +15 -10
  70. package/dist/src/runtime/emit/update.js.map +1 -1
  71. package/dist/src/runtime/emit/write-coercion.d.ts +7 -0
  72. package/dist/src/runtime/emit/write-coercion.d.ts.map +1 -0
  73. package/dist/src/runtime/emit/write-coercion.js +20 -0
  74. package/dist/src/runtime/emit/write-coercion.js.map +1 -0
  75. package/dist/src/runtime/register.d.ts.map +1 -1
  76. package/dist/src/runtime/register.js +2 -0
  77. package/dist/src/runtime/register.js.map +1 -1
  78. package/dist/src/runtime/scalar-fusion.d.ts.map +1 -1
  79. package/dist/src/runtime/scalar-fusion.js +4 -0
  80. package/dist/src/runtime/scalar-fusion.js.map +1 -1
  81. package/dist/src/schema/catalog.d.ts +99 -2
  82. package/dist/src/schema/catalog.d.ts.map +1 -1
  83. package/dist/src/schema/catalog.js +100 -26
  84. package/dist/src/schema/catalog.js.map +1 -1
  85. package/dist/src/schema/manager.d.ts.map +1 -1
  86. package/dist/src/schema/manager.js +10 -0
  87. package/dist/src/schema/manager.js.map +1 -1
  88. package/package.json +3 -3
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Rule: Semi-Join Pushdown — reassociate a semi join below an inner/cross join
3
+ *
4
+ * A semi join is an existential *filter* on its left input: it emits the left
5
+ * rows for which the key source holds a match, and publishes the left input's
6
+ * attributes verbatim. When its condition reads columns from only ONE side of an
7
+ * inner/cross join underneath it, the filter can be applied to that side first:
8
+ *
9
+ * Join(semi, Join(inner|cross, L, R), K, cond) cond reads L (and K) only
10
+ * → Join(inner|cross, Join(semi, L, K, cond), R)
11
+ *
12
+ * Join(semi, Join(inner|cross, L, R), K, cond) cond reads R (and K) only
13
+ * → Join(inner|cross, L, Join(semi, R, K, cond))
14
+ *
15
+ * This is the same argument `rules/predicate/rule-join-predicate-pushdown.ts`
16
+ * makes for scalar conjuncts — see its header's null-extension table. Neither
17
+ * side of an `inner` / `cross` join is ever null-extended, so every output row
18
+ * carries that side's genuine input values, and filtering the side before the
19
+ * join removes exactly the output rows filtering after it would have.
20
+ *
21
+ * Soundness, stated for the semi case: `semi(X, K, p) = { x ∈ X : ∃k ∈ K. p(x, k) }`.
22
+ * When `p` reads only the `L` part of `x`, the test is per-`L`-row and
23
+ * deterministic, so it commutes with the join — whether the join drops `L` rows
24
+ * or fans them out, filtering before removes precisely the output rows filtering
25
+ * after would have.
26
+ *
27
+ * Output attributes are unchanged in identity AND order. A semi join publishes
28
+ * its left input's attributes verbatim, so both the original and the rewritten
29
+ * shape yield `L.attrs ++ R.attrs`; every `ColumnReferenceNode` above the join
30
+ * resolves to the same attribute id it did before.
31
+ *
32
+ * WHY: `rules/access/rule-key-set-seek.ts` — which turns a semi join into
33
+ * "materialize the key set once, then seek the target index per key" — only
34
+ * fires when the filtered side peels down to a bare access leaf. A compound
35
+ * query (`… from entry e join txn t on t.id = e.txn_id where e.txn_id in
36
+ * (select …)`) puts a join there instead, so that rule declines and `entry` is
37
+ * read end-to-end. After this reassociation the filtered side is a bare leaf
38
+ * again and the existing rule fires unchanged — no new peel through `JoinNode`
39
+ * is needed (walking a join in the peel would be unsound; this rule is the
40
+ * sound way to reach the same plan).
41
+ *
42
+ * Deeper nesting works without extra machinery: the rewrite's new semi join is a
43
+ * child of the returned node, so the top-down descent visits it and
44
+ * `Join(semi, Join(inner, Join(inner, A, B), C), K)` pushes down twice.
45
+ *
46
+ * Admission gates (all must hold):
47
+ * - Anchor is a semi `JoinNode` with a condition and no `exists … as` flags.
48
+ * - `node.left` is an `inner` or `cross` `JoinNode`.
49
+ * - The key source is uncorrelated, deterministic and write-free — the same
50
+ * admission test `rule-key-set-seek`'s `admitJoin` applies, and for the same
51
+ * reason: re-rooting the semi join must not change how the source is drained.
52
+ * - The condition is functional (deterministic + read-only): the reassociation
53
+ * changes how many rows it is evaluated against.
54
+ * - Neither inner-join branch is correlated. A LATERAL right side reads the left
55
+ * side, and re-rooting either branch under a semi join must not disturb that.
56
+ * - Neither inner-join branch carries a write.
57
+ * - Every attribute id the condition needs (`collectPredicateAttributeIds`,
58
+ * which also accounts for correlated references inside a sub-query operand) is
59
+ * either a key-source attribute or an attribute of EXACTLY ONE inner-join
60
+ * branch. An id in neither branch (an outer reference, an `exists … as` flag)
61
+ * declines; ids spanning both branches decline; ids touching neither branch
62
+ * decline because there is nothing to gain.
63
+ *
64
+ * `inner.condition` / `inner.usingColumns` / `inner.existence` carry over
65
+ * verbatim to the rebuilt inner join, and `node.condition` / `node.usingColumns`
66
+ * to the pushed semi join — both rebuilds go through `withChildren`, which
67
+ * threads those fields. Existence flags on an INNER join are unreachable today
68
+ * (the parser only accepts `exists … as` on `left join`), and the attribute-id
69
+ * gate above already declines any condition that reads a flag, so no redundant
70
+ * guard is added for them.
71
+ *
72
+ * NOT IN SCOPE — deliberately:
73
+ * - NOTE: `anti` joins are declined by design, not by accident.
74
+ * `anti(L ⋈ R, K) = anti(L, K) ⋈ R` when the condition reads only `L`,
75
+ * exactly as above (the test is still per-`L`-row and deterministic), but
76
+ * `rule-key-set-seek` admits `semi` only, so there is nothing downstream to
77
+ * gain. Revisit if that rule ever admits `anti`.
78
+ * - NOTE: a LEFT join on the probe side with the semi condition reading the
79
+ * PRESERVED (left) side is also sound — `σ(L ⟕ R) = σ(L) ⟕ R` when `σ` reads
80
+ * only `L` — and is left out only to keep the first pass to the shape in the
81
+ * repro. Revisit if a compound outer-join shape shows up wanting the seek.
82
+ * The MIRROR case is NOT sound and must stay declined permanently: a
83
+ * condition reading the NULL-EXTENDED side (the right side of a `left join`)
84
+ * does not commute, because filtering `R` before the join re-pads the left row
85
+ * with NULLs instead of dropping it.
86
+ * `backlog/feat-outer-join-to-inner-on-null-rejecting-filter`, if it lands,
87
+ * converts some outer joins to inner ones and this rule then covers them.
88
+ * - NOTE: the anchor's left input must be the `JoinNode` ITSELF — no wrapper is
89
+ * peeled. `select … from (select … from a join b …) x where x.k in (select …)`
90
+ * and a residual `Filter` that survived predicate pushdown (one spanning both
91
+ * branches, e.g. `… and e.amount > t.id`) both leave a node in between and
92
+ * decline. Revisit if a wrapped compound shape shows up wanting the seek; the
93
+ * peel would have to re-wrap whatever it walked through, which is why the
94
+ * first pass does not attempt it.
95
+ *
96
+ * NOTE: pushing leaves a semi join as a LEAF of the enclosing inner spine, and
97
+ * `rule-quickpick-enumeration` abandons any join graph containing a non-inner
98
+ * join — so a 3+-relation join under an `IN (SELECT …)` filter now gets no join
99
+ * enumeration where it used to. Seek beats enumeration on the shapes this rule
100
+ * targets; revisit (by teaching QuickPick to admit a non-inner join as one opaque
101
+ * leaf) if such a query shows up with a bad join order. See the matching `NOTE:`
102
+ * in `rule-quickpick-enumeration.ts`.
103
+ */
104
+ import { createLogger } from '../../../common/logger.js';
105
+ import { JoinNode } from '../../nodes/join-node.js';
106
+ import { PlanNodeCharacteristics } from '../../framework/characteristics.js';
107
+ import { collectPredicateAttributeIds } from '../../analysis/predicate-dependencies.js';
108
+ import { isCorrelatedSubquery } from '../../cache/correlation-detector.js';
109
+ const log = createLogger('optimizer:rule:semi-join-pushdown');
110
+ const attributeIds = (node) => new Set(node.getAttributes().map(a => a.id));
111
+ /**
112
+ * The single inner-join branch `cond` reads, or null when it spans both, escapes
113
+ * both (an outer reference or an existence flag), or reads neither.
114
+ */
115
+ function conditionSide(cond, keyIds, leftIds, rightIds) {
116
+ let needsLeft = false;
117
+ let needsRight = false;
118
+ for (const id of collectPredicateAttributeIds(cond)) {
119
+ if (keyIds.has(id))
120
+ continue;
121
+ if (leftIds.has(id)) {
122
+ needsLeft = true;
123
+ continue;
124
+ }
125
+ if (rightIds.has(id)) {
126
+ needsRight = true;
127
+ continue;
128
+ }
129
+ // An id belonging to neither branch nor the key source: an outer
130
+ // reference, or an `exists … as` flag the inner join appends. Pushing
131
+ // below either branch would strand it.
132
+ return null;
133
+ }
134
+ // Equal means either "spans both branches" (unsound to push to one) or
135
+ // "reads neither" (nothing to gain — a condition over constants alone).
136
+ if (needsLeft === needsRight)
137
+ return null;
138
+ return needsLeft ? 'left' : 'right';
139
+ }
140
+ export function ruleSemiJoinPushdown(node, _context) {
141
+ if (!(node instanceof JoinNode))
142
+ return null;
143
+ if (node.joinType !== 'semi')
144
+ return null;
145
+ if (!node.condition)
146
+ return null;
147
+ // An `exists … as` flag on the anchor would be published above it, but the
148
+ // pushed semi join publishes only its own left input's attributes.
149
+ if (node.hasExistenceColumns)
150
+ return null;
151
+ const inner = node.left;
152
+ if (!(inner instanceof JoinNode))
153
+ return null;
154
+ if (inner.joinType !== 'inner' && inner.joinType !== 'cross')
155
+ return null;
156
+ // NOTE: unconditional — no cost gate, matching `rule-join-predicate-pushdown`'s
157
+ // treatment of scalar conjuncts. At Structural time the row estimates are not
158
+ // usable anyway (most table-backed inputs report `Infinity`; see the NOTE in
159
+ // `rule-join-greedy-commute`). The one shape where pushdown does extra work is a
160
+ // strongly *filtering* inner join, where the semi join probes |L| rows instead of
161
+ // the smaller |L ⋈ R| — offset by the join then receiving a smaller left input.
162
+ // Revisit condition: if a filtering-join shape ever shows up regressed in
163
+ // `yarn bench:gate`, gate this on estimated rows.
164
+ const keySource = node.right;
165
+ // The key source is drained once by the semi join wherever it sits; a
166
+ // correlated / non-deterministic / write-bearing source must keep its
167
+ // per-execution semantics. Same admission test as `rule-key-set-seek`.
168
+ if (isCorrelatedSubquery(keySource)) {
169
+ log('decline: key source is correlated');
170
+ return null;
171
+ }
172
+ if (!PlanNodeCharacteristics.isDeterministic(keySource)) {
173
+ log('decline: key source is non-deterministic');
174
+ return null;
175
+ }
176
+ if (PlanNodeCharacteristics.subtreeHasSideEffects(keySource)) {
177
+ log('decline: key source has side effects');
178
+ return null;
179
+ }
180
+ // Both branches are re-rooted (one under the semi join, one under the rebuilt
181
+ // inner join) — refuse to touch a subtree carrying a write.
182
+ if (PlanNodeCharacteristics.subtreeHasSideEffects(inner.left)
183
+ || PlanNodeCharacteristics.subtreeHasSideEffects(inner.right)) {
184
+ log('decline: an inner-join branch has side effects');
185
+ return null;
186
+ }
187
+ // A LATERAL right side reads the left side; re-rooting either branch must not
188
+ // disturb the row context it reads from.
189
+ if (isCorrelatedSubquery(inner.left) || isCorrelatedSubquery(inner.right)) {
190
+ log('decline: an inner-join branch is correlated');
191
+ return null;
192
+ }
193
+ // The condition is evaluated against a different number of rows after the
194
+ // move, so a non-deterministic or write-bearing one must stay put.
195
+ if (!PlanNodeCharacteristics.isFunctional(node.condition)) {
196
+ log('decline: semi-join condition is not functional');
197
+ return null;
198
+ }
199
+ const side = conditionSide(node.condition, attributeIds(keySource), attributeIds(inner.left), attributeIds(inner.right));
200
+ if (side === null)
201
+ return null;
202
+ const target = side === 'left' ? inner.left : inner.right;
203
+ // `withChildren` (not `new JoinNode(...)`) so `usingColumns` / `existence`
204
+ // thread through both rebuilds.
205
+ const pushedSemi = node.withChildren([target, keySource, node.condition]);
206
+ const innerChildren = side === 'left'
207
+ ? [pushedSemi, inner.right]
208
+ : [inner.left, pushedSemi];
209
+ if (inner.condition)
210
+ innerChildren.push(inner.condition);
211
+ log('Pushed semi join below %s join onto its %s branch', inner.joinType, side);
212
+ return inner.withChildren(innerChildren);
213
+ }
214
+ //# sourceMappingURL=rule-semi-join-pushdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-semi-join-pushdown.js","sourceRoot":"","sources":["../../../../../src/planner/rules/join/rule-semi-join-pushdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Column-reference walks over scalar plan subtrees.
3
+ *
4
+ * Lives here rather than in `analysis/constraint-extractor.ts` so that
5
+ * `planner/nodes/` can import it as a *value*: the runtime cycle
6
+ * `constraint-extractor → nodes/reference → …` is real, and this module
7
+ * deliberately depends on nothing but the node-type enum.
8
+ */
9
+ import type { ScalarPlanNode } from '../nodes/plan-node.js';
10
+ /**
11
+ * Collect the attributeIds of every ColumnReference in a scalar subtree.
12
+ * Walking into children (rather than only unwrapping a top-level Cast) reaches
13
+ * references nested inside arithmetic, function calls, casts, etc. — e.g.
14
+ * `outer.id + 1`, `coalesce(outer.id, 0)`, `cast(outer.id + 1 as integer)`.
15
+ */
16
+ export declare function collectColumnRefAttributeIds(node: ScalarPlanNode): number[];
17
+ //# sourceMappingURL=column-refs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"column-refs.d.ts","sourceRoot":"","sources":["../../../../src/planner/util/column-refs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,EAAE,CAa3E"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Column-reference walks over scalar plan subtrees.
3
+ *
4
+ * Lives here rather than in `analysis/constraint-extractor.ts` so that
5
+ * `planner/nodes/` can import it as a *value*: the runtime cycle
6
+ * `constraint-extractor → nodes/reference → …` is real, and this module
7
+ * deliberately depends on nothing but the node-type enum.
8
+ */
9
+ import { PlanNodeType } from '../nodes/plan-node-type.js';
10
+ /**
11
+ * Collect the attributeIds of every ColumnReference in a scalar subtree.
12
+ * Walking into children (rather than only unwrapping a top-level Cast) reaches
13
+ * references nested inside arithmetic, function calls, casts, etc. — e.g.
14
+ * `outer.id + 1`, `coalesce(outer.id, 0)`, `cast(outer.id + 1 as integer)`.
15
+ */
16
+ export function collectColumnRefAttributeIds(node) {
17
+ const ids = [];
18
+ const stack = [node];
19
+ while (stack.length) {
20
+ const n = stack.pop();
21
+ if (n.nodeType === PlanNodeType.ColumnReference) {
22
+ ids.push(n.attributeId);
23
+ }
24
+ for (const c of n.getChildren()) {
25
+ stack.push(c);
26
+ }
27
+ }
28
+ return ids;
29
+ }
30
+ //# sourceMappingURL=column-refs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"column-refs.js","sourceRoot":"","sources":["../../../../src/planner/util/column-refs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAI1D;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAAoB;IAChE,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,KAAK,GAAqB,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QACvB,IAAI,CAAC,CAAC,QAAQ,KAAK,YAAY,CAAC,eAAe,EAAE,CAAC;YACjD,GAAG,CAAC,IAAI,CAAE,CAAoC,CAAC,WAAW,CAAC,CAAC;QAC7D,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACjC,KAAK,CAAC,IAAI,CAAC,CAA8B,CAAC,CAAC;QAC5C,CAAC;IACF,CAAC;IACD,OAAO,GAAG,CAAC;AACZ,CAAC"}
@@ -6,10 +6,30 @@ export declare function emitAlterTable(plan: AlterTableNode, ctx: EmissionContex
6
6
  /**
7
7
  * Build the shadow-table CREATE TABLE DDL used by the non-memory rebuild path.
8
8
  *
9
- * Nullability is emitted explicitly for every column, matching the "no-db"
10
- * stance of `generateTableDDL` in ddl-generator.ts: safe under any session's
11
- * `default_column_nullability` setting. DEFAULT and COLLATE are preserved so
12
- * the shadow table faithfully mirrors the original schema.
9
+ * Rendered through the canonical emitter (`generateTableDDL`) over a copy of the
10
+ * REAL `TableSchema` with only the name and key substituted, so the shadow
11
+ * carries everything the table declares — constraints, foreign keys, tags,
12
+ * defaults, collations, generated columns, the key's ON CONFLICT action — not a
13
+ * hand-picked subset. An omission in emitted DDL is never neutral: a missing
14
+ * PRIMARY KEY clause re-parses as a different key (all-columns), and a missing
15
+ * constraint re-parses as no constraint.
16
+ *
17
+ * Called WITHOUT a `db` argument: that form fully qualifies the name, always
18
+ * emits USING, and annotates nullability on every column, so the text re-parses
19
+ * to the same shape under any session's `default_column_nullability` /
20
+ * `default_vtab_module` settings. An empty `newPkDef` emits `PRIMARY KEY ()` —
21
+ * the singleton key — rather than omitting the clause (which would re-parse as
22
+ * the opposite thing: keyed by every column).
23
+ *
24
+ * The copied schema's per-column `primaryKey` flags are stale (they still mirror
25
+ * the OLD key) — harmless: the emitter reads only `primaryKeyDefinition`, and the
26
+ * shadow's real schema is built by re-parsing the emitted text.
27
+ *
28
+ * Deliberately WHOLE-TABLE only (no column subset parameter): stored constraints
29
+ * address columns by index, so a subset copy would need every constraint and
30
+ * index remapped or dropped. `ALTER TABLE … DROP COLUMN` goes through
31
+ * `module.alterTable`, never through this rebuild; if a column-dropping rebuild
32
+ * is ever wanted it must come back with deliberate constraint handling.
13
33
  */
14
- export declare function buildShadowTableDdl(tableSchema: TableSchema, shadowName: string, survivingColumns: string[], newPkDef: PrimaryKeyColumnDefinition[]): string;
34
+ export declare function buildShadowTableDdl(tableSchema: TableSchema, shadowName: string, newPkDef: PrimaryKeyColumnDefinition[]): string;
15
35
  //# sourceMappingURL=alter-table.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"alter-table.d.ts","sourceRoot":"","sources":["../../../../src/runtime/emit/alter-table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAqC,MAAM,yCAAyC,CAAC;AACjH,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,aAAa,CAAC;AAE/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAM9D,OAAO,KAAK,EAAE,WAAW,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAkDrF,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,WAAW,CAkJtF;AA2xDD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAClC,WAAW,EAAE,WAAW,EACxB,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,MAAM,EAAE,EAC1B,QAAQ,EAAE,0BAA0B,EAAE,GACpC,MAAM,CAuCR"}
1
+ {"version":3,"file":"alter-table.d.ts","sourceRoot":"","sources":["../../../../src/runtime/emit/alter-table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAqC,MAAM,yCAAyC,CAAC;AACjH,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,aAAa,CAAC;AAE/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAM9D,OAAO,KAAK,EAAE,WAAW,EAAE,0BAA0B,EAAe,MAAM,uBAAuB,CAAC;AAmDlG,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,WAAW,CAkJtF;AAyyDD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,mBAAmB,CAClC,WAAW,EAAE,WAAW,EACxB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,0BAA0B,EAAE,GACpC,MAAM,CAMR"}
@@ -10,7 +10,8 @@ import { quoteIdentifier, expressionToString, astToString } from '../../emit/ast
10
10
  import { renameTableInAst, renameColumnInAst, renameColumnInCheckExpression, renameColumnInColumnExpressions, renameTableInCheckConstraints, renameTableInIndexPredicates, renameTableInColumnExpressions, objectRefKey } from '../../schema/rename-rewriter.js';
11
11
  import { snapshotObjectRefResolvers, tableRenameTargetsFor } from '../../schema/object-ref-resolver.js';
12
12
  import { assertCatalogObjectPersistable, assertRenameDependentsPersistable } from '../../schema/catalog-persistability.js';
13
- import { assertUniqueConstraintIndexNameFree, assertUniqueConstraintNotDuplicated, assertUniqueConstraintBackingNamesDistinct, uniqueConstraintColumnSetKey } from '../../schema/catalog.js';
13
+ import { assertUniqueConstraintIndexNameFree, assertUniqueConstraintNotDuplicated, assertUniqueConstraintBackingNamesDistinct, uniqueConstraintColumnSetKey, isImplicitCoveringIndex } from '../../schema/catalog.js';
14
+ import { generateTableDDL, generateIndexDDL } from '../../schema/ddl-generator.js';
14
15
  import { isTruthy } from '../../util/comparison.js';
15
16
  import { assertDdlTransactionPolicy, isExplicitTransactionOpen } from './ddl-transaction-policy.js';
16
17
  import { buildColumnSourceResolver } from '../../schema/column-source-resolver.js';
@@ -1553,6 +1554,18 @@ async function runAlterPrimaryKey(rctx, tableSchema, schema, columns, sql) {
1553
1554
  + `this one — without 'renameTable' the module would keep the rows under the shadow name and `
1554
1555
  + `the rebuilt table could not be opened.`, StatusCode.UNSUPPORTED);
1555
1556
  }
1557
+ // Second capability check: the rebuild re-creates the table's user indexes with
1558
+ // CREATE INDEX statements (the shadow's CREATE TABLE carries every constraint, but
1559
+ // indexes have their own statements), and CREATE INDEX routes through
1560
+ // `module.createIndex`. Refuse up front — failing after the original table is
1561
+ // dropped would strand a half-rebuilt table, and silently dropping the indexes is
1562
+ // data loss of a different kind (a UNIQUE index stops enforcing).
1563
+ if (!module.createIndex && rebuildUserIndexes(tableSchema).length > 0) {
1564
+ throw new QuereusError(`Module '${tableSchema.vtabModuleName ?? '<unknown>'}' does not support ALTER PRIMARY KEY on table `
1565
+ + `'${tableSchema.name}': it cannot re-key in place, and the engine's fallback rebuild must `
1566
+ + `re-create the table's indexes on the rebuilt table — without 'createIndex' they would be `
1567
+ + `silently lost. Drop the indexes first, or use a module that can re-key or create indexes.`, StatusCode.UNSUPPORTED);
1568
+ }
1556
1569
  // NOTE: refused on every `DdlTransactionality` tier, including a module declaring
1557
1570
  // 'transactional'. None does today; if one appears its DROP + RENAME would roll back
1558
1571
  // together with the row copy, making this refusal over-broad — exempt that tier then, the
@@ -1564,7 +1577,7 @@ async function runAlterPrimaryKey(rctx, tableSchema, schema, columns, sql) {
1564
1577
  + `copy does not — a rollback would leave an empty table and destroy rows committed before this `
1565
1578
  + `transaction began. COMMIT or ROLLBACK first, then re-issue in autocommit mode.`, StatusCode.ERROR);
1566
1579
  }
1567
- await rebuildTableWithNewShape(rctx, tableSchema, schema, tableSchema.columns.map(c => c.name), newPkDef);
1580
+ await rebuildTableWithNewShape(rctx, tableSchema, schema, newPkDef);
1568
1581
  // A DEFENSIVE NO-OP on this path today, not the working re-key the native arm above does.
1569
1582
  // Nothing can be in the batch for this table by now: the rebuild's own events are
1570
1583
  // suppressed (see `rebuildViaShadowTable`), the ALTER statement writes no rows of its own,
@@ -1572,8 +1585,8 @@ async function runAlterPrimaryKey(rctx, tableSchema, schema, columns, sql) {
1572
1585
  // Kept anyway — it costs one walk of an empty batch, and it is the correct call the moment
1573
1586
  // that transaction guard is loosened; deleting it would quietly make this arm's
1574
1587
  // as-of-delivery `key` guarantee depend on the guard staying exactly as it is. Every column
1575
- // survives the rebuild in order (`survivingColumns` above is the full list), so the indices
1576
- // would still line up with the event images.
1588
+ // survives the rebuild in order (the rebuild is whole-table by construction — see
1589
+ // `buildShadowTableDdl`), so the indices would still line up with the event images.
1577
1590
  rctx.db._getEventEmitter().rekeyBatchedDataEvents(tableSchema.schemaName, tableSchema.name, oldPkIndices, newPkIndices);
1578
1591
  // The rebuild's own four statements are silent (`rebuildViaShadowTable` runs them inside
1579
1592
  // `withPublicEventsSuppressed`, which also swallows the inner RENAME TO arm's event). This
@@ -1596,17 +1609,17 @@ async function runAlterPrimaryKey(rctx, tableSchema, schema, columns, sql) {
1596
1609
  return null;
1597
1610
  }
1598
1611
  /**
1599
- * Rebuilds a table with a new column projection and/or primary key, via the
1600
- * shadow-table SQL approach with DROP+RENAME. This is the fallback for a module
1601
- * whose `alterTable` throws `UNSUPPORTED` for `alterPrimaryKey` (or omits the
1602
- * hook); the built-in memory and store modules both re-key in place and never
1603
- * reach it. See `rebuildViaShadowTable` for the two preconditions
1604
- * `runAlterPrimaryKey` checks before calling this.
1612
+ * Rebuilds a table with a new primary key, via the shadow-table SQL approach
1613
+ * with DROP+RENAME. This is the fallback for a module whose `alterTable` throws
1614
+ * `UNSUPPORTED` for `alterPrimaryKey` (or omits the hook); the built-in memory
1615
+ * and store modules both re-key in place and never reach it. See
1616
+ * `rebuildViaShadowTable` for the preconditions `runAlterPrimaryKey` checks
1617
+ * before calling this.
1605
1618
  */
1606
- async function rebuildTableWithNewShape(rctx, tableSchema, schema, survivingColumns, newPkDef) {
1619
+ async function rebuildTableWithNewShape(rctx, tableSchema, schema, newPkDef) {
1607
1620
  const tableName = tableSchema.name;
1608
1621
  const schemaName = tableSchema.schemaName;
1609
- await rebuildViaShadowTable(rctx, tableSchema, schema, survivingColumns, newPkDef);
1622
+ await rebuildViaShadowTable(rctx, tableSchema, newPkDef);
1610
1623
  const finalSchema = schema.getTable(tableName);
1611
1624
  if (finalSchema) {
1612
1625
  rctx.db.schemaManager.getChangeNotifier().notifyChange({
@@ -1621,49 +1634,54 @@ async function rebuildTableWithNewShape(rctx, tableSchema, schema, survivingColu
1621
1634
  /**
1622
1635
  * Build the shadow-table CREATE TABLE DDL used by the non-memory rebuild path.
1623
1636
  *
1624
- * Nullability is emitted explicitly for every column, matching the "no-db"
1625
- * stance of `generateTableDDL` in ddl-generator.ts: safe under any session's
1626
- * `default_column_nullability` setting. DEFAULT and COLLATE are preserved so
1627
- * the shadow table faithfully mirrors the original schema.
1637
+ * Rendered through the canonical emitter (`generateTableDDL`) over a copy of the
1638
+ * REAL `TableSchema` with only the name and key substituted, so the shadow
1639
+ * carries everything the table declares — constraints, foreign keys, tags,
1640
+ * defaults, collations, generated columns, the key's ON CONFLICT action — not a
1641
+ * hand-picked subset. An omission in emitted DDL is never neutral: a missing
1642
+ * PRIMARY KEY clause re-parses as a different key (all-columns), and a missing
1643
+ * constraint re-parses as no constraint.
1644
+ *
1645
+ * Called WITHOUT a `db` argument: that form fully qualifies the name, always
1646
+ * emits USING, and annotates nullability on every column, so the text re-parses
1647
+ * to the same shape under any session's `default_column_nullability` /
1648
+ * `default_vtab_module` settings. An empty `newPkDef` emits `PRIMARY KEY ()` —
1649
+ * the singleton key — rather than omitting the clause (which would re-parse as
1650
+ * the opposite thing: keyed by every column).
1651
+ *
1652
+ * The copied schema's per-column `primaryKey` flags are stale (they still mirror
1653
+ * the OLD key) — harmless: the emitter reads only `primaryKeyDefinition`, and the
1654
+ * shadow's real schema is built by re-parsing the emitted text.
1655
+ *
1656
+ * Deliberately WHOLE-TABLE only (no column subset parameter): stored constraints
1657
+ * address columns by index, so a subset copy would need every constraint and
1658
+ * index remapped or dropped. `ALTER TABLE … DROP COLUMN` goes through
1659
+ * `module.alterTable`, never through this rebuild; if a column-dropping rebuild
1660
+ * is ever wanted it must come back with deliberate constraint handling.
1628
1661
  */
1629
- export function buildShadowTableDdl(tableSchema, shadowName, survivingColumns, newPkDef) {
1630
- const colDefs = [];
1631
- for (const colName of survivingColumns) {
1632
- const idx = tableSchema.columnIndexMap.get(colName.toLowerCase());
1633
- if (idx === undefined)
1634
- continue;
1635
- const col = tableSchema.columns[idx];
1636
- let def = quoteIdentifier(col.name) + ' ' + col.logicalType.name;
1637
- def += col.notNull ? ' not null' : ' null';
1638
- if (col.collation && col.collation !== 'BINARY')
1639
- def += ` collate ${col.collation}`;
1640
- if (col.defaultValue !== null && col.defaultValue !== undefined) {
1641
- def += ` default ${expressionToString(col.defaultValue)}`;
1642
- }
1643
- colDefs.push(def);
1644
- }
1645
- const pkColNames = [];
1646
- for (const pk of newPkDef) {
1647
- const colName = tableSchema.columns[pk.index].name;
1648
- let entry = quoteIdentifier(colName);
1649
- if (pk.desc)
1650
- entry += ' desc';
1651
- pkColNames.push(entry);
1652
- }
1653
- let createDdl = `create table ${qualifyTableName(tableSchema.schemaName, shadowName)} (${colDefs.join(', ')}`;
1654
- createDdl += pkColNames.length > 0
1655
- ? `, primary key (${pkColNames.join(', ')}))`
1656
- : `)`;
1657
- if (tableSchema.vtabModuleName) {
1658
- createDdl += ` using ${tableSchema.vtabModuleName}`;
1659
- if (tableSchema.vtabArgs && Object.keys(tableSchema.vtabArgs).length > 0) {
1660
- const args = Object.entries(tableSchema.vtabArgs)
1661
- .map(([key, value]) => `${key} = ${JSON.stringify(value)}`)
1662
- .join(', ');
1663
- createDdl += ` (${args})`;
1664
- }
1665
- }
1666
- return createDdl;
1662
+ export function buildShadowTableDdl(tableSchema, shadowName, newPkDef) {
1663
+ return generateTableDDL({
1664
+ ...tableSchema,
1665
+ name: shadowName,
1666
+ primaryKeyDefinition: newPkDef,
1667
+ });
1668
+ }
1669
+ /**
1670
+ * The table's user indexes — the ones the rebuild must re-create with explicit
1671
+ * CREATE INDEX statements. Implicit covering structures (the auto-built backing
1672
+ * of a declared UNIQUE constraint) are excluded: the shadow's CREATE TABLE
1673
+ * carries the constraint itself, which re-materializes its backing structure.
1674
+ * UNIQUE constraints synthesized FROM an index (`derivedFromIndex`) are the
1675
+ * opposite case — `generateTableDDL` skips them, and the `CREATE UNIQUE INDEX`
1676
+ * emitted for their index re-synthesizes them (see `appendIndexToTableSchema`).
1677
+ *
1678
+ * NOTE: an *exposed* implicit index's user tags (`exposedIndexTags` in store
1679
+ * mode, materialized `IndexSchema.tags` in memory mode) are not carried by
1680
+ * either channel through a rebuild; if a rebuild-path backend ever exposes
1681
+ * implicit indexes with tags, they need explicit re-application here.
1682
+ */
1683
+ function rebuildUserIndexes(tableSchema) {
1684
+ return (tableSchema.indexes ?? []).filter(idx => !isImplicitCoveringIndex(tableSchema, idx.name));
1667
1685
  }
1668
1686
  /**
1669
1687
  * SET MAINTAINED [(cols)] AS <body> — attach a derivation to a plain table, or
@@ -1728,13 +1746,16 @@ async function runDropMaintained(rctx, tableSchema, schema) {
1728
1746
  * Generic rebuild via shadow table SQL, for a module without a native
1729
1747
  * `alterPrimaryKey`.
1730
1748
  *
1731
- * Two preconditions the caller (`runAlterPrimaryKey`) enforces, because this rebuild has no
1749
+ * Preconditions the caller (`runAlterPrimaryKey`) enforces, because this rebuild has no
1732
1750
  * correct outcome without them:
1733
1751
  *
1734
1752
  * - **The module must implement `renameTable`.** The last step renames the shadow table over
1735
1753
  * the original; a module that files its rows under the table's name and never hears about
1736
1754
  * the rename keeps them under the shadow name while the catalog says otherwise, and the
1737
1755
  * rebuilt table cannot be connected at all.
1756
+ * - **The module must implement `createIndex` when the table has user indexes.** The rebuild
1757
+ * finishes by re-creating them on the rebuilt table; without the hook they would be
1758
+ * silently lost.
1738
1759
  * - **No explicit transaction may be open.** The two halves have different transactional
1739
1760
  * lifetimes: the schema half (DROP + RENAME) escapes ROLLBACK on every
1740
1761
  * `DdlTransactionality` tier a built-in module reaches, while the row copy is staged in the
@@ -1744,17 +1765,23 @@ async function runDropMaintained(rctx, tableSchema, schema) {
1744
1765
  * offers; making the data half survive would commit part of the user's transaction behind
1745
1766
  * their back.
1746
1767
  *
1747
- * The four statements are engine scaffolding, not statements the application issued, so the
1768
+ * The statements are engine scaffolding, not statements the application issued, so the
1748
1769
  * whole rebuild runs with the PUBLIC event channels suppressed — see the scope inside.
1749
1770
  */
1750
- async function rebuildViaShadowTable(rctx, tableSchema, _schema, survivingColumns, newPkDef) {
1771
+ async function rebuildViaShadowTable(rctx, tableSchema, newPkDef) {
1751
1772
  const tableName = tableSchema.name;
1752
1773
  const schemaName = tableSchema.schemaName;
1753
1774
  const shadowName = `${tableName}__rekey_${Date.now()}`;
1754
1775
  const qualifiedShadow = qualifyTableName(schemaName, shadowName);
1755
1776
  const qualifiedTable = qualifyTableName(schemaName, tableName);
1756
- const createDdl = buildShadowTableDdl(tableSchema, shadowName, survivingColumns, newPkDef);
1757
- const projection = survivingColumns.map(c => quoteIdentifier(c)).join(', ');
1777
+ const createDdl = buildShadowTableDdl(tableSchema, shadowName, newPkDef);
1778
+ // Whole-table copy (see buildShadowTableDdl for why no subset is representable).
1779
+ // Generated columns are excluded from the projection: the shadow declares the same
1780
+ // GENERATED ALWAYS AS clauses, so it computes them itself and rejects direct writes.
1781
+ const projection = tableSchema.columns
1782
+ .filter(c => !c.generated)
1783
+ .map(c => quoteIdentifier(c.name))
1784
+ .join(', ');
1758
1785
  // The rebuild's statements are ordinary SQL, so without this scope they would raise
1759
1786
  // ordinary notifications describing the scaffolding rather than what the user asked for:
1760
1787
  // the copy announces every existing row as a fresh `insert` (relabelled onto the real
@@ -1777,15 +1804,70 @@ async function rebuildViaShadowTable(rctx, tableSchema, _schema, survivingColumn
1777
1804
  await rctx.db._getEventEmitter().withPublicEventsSuppressed(async () => {
1778
1805
  try {
1779
1806
  await rctx.db._execWithinTransaction(createDdl);
1780
- await rctx.db._execWithinTransaction(`insert into ${qualifiedShadow} (${projection}) select ${projection} from ${qualifiedTable}`);
1781
- await rctx.db._execWithinTransaction(`drop table ${qualifiedTable}`);
1807
+ // `or abort` is load-bearing, not decoration. The shadow now declares the table's
1808
+ // own conflict actions (the whole point of rendering it through the canonical
1809
+ // writer), so a row-set that COLLIDES under the new key — the case the in-place
1810
+ // re-key path refuses with CONSTRAINT — would otherwise be resolved by the
1811
+ // declared action instead of reported: `primary key (a) on conflict replace`
1812
+ // re-keyed onto a non-unique column silently keeps the last row and reports
1813
+ // success. The copy is engine scaffolding, not a user INSERT, so it takes the
1814
+ // statement-level clause that outranks every declared default
1815
+ // (see row-constraints.ts: statement OR clause > per-constraint default > ABORT)
1816
+ // and surfaces the collision the way every other re-key path does.
1817
+ await rctx.db._execWithinTransaction(`insert or abort into ${qualifiedShadow} (${projection}) select ${projection} from ${qualifiedTable}`);
1818
+ // The DROP is scaffolding: the shadow's rows ARE the table's rows, moments from
1819
+ // being renamed back over it. Two FK situations would otherwise refuse it:
1820
+ // - a SELF-referencing FK — the shadow's copy references the ORIGINAL by name
1821
+ // (correct for the copy: the per-row EXISTS checks run against the intact
1822
+ // original), so the drop guard sees the shadow as a referencing child;
1823
+ // - another table's FK into this one with rows present — the referencing rows
1824
+ // stay satisfied because the rename restores the name with identical rows,
1825
+ // matching the in-place re-key path (which never breaks such children).
1826
+ // Suppress the parent-side RESTRICT guard for exactly this statement; restored
1827
+ // in the finally so a failing drop cannot latch it.
1828
+ //
1829
+ // NOTE: two OTHER drop-refusal shapes are not handled here, and both are
1830
+ // reachable rather than hypothetical (verified against a stub module with no
1831
+ // `alterTable`) — see backlog `bug-rebuild-drop-refused-by-user-facing-guards`.
1832
+ // `emitDropTable` also runs `assertNoAssertionDependsOn` and
1833
+ // `assertNoExpressionDependsOn`, so an assertion over this table — or ANOTHER
1834
+ // table's CHECK / DEFAULT / generated body whose subquery names it — refuses the
1835
+ // internal DROP, and the whole ALTER fails with `cannot drop table '<name>'`,
1836
+ // naming a drop the user never asked for. The rebuild unwinds cleanly (the shadow
1837
+ // is dropped in the catch below, the table is untouched), so this costs a
1838
+ // confusing refusal, not data. The fix is a scaffolding-drop scope those guards
1839
+ // honor too, not a wider FK flag.
1840
+ const priorSuppress = rctx.db._setFkRestrictSuppressed(true);
1841
+ try {
1842
+ await rctx.db._execWithinTransaction(`drop table ${qualifiedTable}`);
1843
+ }
1844
+ finally {
1845
+ rctx.db._setFkRestrictSuppressed(priorSuppress);
1846
+ }
1847
+ // The rename's propagate pass rewrites the shadow's dangling references to the
1848
+ // dropped original (e.g. a self-referencing FK) onto the restored name.
1782
1849
  await rctx.db._execWithinTransaction(`alter table ${qualifiedShadow} rename to ${quoteIdentifier(tableName)}`);
1850
+ // Re-create the table's user indexes on the rebuilt table (see
1851
+ // rebuildUserIndexes for what is included and why). AFTER the rename so the
1852
+ // index names — unique per schema — never coexist with the original's, and
1853
+ // rendered from the ORIGINAL schema, whose name/schema/column order the rebuilt
1854
+ // table shares. `runAlterPrimaryKey` refused up front if the module cannot
1855
+ // createIndex, so a failure here is exceptional; it propagates rather than
1856
+ // silently dropping the index.
1857
+ for (const idx of rebuildUserIndexes(tableSchema)) {
1858
+ await rctx.db._execWithinTransaction(generateIndexDDL(idx, tableSchema));
1859
+ }
1783
1860
  }
1784
1861
  catch (e) {
1785
1862
  try {
1786
1863
  await rctx.db._execWithinTransaction(`drop table if exists ${qualifiedShadow}`);
1787
1864
  }
1788
- catch { /* ignore */ }
1865
+ catch (cleanupError) {
1866
+ // Never masks the original failure (rethrown just below), but a shadow table the
1867
+ // engine could not clean up outlives the statement under a machine-generated name
1868
+ // — say so rather than dropping it on the floor.
1869
+ warnLog('failed to drop leftover shadow table %s after a failed rebuild: %s', qualifiedShadow, cleanupError instanceof Error ? cleanupError.message : String(cleanupError));
1870
+ }
1789
1871
  throw e;
1790
1872
  }
1791
1873
  });