@minnowdb/core 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -1
  3. package/dist/block-format/block.d.ts +46 -0
  4. package/dist/block-format/block.d.ts.map +1 -0
  5. package/dist/block-format/block.js +244 -0
  6. package/dist/block-format/block.js.map +1 -0
  7. package/dist/block-format/checksum.d.ts +7 -0
  8. package/dist/block-format/checksum.d.ts.map +1 -0
  9. package/dist/block-format/checksum.js +56 -0
  10. package/dist/block-format/checksum.js.map +1 -0
  11. package/dist/block-format/codecs.d.ts +24 -0
  12. package/dist/block-format/codecs.d.ts.map +1 -0
  13. package/dist/block-format/codecs.js +102 -0
  14. package/dist/block-format/codecs.js.map +1 -0
  15. package/dist/block-format/column.d.ts +8 -0
  16. package/dist/block-format/column.d.ts.map +1 -0
  17. package/dist/block-format/column.js +166 -0
  18. package/dist/block-format/column.js.map +1 -0
  19. package/dist/block-format/index.d.ts +7 -0
  20. package/dist/block-format/index.d.ts.map +1 -0
  21. package/dist/block-format/index.js +7 -0
  22. package/dist/block-format/index.js.map +1 -0
  23. package/dist/block-format/physical.d.ts +20 -0
  24. package/dist/block-format/physical.d.ts.map +1 -0
  25. package/dist/block-format/physical.js +439 -0
  26. package/dist/block-format/physical.js.map +1 -0
  27. package/dist/block-format/types.d.ts +93 -0
  28. package/dist/block-format/types.d.ts.map +1 -0
  29. package/dist/block-format/types.js +2 -0
  30. package/dist/block-format/types.js.map +1 -0
  31. package/dist/engine/artifact-cache.d.ts +22 -0
  32. package/dist/engine/artifact-cache.d.ts.map +1 -0
  33. package/dist/engine/artifact-cache.js +68 -0
  34. package/dist/engine/artifact-cache.js.map +1 -0
  35. package/dist/engine/batch.d.ts +28 -0
  36. package/dist/engine/batch.d.ts.map +1 -0
  37. package/dist/engine/batch.js +38 -0
  38. package/dist/engine/batch.js.map +1 -0
  39. package/dist/engine/buffered-writer.d.ts +43 -0
  40. package/dist/engine/buffered-writer.d.ts.map +1 -0
  41. package/dist/engine/buffered-writer.js +158 -0
  42. package/dist/engine/buffered-writer.js.map +1 -0
  43. package/dist/engine/catalog.d.ts +82 -0
  44. package/dist/engine/catalog.d.ts.map +1 -0
  45. package/dist/engine/catalog.js +47 -0
  46. package/dist/engine/catalog.js.map +1 -0
  47. package/dist/engine/client.d.ts +211 -0
  48. package/dist/engine/client.d.ts.map +1 -0
  49. package/dist/engine/client.js +538 -0
  50. package/dist/engine/client.js.map +1 -0
  51. package/dist/engine/coordinator.d.ts +17 -0
  52. package/dist/engine/coordinator.d.ts.map +1 -0
  53. package/dist/engine/coordinator.js +60 -0
  54. package/dist/engine/coordinator.js.map +1 -0
  55. package/dist/engine/database.d.ts +661 -0
  56. package/dist/engine/database.d.ts.map +1 -0
  57. package/dist/engine/database.js +10313 -0
  58. package/dist/engine/database.js.map +1 -0
  59. package/dist/engine/defaults.d.ts +33 -0
  60. package/dist/engine/defaults.d.ts.map +1 -0
  61. package/dist/engine/defaults.js +87 -0
  62. package/dist/engine/defaults.js.map +1 -0
  63. package/dist/engine/errors.d.ts +55 -0
  64. package/dist/engine/errors.d.ts.map +1 -0
  65. package/dist/engine/errors.js +81 -0
  66. package/dist/engine/errors.js.map +1 -0
  67. package/dist/engine/fts.d.ts +93 -0
  68. package/dist/engine/fts.d.ts.map +1 -0
  69. package/dist/engine/fts.js +273 -0
  70. package/dist/engine/fts.js.map +1 -0
  71. package/dist/engine/group-index.d.ts +32 -0
  72. package/dist/engine/group-index.d.ts.map +1 -0
  73. package/dist/engine/group-index.js +366 -0
  74. package/dist/engine/group-index.js.map +1 -0
  75. package/dist/engine/index.d.ts +13 -0
  76. package/dist/engine/index.d.ts.map +1 -0
  77. package/dist/engine/index.js +12 -0
  78. package/dist/engine/index.js.map +1 -0
  79. package/dist/engine/join-index.d.ts +12 -0
  80. package/dist/engine/join-index.d.ts.map +1 -0
  81. package/dist/engine/join-index.js +217 -0
  82. package/dist/engine/join-index.js.map +1 -0
  83. package/dist/engine/live.d.ts +86 -0
  84. package/dist/engine/live.d.ts.map +1 -0
  85. package/dist/engine/live.js +289 -0
  86. package/dist/engine/live.js.map +1 -0
  87. package/dist/engine/memory.d.ts +37 -0
  88. package/dist/engine/memory.d.ts.map +1 -0
  89. package/dist/engine/memory.js +119 -0
  90. package/dist/engine/memory.js.map +1 -0
  91. package/dist/engine/optimizer.d.ts +20 -0
  92. package/dist/engine/optimizer.d.ts.map +1 -0
  93. package/dist/engine/optimizer.js +1170 -0
  94. package/dist/engine/optimizer.js.map +1 -0
  95. package/dist/engine/query-cache.d.ts +10 -0
  96. package/dist/engine/query-cache.d.ts.map +1 -0
  97. package/dist/engine/query-cache.js +58 -0
  98. package/dist/engine/query-cache.js.map +1 -0
  99. package/dist/engine/query.d.ts +844 -0
  100. package/dist/engine/query.d.ts.map +1 -0
  101. package/dist/engine/query.js +7450 -0
  102. package/dist/engine/query.js.map +1 -0
  103. package/dist/engine/schema-wire.d.ts +92 -0
  104. package/dist/engine/schema-wire.d.ts.map +1 -0
  105. package/dist/engine/schema-wire.js +115 -0
  106. package/dist/engine/schema-wire.js.map +1 -0
  107. package/dist/engine/schema.d.ts +394 -0
  108. package/dist/engine/schema.d.ts.map +1 -0
  109. package/dist/engine/schema.js +709 -0
  110. package/dist/engine/schema.js.map +1 -0
  111. package/dist/engine/sort-keys.d.ts +36 -0
  112. package/dist/engine/sort-keys.d.ts.map +1 -0
  113. package/dist/engine/sort-keys.js +60 -0
  114. package/dist/engine/sort-keys.js.map +1 -0
  115. package/dist/engine/sql-json.d.ts +30 -0
  116. package/dist/engine/sql-json.d.ts.map +1 -0
  117. package/dist/engine/sql-json.js +120 -0
  118. package/dist/engine/sql-json.js.map +1 -0
  119. package/dist/engine/sql-semantics.d.ts +31 -0
  120. package/dist/engine/sql-semantics.d.ts.map +1 -0
  121. package/dist/engine/sql-semantics.js +121 -0
  122. package/dist/engine/sql-semantics.js.map +1 -0
  123. package/dist/engine/vector.d.ts +87 -0
  124. package/dist/engine/vector.d.ts.map +1 -0
  125. package/dist/engine/vector.js +4227 -0
  126. package/dist/engine/vector.js.map +1 -0
  127. package/dist/engine/worker-host.d.ts +48 -0
  128. package/dist/engine/worker-host.d.ts.map +1 -0
  129. package/dist/engine/worker-host.js +501 -0
  130. package/dist/engine/worker-host.js.map +1 -0
  131. package/dist/engine/worker.d.ts +2 -0
  132. package/dist/engine/worker.d.ts.map +1 -0
  133. package/dist/engine/worker.js +14 -0
  134. package/dist/engine/worker.js.map +1 -0
  135. package/dist/index.d.ts +2 -0
  136. package/dist/index.d.ts.map +1 -0
  137. package/dist/index.js +2 -0
  138. package/dist/index.js.map +1 -0
  139. package/dist/plan/index.d.ts +17 -0
  140. package/dist/plan/index.d.ts.map +1 -0
  141. package/dist/plan/index.js +17 -0
  142. package/dist/plan/index.js.map +1 -0
  143. package/dist/storage/fixture-shape.d.ts +42 -0
  144. package/dist/storage/fixture-shape.d.ts.map +1 -0
  145. package/dist/storage/fixture-shape.js +146 -0
  146. package/dist/storage/fixture-shape.js.map +1 -0
  147. package/dist/storage/index.d.ts +5 -0
  148. package/dist/storage/index.d.ts.map +1 -0
  149. package/dist/storage/index.js +5 -0
  150. package/dist/storage/index.js.map +1 -0
  151. package/dist/storage/indexeddb.d.ts +139 -0
  152. package/dist/storage/indexeddb.d.ts.map +1 -0
  153. package/dist/storage/indexeddb.js +3161 -0
  154. package/dist/storage/indexeddb.js.map +1 -0
  155. package/dist/storage/memory.d.ts +121 -0
  156. package/dist/storage/memory.d.ts.map +1 -0
  157. package/dist/storage/memory.js +1455 -0
  158. package/dist/storage/memory.js.map +1 -0
  159. package/dist/storage/snapshot.d.ts +108 -0
  160. package/dist/storage/snapshot.d.ts.map +1 -0
  161. package/dist/storage/snapshot.js +276 -0
  162. package/dist/storage/snapshot.js.map +1 -0
  163. package/dist/storage/types.d.ts +895 -0
  164. package/dist/storage/types.d.ts.map +1 -0
  165. package/dist/storage/types.js +1549 -0
  166. package/dist/storage/types.js.map +1 -0
  167. package/dist/testing/index.d.ts +76 -0
  168. package/dist/testing/index.d.ts.map +1 -0
  169. package/dist/testing/index.js +233 -0
  170. package/dist/testing/index.js.map +1 -0
  171. package/dist/testing/seeds.d.ts +11 -0
  172. package/dist/testing/seeds.d.ts.map +1 -0
  173. package/dist/testing/seeds.js +50 -0
  174. package/dist/testing/seeds.js.map +1 -0
  175. package/dist/transactions/index.d.ts +148 -0
  176. package/dist/transactions/index.d.ts.map +1 -0
  177. package/dist/transactions/index.js +662 -0
  178. package/dist/transactions/index.js.map +1 -0
  179. package/dist/worker-protocol/index.d.ts +84 -0
  180. package/dist/worker-protocol/index.d.ts.map +1 -0
  181. package/dist/worker-protocol/index.js +117 -0
  182. package/dist/worker-protocol/index.js.map +1 -0
  183. package/package.json +66 -16
  184. package/sql-feature-matrix.json +1275 -0
  185. package/index.d.ts +0 -1
  186. package/index.js +0 -1
@@ -0,0 +1,1170 @@
1
+ import { childExpressions, expressionAliases, forEachBlockExpression, forEachNestedBlock, isScalarFunctionName, scalarFunctionNames, scalarFunctionValue, splitCondition, } from "./query.js";
2
+ /**
3
+ * Deterministic plan-to-plan rewrites over the shared compiled representation. Every rule
4
+ * preserves result semantics exactly; rules that cannot prove safety leave the plan unchanged.
5
+ * The optimizer runs before subquery resolution, so subquery blocks optimize recursively and
6
+ * predicates containing subqueries stay self-contained when they move.
7
+ */
8
+ export function optimizePlan(plan) {
9
+ const optimized = structuredClone(plan);
10
+ optimizeBlock(optimized);
11
+ return optimized;
12
+ }
13
+ /**
14
+ * Cost-based build-side selection using prepared or catalog-derived row counts. A single inner
15
+ * equi-join probes from the base into an index over the joined table, so a joined input more than
16
+ * twice the base size swaps sides and builds over the smaller input. Asymmetric and observable
17
+ * source-order shapes stay unchanged.
18
+ */
19
+ export function chooseJoinOrder(plan, inputs) {
20
+ const join = plan.joins[0];
21
+ if (plan.joins.length !== 1 ||
22
+ join?.kind !== "inner" ||
23
+ join.on !== undefined ||
24
+ plan.select[0]?.expression.kind === "wildcard") {
25
+ return plan;
26
+ }
27
+ const baseRows = inputs.get(plan.base.table)?.rowCount ?? 0;
28
+ const joinRows = inputs.get(join.table)?.rowCount ?? 0;
29
+ if (joinRows <= baseRows * 2)
30
+ return plan;
31
+ const { kind, left, right, ...joinSource } = join;
32
+ void kind;
33
+ return {
34
+ ...plan,
35
+ base: joinSource,
36
+ joins: [{ ...plan.base, kind: "inner", left, right }],
37
+ };
38
+ }
39
+ function optimizeBlock(block) {
40
+ decorrelateBlock(block);
41
+ for (const source of [block.base, ...block.joins]) {
42
+ if (source.derived !== undefined)
43
+ optimizeBlock(source.derived);
44
+ source.union?.blocks.forEach(optimizeBlock);
45
+ if (source.windowed !== undefined)
46
+ optimizeBlock(source.windowed.block);
47
+ if (source.recursive !== undefined) {
48
+ optimizeBlock(source.recursive.base);
49
+ optimizeBlock(source.recursive.step);
50
+ }
51
+ }
52
+ foldBlockConstants(block);
53
+ extractJoinKeys(block);
54
+ normalizeBooleanPredicates(block);
55
+ coalesceOrEqualityLists(block);
56
+ pushPredicatesIntoDerived(block);
57
+ pruneDerivedProjections(block);
58
+ combineDerivedLimit(block);
59
+ }
60
+ // --- Join key extraction ---------------------------------------------------------------------
61
+ /**
62
+ * Pulls a hash key out of a conjunctive ON clause.
63
+ *
64
+ * A join carries either an equality — which builds a hash index and probes it — or a general
65
+ * condition, which is a nested loop over every pair of rows. Only a bare `a.x = b.x` was ever
66
+ * recognized as the first kind, so `ON b.order_id = a.order_id AND b.product_id > a.product_id`
67
+ * became a cross product with a filter, as did the plain multi-key `ON a.x = b.x AND a.y = b.y`.
68
+ * The equality is right there in both.
69
+ *
70
+ * For an inner join the ON clause and WHERE are the same thing, so one equality becomes the key
71
+ * and every remaining conjunct becomes an ordinary predicate. A left join is left alone: its ON
72
+ * decides which rows are null-extended, and a predicate applied afterwards would delete them.
73
+ */
74
+ function extractJoinKeys(block) {
75
+ const available = new Set([block.base.alias]);
76
+ for (const join of block.joins) {
77
+ const condition = join.on;
78
+ if (condition === undefined || join.kind !== "inner") {
79
+ available.add(join.alias);
80
+ continue;
81
+ }
82
+ const conjuncts = splitCondition(condition);
83
+ const keyIndex = conjuncts.findIndex((conjunct) => conjunct.operator === "=" &&
84
+ joinsAcross(conjunct.left, conjunct.right, join.alias, available));
85
+ const key = conjuncts[keyIndex];
86
+ if (key === undefined) {
87
+ available.add(join.alias);
88
+ continue;
89
+ }
90
+ join.left = key.left;
91
+ join.right = key.right;
92
+ delete join.on;
93
+ block.predicates.push(...conjuncts.filter((_, index) => index !== keyIndex));
94
+ available.add(join.alias);
95
+ }
96
+ }
97
+ /**
98
+ * Whether an equality's two sides split cleanly along this join: one side reads only the table
99
+ * being joined, the other only tables already in hand. Anything else — a side spanning both, a
100
+ * side naming a table that has not been joined yet, a comparison of two columns of the same
101
+ * table — is a filter rather than a key, and stays in the residual.
102
+ */
103
+ function joinsAcross(left, right, alias, available) {
104
+ const sides = [expressionAliases(left), expressionAliases(right)];
105
+ const [joined, prior] = sides[0]?.has(alias) === true ? sides : [sides[1], sides[0]];
106
+ if (joined === undefined || prior === undefined)
107
+ return false;
108
+ if (joined.size !== 1 || !joined.has(alias))
109
+ return false;
110
+ return prior.size > 0 && !prior.has(alias) && [...prior].every((name) => available.has(name));
111
+ }
112
+ // --- Boolean predicate normalization --------------------------------------------------------------
113
+ //
114
+ // The scan compiles an unboxed kernel per predicate, but only for a predicate that *is* a
115
+ // comparison -- a NOT, or a conjunction nested inside one predicate, has no kernel and sends
116
+ // every row through the generic expression evaluator. Both shapes have kernel-compilable
117
+ // equivalents. NOT distributes into its operand (De Morgan, and each comparison into its
118
+ // complement), and a conjunction splits into independent predicates. Every rewrite below holds
119
+ // in three-valued logic: negating a comparison keeps NULL comparisons unknown, exactly as
120
+ // negating the original did.
121
+ /** The comparison that is true exactly when `operator` is false, and unknown where it is. */
122
+ const complementaryOperators = new Map([
123
+ ["=", "!="],
124
+ ["!=", "="],
125
+ ["<>", "="],
126
+ [">", "<="],
127
+ [">=", "<"],
128
+ ["<", ">="],
129
+ ["<=", ">"],
130
+ ["IN", "NOT IN"],
131
+ ["NOT IN", "IN"],
132
+ ["LIKE", "NOT LIKE"],
133
+ ["NOT LIKE", "LIKE"],
134
+ ["ILIKE", "NOT ILIKE"],
135
+ ["NOT ILIKE", "ILIKE"],
136
+ ["IS NULL", "IS NOT NULL"],
137
+ ["IS NOT NULL", "IS NULL"],
138
+ ["IS DISTINCT FROM", "IS NOT DISTINCT FROM"],
139
+ ["IS NOT DISTINCT FROM", "IS DISTINCT FROM"],
140
+ ]);
141
+ /** Rewrites `NOT x` into an equivalent form without the NOT, or undefined when x has none. */
142
+ function negated(expression) {
143
+ if (expression.kind === "not")
144
+ return simplifyNegations(expression.operand);
145
+ if (expression.kind === "logical") {
146
+ // De Morgan. Both branches must lose their NOT too, or the rewrite trades one for two.
147
+ const left = negated(expression.left);
148
+ const right = negated(expression.right);
149
+ if (left === undefined || right === undefined)
150
+ return undefined;
151
+ return { kind: "logical", operator: expression.operator === "and" ? "or" : "and", left, right };
152
+ }
153
+ if (expression.kind !== "condition")
154
+ return undefined;
155
+ const complement = complementaryOperators.get(expression.operator);
156
+ if (complement === undefined)
157
+ return undefined;
158
+ return {
159
+ ...expression,
160
+ operator: complement,
161
+ left: simplifyNegations(expression.left),
162
+ right: simplifyNegations(expression.right),
163
+ };
164
+ }
165
+ function simplifyNegations(expression) {
166
+ if (expression.kind === "not") {
167
+ return (negated(expression.operand) ?? { kind: "not", operand: simplifyNegations(expression.operand) });
168
+ }
169
+ if (expression.kind === "logical" || expression.kind === "condition") {
170
+ return {
171
+ ...expression,
172
+ left: simplifyNegations(expression.left),
173
+ right: simplifyNegations(expression.right),
174
+ };
175
+ }
176
+ if (expression.kind === "list") {
177
+ return { kind: "list", items: expression.items.map(simplifyNegations) };
178
+ }
179
+ if (expression.kind === "case") {
180
+ return {
181
+ ...expression,
182
+ branches: expression.branches.map((branch) => ({
183
+ when: simplifyNegations(branch.when),
184
+ then: simplifyNegations(branch.then),
185
+ })),
186
+ ...(expression.otherwise === undefined
187
+ ? {}
188
+ : { otherwise: simplifyNegations(expression.otherwise) }),
189
+ };
190
+ }
191
+ return expression;
192
+ }
193
+ /**
194
+ * Expands one predicate into the conjunction it represents. A bare boolean expression arrives
195
+ * wrapped in IS TRUE, which keeps a row exactly when the expression is true -- the same test the
196
+ * WHERE filter applies -- so a comparison inside the wrapper becomes the predicate itself, and an
197
+ * AND inside it becomes separate predicates that each compile their own kernel.
198
+ */
199
+ function expandPredicate(predicate, output) {
200
+ if (predicate.operator !== "IS TRUE") {
201
+ output.push(predicate);
202
+ return;
203
+ }
204
+ const inner = predicate.left;
205
+ if (inner.kind === "logical" && inner.operator === "and") {
206
+ expandPredicate({ left: inner.left, operator: "IS TRUE", right: predicate.right }, output);
207
+ expandPredicate({ left: inner.right, operator: "IS TRUE", right: predicate.right }, output);
208
+ return;
209
+ }
210
+ if (inner.kind === "condition") {
211
+ output.push({
212
+ left: inner.left,
213
+ operator: inner.operator,
214
+ right: inner.right,
215
+ ...(inner.escape === undefined ? {} : { escape: inner.escape }),
216
+ });
217
+ return;
218
+ }
219
+ output.push(predicate);
220
+ }
221
+ function normalizeBooleanPredicates(block) {
222
+ const expanded = [];
223
+ for (const predicate of block.predicates) {
224
+ expandPredicate({
225
+ ...predicate,
226
+ left: simplifyNegations(predicate.left),
227
+ right: simplifyNegations(predicate.right),
228
+ }, expanded);
229
+ }
230
+ block.predicates = expanded;
231
+ }
232
+ // --- OR-of-equalities to IN ---------------------------------------------------------------------
233
+ //
234
+ // `col = a OR col = b OR col = c` is exactly how SQL defines `col IN (a, b, c)` -- same result,
235
+ // same three-valued NULL behaviour -- but the two shapes execute nothing alike. An OR is one
236
+ // predicate the scan cannot compile a kernel for, so every row walks the generic expression
237
+ // evaluator; an IN list compiles to an unboxed membership kernel and, because zone maps
238
+ // understand IN, prunes whole blocks the list cannot reach. Normalizing the disjunction into
239
+ // the list form is the difference between reading the table and reading one block of it.
240
+ /** The column reference an `=` leaf tests, with the value it is compared against. */
241
+ function orEqualityLeaf(expression) {
242
+ if (expression.kind !== "condition" || expression.operator !== "=")
243
+ return undefined;
244
+ // Only literals and placeholders become list members: they are side-effect free and stay
245
+ // valid when the list binds later. Anything else keeps its own evaluation order.
246
+ const isValue = (candidate) => candidate.kind === "literal" || candidate.kind === "parameter";
247
+ const { left, right } = expression;
248
+ if (left.kind === "column" && isValue(right))
249
+ return { reference: left.reference, value: right };
250
+ if (right.kind === "column" && isValue(left))
251
+ return { reference: right.reference, value: left };
252
+ return undefined;
253
+ }
254
+ /**
255
+ * Flattens an OR tree into `=` leaves that all test the same column, or undefined when any
256
+ * branch is something else -- a mixed disjunction keeps its original shape rather than being
257
+ * split into a partial rewrite that would still leave an OR behind.
258
+ */
259
+ function sameColumnOrEqualities(expression) {
260
+ const values = [];
261
+ let reference;
262
+ const visit = (node) => {
263
+ if (node.kind === "logical" && node.operator === "or") {
264
+ return visit(node.left) && visit(node.right);
265
+ }
266
+ const leaf = orEqualityLeaf(node);
267
+ if (leaf === undefined)
268
+ return false;
269
+ reference ??= leaf.reference;
270
+ if (leaf.reference !== reference)
271
+ return false;
272
+ values.push(leaf.value);
273
+ return true;
274
+ };
275
+ if (!visit(expression))
276
+ return undefined;
277
+ if (reference === undefined || values.length < 2)
278
+ return undefined;
279
+ return { reference, values };
280
+ }
281
+ function coalesceOrEqualityLists(block) {
282
+ block.predicates = block.predicates.map((predicate) => {
283
+ // A bare OR reaches the predicate list wrapped in IS TRUE, which collapses unknown to
284
+ // false exactly as the WHERE filter does -- so dropping the wrapper changes nothing.
285
+ if (predicate.operator !== "IS TRUE" || predicate.left.kind !== "logical")
286
+ return predicate;
287
+ const coalesced = sameColumnOrEqualities(predicate.left);
288
+ if (coalesced === undefined)
289
+ return predicate;
290
+ return {
291
+ left: { kind: "column", reference: coalesced.reference },
292
+ operator: "IN",
293
+ right: { kind: "list", items: coalesced.values },
294
+ };
295
+ });
296
+ }
297
+ const comparisonOperators = new Set(["=", "!=", "<>", ">", ">=", "<", "<="]);
298
+ const aggregateCallNames = new Set(["COUNT", "SUM", "AVG", "MIN", "MAX"]);
299
+ function decorrelateBlock(block) {
300
+ const scope = new Set([block.base.alias, ...block.joins.map((join) => join.alias)]);
301
+ const nextAlias = correlationAliasFactory(scope);
302
+ const rewritten = [];
303
+ for (const predicate of block.predicates) {
304
+ const replacement = decorrelatePredicate(block, predicate, scope, nextAlias);
305
+ if (replacement === "consumed")
306
+ continue;
307
+ rewritten.push(replacement ?? predicate);
308
+ }
309
+ block.predicates = rewritten;
310
+ const grouped = block.groupBy.length > 0 || block.select.some((item) => containsAggregateCall(item.expression));
311
+ for (const item of block.select) {
312
+ if (!expressionHasCorrelatedSubquery(item.expression))
313
+ continue;
314
+ if (grouped) {
315
+ throw new TypeError("Correlated select-list subqueries are not supported with grouping or aggregates");
316
+ }
317
+ item.expression = rewriteCorrelatedScalars(block, item.expression, scope, nextAlias);
318
+ }
319
+ assertNoCorrelation(block);
320
+ }
321
+ function containsAggregateCall(expression) {
322
+ if (expression.kind === "call" && aggregateCallNames.has(expression.name))
323
+ return true;
324
+ return childExpressions(expression).some(containsAggregateCall);
325
+ }
326
+ function expressionHasCorrelatedSubquery(expression) {
327
+ if (expression.kind === "subquery")
328
+ return blockReferencesOutside(expression.block);
329
+ if (expression.kind === "exists")
330
+ return false;
331
+ return childExpressions(expression).some(expressionHasCorrelatedSubquery);
332
+ }
333
+ /** Replaces correlated scalar subqueries inside one select expression with decorrelated joins. */
334
+ function rewriteCorrelatedScalars(block, expression, scope, nextAlias) {
335
+ if (expression.kind === "subquery" && blockReferencesOutside(expression.block)) {
336
+ return decorrelateScalarSubquery(block, expression, scope, nextAlias);
337
+ }
338
+ if (expression.kind === "binary" ||
339
+ expression.kind === "condition" ||
340
+ expression.kind === "logical") {
341
+ expression.left = rewriteCorrelatedScalars(block, expression.left, scope, nextAlias);
342
+ expression.right = rewriteCorrelatedScalars(block, expression.right, scope, nextAlias);
343
+ return expression;
344
+ }
345
+ if (expression.kind === "call") {
346
+ expression.arguments = expression.arguments.map((argument) => rewriteCorrelatedScalars(block, argument, scope, nextAlias));
347
+ return expression;
348
+ }
349
+ if (expression.kind === "not") {
350
+ expression.operand = rewriteCorrelatedScalars(block, expression.operand, scope, nextAlias);
351
+ return expression;
352
+ }
353
+ if (expression.kind === "case") {
354
+ for (const branch of expression.branches) {
355
+ branch.when = rewriteCorrelatedScalars(block, branch.when, scope, nextAlias);
356
+ branch.then = rewriteCorrelatedScalars(block, branch.then, scope, nextAlias);
357
+ }
358
+ if (expression.otherwise !== undefined) {
359
+ expression.otherwise = rewriteCorrelatedScalars(block, expression.otherwise, scope, nextAlias);
360
+ }
361
+ return expression;
362
+ }
363
+ return expression;
364
+ }
365
+ function correlationAliasFactory(scope) {
366
+ let sequence = 0;
367
+ return () => {
368
+ for (;;) {
369
+ sequence += 1;
370
+ const alias = `corr_${String(sequence)}`;
371
+ if (!scope.has(alias))
372
+ return alias;
373
+ }
374
+ };
375
+ }
376
+ function decorrelatePredicate(block, predicate, scope, nextAlias) {
377
+ // EXISTS / NOT EXISTS as a bare WHERE conjunct.
378
+ if (predicate.operator === "IS TRUE") {
379
+ let node = predicate.left;
380
+ let negated = false;
381
+ while (node.kind === "not") {
382
+ negated = !negated;
383
+ node = node.operand;
384
+ }
385
+ if (node.kind !== "exists")
386
+ return undefined;
387
+ if (node.negated)
388
+ negated = !negated;
389
+ const inner = node.block;
390
+ if (!blockReferencesOutside(inner))
391
+ return undefined;
392
+ rejectGroupedInner(inner, "EXISTS");
393
+ guardWildcard(block);
394
+ const keys = extractCorrelation(inner, scope, "EXISTS");
395
+ const alias = nextAlias();
396
+ const derived = {
397
+ sql: "(correlated exists)",
398
+ base: inner.base,
399
+ joins: inner.joins,
400
+ select: keys.map((key, index) => ({ expression: key.inner, alias: `k${String(index)}` })),
401
+ predicates: inner.predicates,
402
+ // Grouping by the keys deduplicates, so the join multiplies no outer row. The parser's
403
+ // injected LIMIT 1 and any ORDER BY are dropped: existence ignores both.
404
+ groupBy: keys.map((key) => key.inner),
405
+ having: [],
406
+ orderBy: [],
407
+ };
408
+ pushCorrelationJoin(block, alias, derived, keys, negated ? "left" : "inner");
409
+ if (!negated)
410
+ return "consumed";
411
+ return {
412
+ left: { kind: "column", reference: `${alias}.k0` },
413
+ operator: "IS NULL",
414
+ right: { kind: "literal", value: null },
415
+ };
416
+ }
417
+ // outer IN (correlated subquery)
418
+ if ((predicate.operator === "IN" || predicate.operator === "NOT IN") &&
419
+ predicate.right.kind === "subquery") {
420
+ const inner = predicate.right.block;
421
+ if (!blockReferencesOutside(inner))
422
+ return undefined;
423
+ if (predicate.operator === "NOT IN") {
424
+ throw new TypeError("Correlated NOT IN subqueries are not supported; use NOT EXISTS");
425
+ }
426
+ rejectGroupedInner(inner, "IN");
427
+ const item = inner.select[0];
428
+ if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
429
+ throw new TypeError("An IN subquery must select exactly one column");
430
+ }
431
+ guardWildcard(block);
432
+ const keys = extractCorrelation(inner, scope, "IN");
433
+ const alias = nextAlias();
434
+ const derived = {
435
+ sql: "(correlated in)",
436
+ base: inner.base,
437
+ joins: inner.joins,
438
+ select: [
439
+ ...keys.map((key, index) => ({ expression: key.inner, alias: `k${String(index)}` })),
440
+ { expression: item.expression, alias: "v" },
441
+ ],
442
+ predicates: inner.predicates,
443
+ groupBy: [...keys.map((key) => key.inner), item.expression],
444
+ having: [],
445
+ orderBy: [],
446
+ };
447
+ pushCorrelationJoin(block, alias, derived, keys, "inner");
448
+ return {
449
+ left: predicate.left,
450
+ operator: "=",
451
+ right: { kind: "column", reference: `${alias}.v` },
452
+ };
453
+ }
454
+ // comparison against a correlated scalar aggregate
455
+ if (!comparisonOperators.has(predicate.operator))
456
+ return undefined;
457
+ const scalarSide = predicate.left.kind === "subquery" && blockReferencesOutside(predicate.left.block)
458
+ ? "left"
459
+ : predicate.right.kind === "subquery" && blockReferencesOutside(predicate.right.block)
460
+ ? "right"
461
+ : undefined;
462
+ if (scalarSide === undefined)
463
+ return undefined;
464
+ const subquery = scalarSide === "left" ? predicate.left : predicate.right;
465
+ if (subquery.kind !== "subquery")
466
+ return undefined;
467
+ const value = decorrelateScalarSubquery(block, subquery, scope, nextAlias);
468
+ return scalarSide === "left"
469
+ ? { left: value, operator: predicate.operator, right: predicate.right }
470
+ : { left: predicate.left, operator: predicate.operator, right: value };
471
+ }
472
+ /**
473
+ * Rewrites one correlated scalar-aggregate subquery into a left join against the grouped
474
+ * aggregate, returning the expression that replaces the subquery node (COUNT wraps in COALESCE
475
+ * so an unmatched group reads 0, matching the empty-subquery semantics).
476
+ */
477
+ function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
478
+ const inner = subquery.block;
479
+ const item = inner.select[0];
480
+ const isAggregate = item?.expression.kind === "call" && aggregateCallNames.has(item.expression.name);
481
+ if (inner.select.length !== 1 || item === undefined || !isAggregate) {
482
+ throw new TypeError("A correlated scalar subquery must select exactly one aggregate expression");
483
+ }
484
+ if (inner.groupBy.length > 0 ||
485
+ inner.having.length > 0 ||
486
+ inner.orderBy.length > 0 ||
487
+ inner.limit !== undefined) {
488
+ throw new TypeError("A correlated scalar subquery cannot use GROUP BY, HAVING, ORDER BY, or LIMIT");
489
+ }
490
+ guardWildcard(block);
491
+ const keys = extractCorrelation(inner, scope, "scalar");
492
+ const alias = nextAlias();
493
+ const derived = {
494
+ sql: "(correlated scalar)",
495
+ base: inner.base,
496
+ joins: inner.joins,
497
+ select: [
498
+ ...keys.map((key, index) => ({ expression: key.inner, alias: `k${String(index)}` })),
499
+ { expression: item.expression, alias: "v" },
500
+ ],
501
+ predicates: inner.predicates,
502
+ groupBy: keys.map((key) => key.inner),
503
+ having: [],
504
+ orderBy: [],
505
+ };
506
+ pushCorrelationJoin(block, alias, derived, keys, "left");
507
+ let value = { kind: "column", reference: `${alias}.v` };
508
+ if (item.expression.kind === "call" && item.expression.name === "COUNT") {
509
+ // COUNT over an empty group is 0, but the unmatched left-join side is NULL.
510
+ value = { kind: "call", name: "COALESCE", arguments: [value, { kind: "literal", value: 0 }] };
511
+ }
512
+ return value;
513
+ }
514
+ function pushCorrelationJoin(block, alias, derived, keys, kind) {
515
+ const source = { table: alias, alias, derived };
516
+ const keyReference = (index) => ({
517
+ kind: "column",
518
+ reference: `${alias}.k${String(index)}`,
519
+ });
520
+ const first = keys[0];
521
+ if (keys.length === 1 && first !== undefined) {
522
+ block.joins.push({ ...source, kind, left: first.outer, right: keyReference(0) });
523
+ return;
524
+ }
525
+ let on;
526
+ keys.forEach((key, index) => {
527
+ const equality = {
528
+ kind: "condition",
529
+ operator: "=",
530
+ left: key.outer,
531
+ right: keyReference(index),
532
+ };
533
+ on =
534
+ on === undefined ? equality : { kind: "logical", operator: "and", left: on, right: equality };
535
+ });
536
+ block.joins.push({
537
+ ...source,
538
+ kind,
539
+ left: { kind: "literal", value: null },
540
+ right: { kind: "literal", value: null },
541
+ ...(on === undefined ? {} : { on }),
542
+ });
543
+ }
544
+ function blockReferencesOutside(block) {
545
+ const refs = [];
546
+ collectOutsideReferences(block, new Set(), refs);
547
+ return refs.length > 0;
548
+ }
549
+ /** Collects qualified column references whose alias no enclosing scope inside `block` defines. */
550
+ function collectOutsideReferences(block, outer, refs) {
551
+ const scope = new Set([...outer, block.base.alias, ...block.joins.map((join) => join.alias)]);
552
+ const visit = (expression) => {
553
+ if (expression.kind === "column") {
554
+ const parts = expression.reference.split(".");
555
+ const alias = parts[0];
556
+ if (parts.length === 2 && alias !== undefined && !scope.has(alias)) {
557
+ refs.push(expression.reference);
558
+ }
559
+ return;
560
+ }
561
+ if (expression.kind === "subquery" || expression.kind === "exists") {
562
+ collectOutsideReferences(expression.block, scope, refs);
563
+ return;
564
+ }
565
+ for (const child of childExpressions(expression))
566
+ visit(child);
567
+ };
568
+ forEachBlockExpression(block, visit);
569
+ forEachNestedBlock(block, (nested) => {
570
+ collectOutsideReferences(nested, scope, refs);
571
+ });
572
+ }
573
+ function extractCorrelation(inner, outerScope, label) {
574
+ const innerScope = new Set([inner.base.alias, ...inner.joins.map((join) => join.alias)]);
575
+ const keys = [];
576
+ const kept = [];
577
+ for (const predicate of inner.predicates) {
578
+ const key = correlationEquality(predicate, innerScope, outerScope);
579
+ if (key === undefined)
580
+ kept.push(predicate);
581
+ else
582
+ keys.push(key);
583
+ }
584
+ inner.predicates = kept;
585
+ const leftover = [];
586
+ collectOutsideReferences(inner, new Set(), leftover);
587
+ if (leftover.length > 0) {
588
+ throw new TypeError(`Correlated ${label} subqueries support only equality conditions between one inner and one outer qualified column (unsupported reference: ${leftover[0] ?? ""})`);
589
+ }
590
+ if (keys.length === 0) {
591
+ throw new TypeError(`Correlated ${label} subquery has no usable correlation condition`);
592
+ }
593
+ return keys;
594
+ }
595
+ function correlationEquality(predicate, innerScope, outerScope) {
596
+ if (predicate.operator !== "=")
597
+ return undefined;
598
+ const { left, right } = predicate;
599
+ if (left.kind !== "column" || right.kind !== "column")
600
+ return undefined;
601
+ const sideOf = (reference) => {
602
+ const parts = reference.split(".");
603
+ // An unqualified reference resolves inside the subquery, as it always has.
604
+ if (parts.length !== 2)
605
+ return "inner";
606
+ const alias = parts[0] ?? "";
607
+ if (innerScope.has(alias))
608
+ return "inner";
609
+ if (outerScope.has(alias))
610
+ return "outer";
611
+ return undefined;
612
+ };
613
+ const leftSide = sideOf(left.reference);
614
+ const rightSide = sideOf(right.reference);
615
+ if (leftSide === "inner" && rightSide === "outer")
616
+ return { inner: left, outer: right };
617
+ if (leftSide === "outer" && rightSide === "inner")
618
+ return { inner: right, outer: left };
619
+ return undefined;
620
+ }
621
+ function rejectGroupedInner(inner, label) {
622
+ if (inner.groupBy.length > 0 || inner.having.length > 0) {
623
+ throw new TypeError(`Correlated ${label} subqueries cannot use GROUP BY or HAVING`);
624
+ }
625
+ }
626
+ function guardWildcard(block) {
627
+ if (block.select.some((item) => item.expression.kind === "wildcard")) {
628
+ throw new TypeError("Correlated subqueries cannot be combined with SELECT *");
629
+ }
630
+ }
631
+ function assertNoCorrelation(block) {
632
+ const visit = (expression) => {
633
+ if (expression.kind === "subquery" || expression.kind === "exists") {
634
+ const refs = [];
635
+ collectOutsideReferences(expression.block, new Set(), refs);
636
+ if (refs.length > 0) {
637
+ throw new TypeError(`Correlated subqueries are supported only as top-level WHERE conditions (reference: ${refs[0] ?? ""})`);
638
+ }
639
+ return;
640
+ }
641
+ for (const child of childExpressions(expression))
642
+ visit(child);
643
+ };
644
+ forEachBlockExpression(block, visit);
645
+ }
646
+ // --- Constant folding ---------------------------------------------------------------------------
647
+ function foldBlockConstants(block) {
648
+ for (const item of block.select)
649
+ item.expression = foldExpression(item.expression);
650
+ block.groupBy = block.groupBy.map(foldExpression);
651
+ for (const predicate of [...block.predicates, ...block.having]) {
652
+ predicate.left = foldExpression(predicate.left);
653
+ predicate.right = foldExpression(predicate.right);
654
+ }
655
+ for (const order of block.orderBy)
656
+ order.expression = foldExpression(order.expression);
657
+ }
658
+ function foldExpression(expression) {
659
+ if (expression.kind === "binary") {
660
+ const left = foldExpression(expression.left);
661
+ const right = foldExpression(expression.right);
662
+ if (left.kind === "literal" && right.kind === "literal") {
663
+ const folded = foldBinary(expression.operator, left.value, right.value);
664
+ if (folded !== undefined)
665
+ return { kind: "literal", value: folded };
666
+ }
667
+ return { ...expression, left, right };
668
+ }
669
+ if (expression.kind === "call") {
670
+ const foldedArguments = expression.arguments.map(foldExpression);
671
+ const literalValues = foldedArguments.flatMap((argument) => argument.kind === "literal" ? [argument.value] : []);
672
+ if (expression.name !== "COALESCE" &&
673
+ isScalarFunctionName(expression.name) &&
674
+ literalValues.length === foldedArguments.length) {
675
+ try {
676
+ const folded = scalarFunctionValue(expression.name, literalValues);
677
+ if (folded === null ||
678
+ typeof folded === "string" ||
679
+ typeof folded === "boolean" ||
680
+ folded instanceof Date ||
681
+ (typeof folded === "number" && Number.isFinite(folded))) {
682
+ return { kind: "literal", value: folded };
683
+ }
684
+ }
685
+ catch {
686
+ // A function that rejects its constant arguments folds nowhere; execution reports it.
687
+ }
688
+ }
689
+ return { ...expression, arguments: foldedArguments };
690
+ }
691
+ if (expression.kind === "list") {
692
+ return { ...expression, items: expression.items.map(foldExpression) };
693
+ }
694
+ if (expression.kind === "window") {
695
+ return {
696
+ ...expression,
697
+ partitionBy: expression.partitionBy.map(foldExpression),
698
+ orderBy: expression.orderBy.map((order) => ({
699
+ ...order,
700
+ expression: foldExpression(order.expression),
701
+ })),
702
+ ...(expression.argument === undefined
703
+ ? {}
704
+ : { argument: foldExpression(expression.argument) }),
705
+ };
706
+ }
707
+ if (expression.kind === "condition" || expression.kind === "logical") {
708
+ return {
709
+ ...expression,
710
+ left: foldExpression(expression.left),
711
+ right: foldExpression(expression.right),
712
+ };
713
+ }
714
+ if (expression.kind === "not") {
715
+ return { ...expression, operand: foldExpression(expression.operand) };
716
+ }
717
+ if (expression.kind === "case") {
718
+ const otherwise = expression.otherwise === undefined ? undefined : foldExpression(expression.otherwise);
719
+ return {
720
+ ...expression,
721
+ branches: expression.branches.map((branch) => ({
722
+ when: foldExpression(branch.when),
723
+ then: foldExpression(branch.then),
724
+ })),
725
+ ...(otherwise === undefined ? {} : { otherwise }),
726
+ };
727
+ }
728
+ return expression;
729
+ }
730
+ /** Folds only when the result stays a finite SQL value, preserving runtime semantics otherwise. */
731
+ function foldBinary(operator, leftValue, rightValue) {
732
+ if (leftValue === null || rightValue === null)
733
+ return null;
734
+ if (operator === "||") {
735
+ if (typeof leftValue === "string" && typeof rightValue === "string") {
736
+ return leftValue + rightValue;
737
+ }
738
+ return undefined;
739
+ }
740
+ if (typeof leftValue === "boolean" || typeof rightValue === "boolean")
741
+ return undefined;
742
+ if (typeof leftValue === "string" || typeof rightValue === "string")
743
+ return undefined;
744
+ const left = leftValue instanceof Date ? leftValue.getTime() : leftValue;
745
+ const right = rightValue instanceof Date ? rightValue.getTime() : rightValue;
746
+ if ((operator === "/" || operator === "%") && right === 0)
747
+ return null;
748
+ const result = operator === "+"
749
+ ? left + right
750
+ : operator === "-"
751
+ ? left - right
752
+ : operator === "*"
753
+ ? left * right
754
+ : operator === "%"
755
+ ? left % right
756
+ : left / right;
757
+ return Number.isFinite(result) ? result : undefined;
758
+ }
759
+ // --- Predicate pushdown into derived sources ----------------------------------------------------
760
+ function pushPredicatesIntoDerived(block) {
761
+ // A left join's null-extended rows must still meet the outer WHERE, so only the base source
762
+ // and inner-join sources accept pushed predicates.
763
+ const sources = [block.base, ...block.joins.filter((join) => join.kind === "inner")];
764
+ const singleSource = block.joins.length === 0;
765
+ const remaining = [];
766
+ for (const predicate of block.predicates) {
767
+ const target = pushdownTarget(predicate, sources, singleSource);
768
+ if (target === undefined) {
769
+ remaining.push(predicate);
770
+ continue;
771
+ }
772
+ target.derived.predicates.push(target.rewritten);
773
+ }
774
+ block.predicates = remaining;
775
+ }
776
+ function pushdownTarget(predicate, sources, singleSource) {
777
+ for (const source of sources) {
778
+ const derived = source.derived;
779
+ if (derived === undefined || derived.limit !== undefined)
780
+ continue;
781
+ const left = rewriteForInner(predicate.left, source, derived, singleSource);
782
+ if (left === undefined)
783
+ continue;
784
+ const right = rewriteForInner(predicate.right, source, derived, singleSource);
785
+ if (right === undefined)
786
+ continue;
787
+ return {
788
+ derived,
789
+ rewritten: { left, operator: predicate.operator, right },
790
+ };
791
+ }
792
+ return undefined;
793
+ }
794
+ /**
795
+ * Rewrites an outer predicate side into the derived block's own scope, or returns undefined when
796
+ * the side cannot be proven to reference only this source's pushable outputs. A column maps to
797
+ * the inner select expression it names; for a grouped inner block only group-key outputs are
798
+ * pushable, because filtering other outputs before aggregation changes group contents.
799
+ */
800
+ function rewriteForInner(expression, source, derived, singleSource) {
801
+ if (expression.kind === "literal")
802
+ return expression;
803
+ if (expression.kind === "subquery")
804
+ return expression;
805
+ if (expression.kind === "wildcard" || expression.kind === "window")
806
+ return undefined;
807
+ if (expression.kind === "column") {
808
+ const parts = expression.reference.split(".");
809
+ if (parts.length === 2 && parts[0] !== source.alias)
810
+ return undefined;
811
+ if (parts.length === 1 && !singleSource)
812
+ return undefined;
813
+ const name = parts.length === 2 ? parts[1] : parts[0];
814
+ const item = derived.select.find(({ alias }) => alias === name);
815
+ if (item === undefined || item.expression.kind === "wildcard")
816
+ return undefined;
817
+ if (containsAggregateOrWindow(item.expression))
818
+ return undefined;
819
+ if (derived.groupBy.length > 0) {
820
+ const signature = JSON.stringify(item.expression);
821
+ if (!derived.groupBy.some((group) => JSON.stringify(group) === signature)) {
822
+ return undefined;
823
+ }
824
+ }
825
+ return structuredClone(item.expression);
826
+ }
827
+ if (expression.kind === "binary") {
828
+ const left = rewriteForInner(expression.left, source, derived, singleSource);
829
+ if (left === undefined)
830
+ return undefined;
831
+ const right = rewriteForInner(expression.right, source, derived, singleSource);
832
+ if (right === undefined)
833
+ return undefined;
834
+ return { ...expression, left, right };
835
+ }
836
+ if (expression.kind === "call") {
837
+ if (containsAggregateOrWindow(expression))
838
+ return undefined;
839
+ const rewrittenArguments = [];
840
+ for (const argument of expression.arguments) {
841
+ const rewritten = rewriteForInner(argument, source, derived, singleSource);
842
+ if (rewritten === undefined)
843
+ return undefined;
844
+ rewrittenArguments.push(rewritten);
845
+ }
846
+ return { ...expression, arguments: rewrittenArguments };
847
+ }
848
+ if (expression.kind === "list") {
849
+ const rewrittenItems = [];
850
+ for (const item of expression.items) {
851
+ const rewritten = rewriteForInner(item, source, derived, singleSource);
852
+ if (rewritten === undefined)
853
+ return undefined;
854
+ rewrittenItems.push(rewritten);
855
+ }
856
+ return { ...expression, items: rewrittenItems };
857
+ }
858
+ if (expression.kind === "condition" || expression.kind === "logical") {
859
+ const left = rewriteForInner(expression.left, source, derived, singleSource);
860
+ if (left === undefined)
861
+ return undefined;
862
+ const right = rewriteForInner(expression.right, source, derived, singleSource);
863
+ if (right === undefined)
864
+ return undefined;
865
+ return { ...expression, left, right };
866
+ }
867
+ if (expression.kind === "not") {
868
+ const operand = rewriteForInner(expression.operand, source, derived, singleSource);
869
+ return operand === undefined ? undefined : { ...expression, operand };
870
+ }
871
+ if (expression.kind === "case") {
872
+ const branches = [];
873
+ for (const branch of expression.branches) {
874
+ const when = rewriteForInner(branch.when, source, derived, singleSource);
875
+ const then = rewriteForInner(branch.then, source, derived, singleSource);
876
+ if (when === undefined || then === undefined)
877
+ return undefined;
878
+ branches.push({ when, then });
879
+ }
880
+ let otherwise;
881
+ if (expression.otherwise !== undefined) {
882
+ otherwise = rewriteForInner(expression.otherwise, source, derived, singleSource);
883
+ if (otherwise === undefined)
884
+ return undefined;
885
+ }
886
+ return { ...expression, branches, ...(otherwise === undefined ? {} : { otherwise }) };
887
+ }
888
+ // A full-text document is defined against its own scan source; never push it inward.
889
+ if (expression.kind === "fts")
890
+ return undefined;
891
+ // An EXISTS block (the only remaining kind) is self-contained, so it moves unchanged.
892
+ return expression;
893
+ }
894
+ function containsAggregateOrWindow(expression) {
895
+ if (expression.kind === "window")
896
+ return true;
897
+ if (expression.kind === "call") {
898
+ if (!scalarFunctionNames.has(expression.name))
899
+ return true;
900
+ return expression.arguments.some(containsAggregateOrWindow);
901
+ }
902
+ if (expression.kind === "binary") {
903
+ return (containsAggregateOrWindow(expression.left) || containsAggregateOrWindow(expression.right));
904
+ }
905
+ if (expression.kind === "list")
906
+ return expression.items.some(containsAggregateOrWindow);
907
+ if (expression.kind === "condition" || expression.kind === "logical") {
908
+ return (containsAggregateOrWindow(expression.left) || containsAggregateOrWindow(expression.right));
909
+ }
910
+ if (expression.kind === "not")
911
+ return containsAggregateOrWindow(expression.operand);
912
+ if (expression.kind === "case") {
913
+ return (expression.branches.some((branch) => containsAggregateOrWindow(branch.when) || containsAggregateOrWindow(branch.then)) ||
914
+ (expression.otherwise !== undefined && containsAggregateOrWindow(expression.otherwise)));
915
+ }
916
+ return false;
917
+ }
918
+ // --- Projection pruning into derived sources ----------------------------------------------------
919
+ function pruneDerivedProjections(block) {
920
+ const sources = [block.base, ...block.joins];
921
+ const singleSource = sources.length === 1;
922
+ if (block.select.some((item) => item.expression.kind === "wildcard"))
923
+ return;
924
+ for (const source of sources) {
925
+ const derived = source.derived;
926
+ if (derived === undefined)
927
+ continue;
928
+ // Grouped or aggregate-bearing blocks define semantics through their select list (DISTINCT
929
+ // desugars into grouping), so only plain projection/filter blocks prune.
930
+ if (derived.groupBy.length > 0)
931
+ continue;
932
+ if (derived.select.some((item) => containsAggregateOrWindow(item.expression)))
933
+ continue;
934
+ if (derived.select.some((item) => item.expression.kind === "wildcard"))
935
+ continue;
936
+ const referenced = referencedOutputAliases(block, source, singleSource);
937
+ if (referenced === undefined)
938
+ continue;
939
+ // The block's own ORDER BY resolves against its select aliases (including the hidden
940
+ // "(order N)" items the ORDER-BY-expression desugar adds), so those outputs are
941
+ // load-bearing even when the outer block never reads them.
942
+ const orderReferences = new Set();
943
+ for (const order of derived.orderBy) {
944
+ if (order.expression.kind === "column")
945
+ orderReferences.add(order.expression.reference);
946
+ }
947
+ // An ORDER BY column can name either a select alias or the projected source column
948
+ // itself (`SELECT b AS y ... ORDER BY b`), so both spellings pin the item.
949
+ const kept = derived.select.filter((item) => referenced.has(item.alias) ||
950
+ orderReferences.has(item.alias) ||
951
+ (item.expression.kind === "column" && orderReferences.has(item.expression.reference)));
952
+ if (kept.length === derived.select.length)
953
+ continue;
954
+ derived.select = kept.length > 0 ? kept : derived.select.slice(0, 1);
955
+ }
956
+ }
957
+ /** Every output alias of one source the outer block references, or undefined when unprovable. */
958
+ function referencedOutputAliases(block, source, singleSource) {
959
+ const referenced = new Set();
960
+ const state = { provable: true };
961
+ const visit = (expression) => {
962
+ if (!state.provable)
963
+ return;
964
+ if (expression.kind === "column") {
965
+ const parts = expression.reference.split(".");
966
+ if (parts.length === 2) {
967
+ if (parts[0] === source.alias)
968
+ referenced.add(parts[1] ?? "");
969
+ return;
970
+ }
971
+ if (singleSource)
972
+ referenced.add(parts[0] ?? "");
973
+ else
974
+ state.provable = false;
975
+ return;
976
+ }
977
+ if (expression.kind === "binary" || expression.kind === "condition") {
978
+ visit(expression.left);
979
+ visit(expression.right);
980
+ }
981
+ else if (expression.kind === "logical") {
982
+ visit(expression.left);
983
+ visit(expression.right);
984
+ }
985
+ else if (expression.kind === "not")
986
+ visit(expression.operand);
987
+ else if (expression.kind === "case") {
988
+ for (const branch of expression.branches) {
989
+ visit(branch.when);
990
+ visit(branch.then);
991
+ }
992
+ if (expression.otherwise !== undefined)
993
+ visit(expression.otherwise);
994
+ }
995
+ else if (expression.kind === "call")
996
+ expression.arguments.forEach(visit);
997
+ else if (expression.kind === "list")
998
+ expression.items.forEach(visit);
999
+ else if (expression.kind === "fts") {
1000
+ // MATCH(*) references every source column, so pruning is unprovable.
1001
+ if (expression.columns === "*")
1002
+ state.provable = false;
1003
+ else
1004
+ expression.columns.forEach(visit);
1005
+ }
1006
+ else if (expression.kind === "window") {
1007
+ expression.partitionBy.forEach(visit);
1008
+ expression.orderBy.forEach((order) => {
1009
+ visit(order.expression);
1010
+ });
1011
+ if (expression.argument !== undefined)
1012
+ visit(expression.argument);
1013
+ }
1014
+ };
1015
+ for (const item of block.select)
1016
+ visit(item.expression);
1017
+ block.groupBy.forEach(visit);
1018
+ for (const predicate of [...block.predicates, ...block.having]) {
1019
+ visit(predicate.left);
1020
+ visit(predicate.right);
1021
+ }
1022
+ for (const join of block.joins) {
1023
+ visit(join.left);
1024
+ visit(join.right);
1025
+ }
1026
+ for (const order of block.orderBy) {
1027
+ // An ORDER BY reference may name an output alias of the outer block rather than a source
1028
+ // column; treating it as a source reference is conservative (it only keeps more columns),
1029
+ // except that unqualified multi-source references stay unprovable.
1030
+ visit(order.expression);
1031
+ }
1032
+ return state.provable ? referenced : undefined;
1033
+ }
1034
+ // --- LIMIT combining ----------------------------------------------------------------------------
1035
+ function combineDerivedLimit(block) {
1036
+ const derived = block.base.derived;
1037
+ if (derived === undefined ||
1038
+ block.joins.length > 0 ||
1039
+ block.predicates.length > 0 ||
1040
+ block.groupBy.length > 0 ||
1041
+ block.having.length > 0 ||
1042
+ block.orderBy.length > 0 ||
1043
+ block.limit === undefined ||
1044
+ // An OFFSET at either level changes which rows survive, so the limits cannot merge.
1045
+ block.offset !== undefined ||
1046
+ derived.offset !== undefined) {
1047
+ return;
1048
+ }
1049
+ derived.limit = Math.min(derived.limit ?? Number.MAX_SAFE_INTEGER, block.limit);
1050
+ }
1051
+ // --- Plan rendering -----------------------------------------------------------------------------
1052
+ /** Renders a stable, human-readable plan for snapshot tests and explain(). */
1053
+ export function renderPlan(plan) {
1054
+ const lines = [];
1055
+ renderBlock(plan, 0, lines);
1056
+ return lines.join("\n");
1057
+ }
1058
+ function renderBlock(block, depth, lines) {
1059
+ const pad = " ".repeat(depth);
1060
+ lines.push(`${pad}select ${block.select
1061
+ .map((item) => `${item.alias}: ${renderExpression(item.expression)}`)
1062
+ .join(", ")}`);
1063
+ renderSource(block.base, "from", depth, lines);
1064
+ for (const join of block.joins) {
1065
+ renderSource(join, `${join.kind} join on ${renderExpression(join.left)} = ${renderExpression(join.right)}`, depth, lines);
1066
+ }
1067
+ for (const predicate of block.predicates) {
1068
+ lines.push(`${pad} where ${renderPredicate(predicate)}`);
1069
+ }
1070
+ if (block.groupBy.length > 0) {
1071
+ lines.push(`${pad} group by ${block.groupBy.map(renderExpression).join(", ")}`);
1072
+ }
1073
+ for (const predicate of block.having) {
1074
+ lines.push(`${pad} having ${renderPredicate(predicate)}`);
1075
+ }
1076
+ if (block.orderBy.length > 0) {
1077
+ lines.push(`${pad} order by ${block.orderBy
1078
+ .map((order) => `${renderExpression(order.expression)} ${order.direction}`)
1079
+ .join(", ")}`);
1080
+ }
1081
+ if (block.limit !== undefined)
1082
+ lines.push(`${pad} limit ${String(block.limit)}`);
1083
+ if (block.offset !== undefined)
1084
+ lines.push(`${pad} offset ${String(block.offset)}`);
1085
+ }
1086
+ function renderSource(source, label, depth, lines) {
1087
+ const pad = " ".repeat(depth);
1088
+ if (source.union !== undefined) {
1089
+ lines.push(`${pad} ${label} union [${source.alias}]`);
1090
+ source.union.blocks.forEach((member, index) => {
1091
+ if (index > 0) {
1092
+ lines.push(`${pad} ${source.union?.ops[index - 1] ?? "union"}`);
1093
+ }
1094
+ renderBlock(member, depth + 2, lines);
1095
+ });
1096
+ return;
1097
+ }
1098
+ if (source.windowed !== undefined) {
1099
+ lines.push(`${pad} ${label} window [${source.windowed.windows
1100
+ .map((window) => `${window.alias}: ${window.name}`)
1101
+ .join(", ")}]`);
1102
+ renderBlock(source.windowed.block, depth + 2, lines);
1103
+ return;
1104
+ }
1105
+ if (source.recursive !== undefined) {
1106
+ lines.push(`${pad} ${label} recursive [${source.alias}] ${source.recursive.all ? "union all" : "union"}`);
1107
+ renderBlock(source.recursive.base, depth + 2, lines);
1108
+ lines.push(`${pad} step`);
1109
+ renderBlock(source.recursive.step, depth + 2, lines);
1110
+ return;
1111
+ }
1112
+ if (source.derived !== undefined) {
1113
+ lines.push(`${pad} ${label} derived [${source.alias}]`);
1114
+ renderBlock(source.derived, depth + 2, lines);
1115
+ return;
1116
+ }
1117
+ lines.push(`${pad} ${label} table ${source.table}${source.alias === source.table ? "" : ` as ${source.alias}`}`);
1118
+ }
1119
+ function renderPredicate(predicate) {
1120
+ return `${renderExpression(predicate.left)} ${predicate.operator} ${renderExpression(predicate.right)}`;
1121
+ }
1122
+ function renderExpression(expression) {
1123
+ if (expression.kind === "literal") {
1124
+ const value = expression.value;
1125
+ if (value === null)
1126
+ return "null";
1127
+ if (value instanceof Date)
1128
+ return `date ${value.toISOString()}`;
1129
+ if (typeof value === "string")
1130
+ return `'${value}'`;
1131
+ return String(value);
1132
+ }
1133
+ if (expression.kind === "parameter")
1134
+ return `$${String(expression.index + 1)}`;
1135
+ if (expression.kind === "column")
1136
+ return expression.reference;
1137
+ if (expression.kind === "wildcard")
1138
+ return "*";
1139
+ if (expression.kind === "binary") {
1140
+ return `(${renderExpression(expression.left)} ${expression.operator} ${renderExpression(expression.right)})`;
1141
+ }
1142
+ if (expression.kind === "call") {
1143
+ return `${expression.name}(${expression.arguments.map(renderExpression).join(", ")})`;
1144
+ }
1145
+ if (expression.kind === "list") {
1146
+ return `(${expression.items.map(renderExpression).join(", ")})`;
1147
+ }
1148
+ if (expression.kind === "subquery")
1149
+ return "(subquery)";
1150
+ if (expression.kind === "condition") {
1151
+ return `(${renderExpression(expression.left)} ${expression.operator} ${renderExpression(expression.right)})`;
1152
+ }
1153
+ if (expression.kind === "logical") {
1154
+ return `(${renderExpression(expression.left)} ${expression.operator} ${renderExpression(expression.right)})`;
1155
+ }
1156
+ if (expression.kind === "not")
1157
+ return `(not ${renderExpression(expression.operand)})`;
1158
+ if (expression.kind === "exists")
1159
+ return "exists (subquery)";
1160
+ if (expression.kind === "case") {
1161
+ return `case [${String(expression.branches.length)} branches]`;
1162
+ }
1163
+ if (expression.kind === "fts") {
1164
+ const columns = expression.columns === "*" ? "*" : expression.columns.map(renderExpression).join(", ");
1165
+ const name = expression.op === "match" ? "match" : "bm25";
1166
+ return `${name}(${columns}) against '${expression.query}'`;
1167
+ }
1168
+ return `${expression.name}(...) over (...)`;
1169
+ }
1170
+ //# sourceMappingURL=optimizer.js.map