@minnowdb/core 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/engine/cancellation.d.ts +2 -0
- package/dist/engine/cancellation.js +4 -0
- package/dist/engine/catalog.d.ts +3 -1
- package/dist/engine/catalog.js +1 -0
- package/dist/engine/client.d.ts +32 -4
- package/dist/engine/client.js +82 -15
- package/dist/engine/database.d.ts +23 -14
- package/dist/engine/database.js +528 -79
- package/dist/engine/defaults.js +11 -0
- package/dist/engine/errors.d.ts +13 -0
- package/dist/engine/errors.js +22 -0
- package/dist/engine/fts.d.ts +2 -15
- package/dist/engine/live.d.ts +1 -7
- package/dist/engine/live.js +2 -12
- package/dist/engine/optimizer.d.ts +7 -0
- package/dist/engine/optimizer.js +1349 -76
- package/dist/engine/query.d.ts +11 -278
- package/dist/engine/query.js +178 -49
- package/dist/engine/schema-wire.d.ts +7 -1
- package/dist/engine/schema-wire.js +4 -0
- package/dist/engine/schema.d.ts +67 -33
- package/dist/engine/schema.js +138 -7
- package/dist/engine/sql-domains.d.ts +8 -0
- package/dist/engine/sql-domains.js +25 -0
- package/dist/engine/sql-json.js +22 -3
- package/dist/engine/vector.d.ts +2 -2
- package/dist/engine/vector.js +369 -43
- package/dist/engine/worker-host.js +119 -44
- package/dist/plan/index.d.ts +5 -4
- package/dist/plan/index.js +3 -3
- package/dist/plan/model.d.ts +224 -0
- package/dist/plan/model.js +1 -0
- package/dist/storage/types.d.ts +7 -0
- package/dist/storage/types.js +16 -0
- package/dist/transactions/index.d.ts +5 -3
- package/dist/transactions/index.js +58 -8
- package/dist/worker-protocol/index.d.ts +6 -1
- package/dist/worker-protocol/index.js +5 -2
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +89 -21
package/dist/engine/optimizer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dateIsoString, dateMilliseconds } from "../date-value.js";
|
|
2
|
-
import { childExpressions, expressionAliases, forEachBlockExpression, forEachNestedBlock, isScalarFunctionName, scalarFunctionNames, scalarFunctionValue, splitCondition, volatileScalarFunctionNames, } from "./query.js";
|
|
2
|
+
import { blockHasSubqueries, childExpressions, DUAL_TABLE, expressionAliases, forEachBlockExpression, forEachNestedBlock, hasAggregate, isAggregateCall, isScalarFunctionName, mapBlockExpressions, mapChildExpressions, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, volatileScalarFunctionNames, } from "./query.js";
|
|
3
3
|
import { isSqlDomainValue } from "./sql-domains.js";
|
|
4
4
|
/**
|
|
5
5
|
* Deterministic plan-to-plan rewrites over the shared compiled representation. Every rule
|
|
@@ -9,9 +9,117 @@ import { isSqlDomainValue } from "./sql-domains.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export function optimizePlan(plan) {
|
|
11
11
|
const optimized = structuredClone(plan);
|
|
12
|
-
|
|
12
|
+
const aliases = new Set();
|
|
13
|
+
collectPlanAliases(optimized, aliases);
|
|
14
|
+
optimizeBlock(optimized, correlationAliasFactory(aliases));
|
|
13
15
|
return optimized;
|
|
14
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolves an unqualified column in a nested block against the nearest enclosing query scope
|
|
19
|
+
* when that name does not exist in the nested block itself. Compilation has no catalog, so this
|
|
20
|
+
* SQL name-resolution step runs later for database-backed plans and returns the original plan
|
|
21
|
+
* unchanged when it finds nothing to qualify.
|
|
22
|
+
*/
|
|
23
|
+
export function qualifyCorrelatedReferences(plan, tableColumns) {
|
|
24
|
+
if (!blockHasSubqueries(plan))
|
|
25
|
+
return plan;
|
|
26
|
+
const qualified = structuredClone(plan);
|
|
27
|
+
const changedReferences = [];
|
|
28
|
+
const outputColumns = (source) => {
|
|
29
|
+
if (source.columnAliases !== undefined)
|
|
30
|
+
return source.columnAliases;
|
|
31
|
+
if (source.derived !== undefined)
|
|
32
|
+
return source.derived.select.map(({ alias }) => alias);
|
|
33
|
+
if (source.union !== undefined) {
|
|
34
|
+
return source.union.blocks[0]?.select.map(({ alias }) => alias) ?? [];
|
|
35
|
+
}
|
|
36
|
+
if (source.windowed !== undefined) {
|
|
37
|
+
return [
|
|
38
|
+
...source.windowed.block.select.map(({ alias }) => alias),
|
|
39
|
+
...source.windowed.windows.map(({ alias }) => alias),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
if (source.recursive !== undefined) {
|
|
43
|
+
return source.recursive.base.select.map(({ alias }) => alias);
|
|
44
|
+
}
|
|
45
|
+
return tableColumns.get(source.table) ?? [];
|
|
46
|
+
};
|
|
47
|
+
const blockScope = (block) => {
|
|
48
|
+
const byColumn = new Map();
|
|
49
|
+
for (const source of [block.base, ...block.joins]) {
|
|
50
|
+
for (const column of outputColumns(source)) {
|
|
51
|
+
const aliases = byColumn.get(column) ?? [];
|
|
52
|
+
aliases.push(source.alias);
|
|
53
|
+
byColumn.set(column, aliases);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return byColumn;
|
|
57
|
+
};
|
|
58
|
+
const resolveBlock = (block, outerScopes) => {
|
|
59
|
+
const local = blockScope(block);
|
|
60
|
+
const rewrite = (expression) => {
|
|
61
|
+
if (expression.kind === "column" && !expression.reference.includes(".")) {
|
|
62
|
+
if ((local.get(expression.reference)?.length ?? 0) > 0)
|
|
63
|
+
return expression;
|
|
64
|
+
for (const outer of outerScopes) {
|
|
65
|
+
const aliases = outer.get(expression.reference) ?? [];
|
|
66
|
+
if (aliases.length === 0)
|
|
67
|
+
continue;
|
|
68
|
+
if (aliases.length > 1) {
|
|
69
|
+
throw new TypeError(`Ambiguous outer column: ${expression.reference}`);
|
|
70
|
+
}
|
|
71
|
+
changedReferences.push(expression.reference);
|
|
72
|
+
return { kind: "column", reference: `${aliases[0] ?? ""}.${expression.reference}` };
|
|
73
|
+
}
|
|
74
|
+
return expression;
|
|
75
|
+
}
|
|
76
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
77
|
+
resolveBlock(expression.block, [local, ...outerScopes]);
|
|
78
|
+
return expression;
|
|
79
|
+
}
|
|
80
|
+
if (expression.kind === "window") {
|
|
81
|
+
expression.partitionBy = expression.partitionBy.map(rewrite);
|
|
82
|
+
for (const order of expression.orderBy)
|
|
83
|
+
order.expression = rewrite(order.expression);
|
|
84
|
+
if (expression.argument !== undefined)
|
|
85
|
+
expression.argument = rewrite(expression.argument);
|
|
86
|
+
return expression;
|
|
87
|
+
}
|
|
88
|
+
return mapChildExpressions(expression, rewrite);
|
|
89
|
+
};
|
|
90
|
+
mapBlockExpressions(block, rewrite);
|
|
91
|
+
for (const source of [block.base, ...block.joins]) {
|
|
92
|
+
const sourceOuter = source.lateral === true ? [local, ...outerScopes] : outerScopes;
|
|
93
|
+
if (source.derived !== undefined)
|
|
94
|
+
resolveBlock(source.derived, sourceOuter);
|
|
95
|
+
source.union?.blocks.forEach((member) => resolveBlock(member, sourceOuter));
|
|
96
|
+
if (source.windowed !== undefined)
|
|
97
|
+
resolveBlock(source.windowed.block, sourceOuter);
|
|
98
|
+
if (source.recursive !== undefined) {
|
|
99
|
+
resolveBlock(source.recursive.base, sourceOuter);
|
|
100
|
+
resolveBlock(source.recursive.step, sourceOuter);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
resolveBlock(qualified, []);
|
|
105
|
+
return changedReferences.length > 0 ? qualified : plan;
|
|
106
|
+
}
|
|
107
|
+
/** Every source alias in a plan tree, including blocks nested inside expressions. */
|
|
108
|
+
function collectPlanAliases(block, aliases) {
|
|
109
|
+
aliases.add(block.base.alias);
|
|
110
|
+
for (const join of block.joins)
|
|
111
|
+
aliases.add(join.alias);
|
|
112
|
+
const visitExpression = (expression) => {
|
|
113
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
114
|
+
collectPlanAliases(expression.block, aliases);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const child of childExpressions(expression))
|
|
118
|
+
visitExpression(child);
|
|
119
|
+
};
|
|
120
|
+
forEachBlockExpression(block, visitExpression);
|
|
121
|
+
forEachNestedBlock(block, (nested) => collectPlanAliases(nested, aliases));
|
|
122
|
+
}
|
|
15
123
|
/**
|
|
16
124
|
* Cost-based build-side selection using prepared or catalog-derived row counts. A single inner
|
|
17
125
|
* equi-join probes from the base into an index over the joined table, so a joined input more than
|
|
@@ -38,20 +146,30 @@ export function chooseJoinOrder(plan, inputs) {
|
|
|
38
146
|
joins: [{ ...plan.base, kind: "inner", left, right }],
|
|
39
147
|
};
|
|
40
148
|
}
|
|
41
|
-
function optimizeBlock(block) {
|
|
149
|
+
function optimizeBlock(block, nextCorrelationAlias) {
|
|
42
150
|
decorrelateLateralSources(block);
|
|
43
|
-
decorrelateBlock(block);
|
|
151
|
+
decorrelateBlock(block, nextCorrelationAlias);
|
|
44
152
|
for (const source of [block.base, ...block.joins]) {
|
|
45
153
|
if (source.derived !== undefined)
|
|
46
|
-
optimizeBlock(source.derived);
|
|
47
|
-
source.union?.blocks.forEach(optimizeBlock);
|
|
48
|
-
if (source.windowed !== undefined)
|
|
49
|
-
optimizeBlock(source.windowed.block);
|
|
154
|
+
optimizeBlock(source.derived, nextCorrelationAlias);
|
|
155
|
+
source.union?.blocks.forEach((member) => optimizeBlock(member, nextCorrelationAlias));
|
|
156
|
+
if (source.windowed !== undefined) {
|
|
157
|
+
optimizeBlock(source.windowed.block, nextCorrelationAlias);
|
|
158
|
+
}
|
|
50
159
|
if (source.recursive !== undefined) {
|
|
51
|
-
optimizeBlock(source.recursive.base);
|
|
52
|
-
optimizeBlock(source.recursive.step);
|
|
160
|
+
optimizeBlock(source.recursive.base, nextCorrelationAlias);
|
|
161
|
+
optimizeBlock(source.recursive.step, nextCorrelationAlias);
|
|
53
162
|
}
|
|
54
163
|
}
|
|
164
|
+
const optimizeExpressionBlock = (expression) => {
|
|
165
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
166
|
+
optimizeBlock(expression.block, nextCorrelationAlias);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
for (const child of childExpressions(expression))
|
|
170
|
+
optimizeExpressionBlock(child);
|
|
171
|
+
};
|
|
172
|
+
forEachBlockExpression(block, optimizeExpressionBlock);
|
|
55
173
|
foldBlockConstants(block);
|
|
56
174
|
extractJoinKeys(block);
|
|
57
175
|
normalizeBooleanPredicates(block);
|
|
@@ -60,7 +178,7 @@ function optimizeBlock(block) {
|
|
|
60
178
|
pruneDerivedProjections(block);
|
|
61
179
|
combineDerivedLimit(block);
|
|
62
180
|
}
|
|
63
|
-
/** Converts
|
|
181
|
+
/** Converts correlated LATERAL derived tables into ordinary set-at-a-time joins. */
|
|
64
182
|
function decorrelateLateralSources(block) {
|
|
65
183
|
if (block.base.lateral === true) {
|
|
66
184
|
throw new TypeError("LATERAL needs a source to its left");
|
|
@@ -85,14 +203,14 @@ function decorrelateLateralSources(block) {
|
|
|
85
203
|
inner.limit !== undefined) {
|
|
86
204
|
throw new TypeError("Correlated LATERAL queries cannot use grouping, HAVING, ORDER BY, or LIMIT");
|
|
87
205
|
}
|
|
88
|
-
const keys = extractCorrelation(inner, available, "LATERAL");
|
|
206
|
+
const keys = extractCorrelation(inner, available, "LATERAL", true);
|
|
89
207
|
const keyAliases = keys.map((_, index) => `\u0000lateral_key_${String(index + 1)}`);
|
|
90
208
|
keys.forEach((key, index) => {
|
|
91
209
|
inner.select.push({ expression: key.inner, alias: keyAliases[index] ?? "" });
|
|
92
210
|
});
|
|
93
211
|
const correlations = keys.map((key, index) => ({
|
|
94
212
|
kind: "condition",
|
|
95
|
-
operator: key.operator,
|
|
213
|
+
operator: reverseComparison(key.operator),
|
|
96
214
|
left: key.outer,
|
|
97
215
|
right: { kind: "column", reference: `${join.alias}.${keyAliases[index] ?? ""}` },
|
|
98
216
|
}));
|
|
@@ -371,10 +489,19 @@ function coalesceOrEqualityLists(block) {
|
|
|
371
489
|
});
|
|
372
490
|
}
|
|
373
491
|
const comparisonOperators = new Set(["=", "!=", "<>", ">", ">=", "<", "<="]);
|
|
374
|
-
const
|
|
375
|
-
|
|
492
|
+
const correlationKeyAlias = (index) => `\0correlation_key_${String(index)}`;
|
|
493
|
+
const correlationProbeAlias = (index) => `\0correlation_probe_${String(index)}`;
|
|
494
|
+
const correlationInputAlias = (index) => `\0correlation_input_${String(index)}`;
|
|
495
|
+
const correlationOrderAlias = (index) => `\0correlation_order_${String(index)}`;
|
|
496
|
+
const correlationValueAlias = "\0correlation_value";
|
|
497
|
+
const correlationMarkerAlias = "\0correlation_match";
|
|
498
|
+
const correlationRowNumberAlias = "\0correlation_row_number";
|
|
499
|
+
const correlationCountAlias = "\0correlation_count";
|
|
500
|
+
const correlationNonNullCountAlias = "\0correlation_non_null_count";
|
|
501
|
+
const correlationDecisionCountAlias = "\0correlation_decision_count";
|
|
502
|
+
const correlationTruthCountAlias = "\0correlation_truth_count";
|
|
503
|
+
function decorrelateBlock(block, nextAlias) {
|
|
376
504
|
const scope = new Set([block.base.alias, ...block.joins.map((join) => join.alias)]);
|
|
377
|
-
const nextAlias = correlationAliasFactory(scope);
|
|
378
505
|
const rewritten = [];
|
|
379
506
|
for (const predicate of block.predicates) {
|
|
380
507
|
const replacement = decorrelatePredicate(block, predicate, scope, nextAlias);
|
|
@@ -382,57 +509,93 @@ function decorrelateBlock(block) {
|
|
|
382
509
|
continue;
|
|
383
510
|
rewritten.push(replacement ?? predicate);
|
|
384
511
|
}
|
|
385
|
-
block.predicates = rewritten
|
|
512
|
+
block.predicates = rewritten.map((predicate) => ({
|
|
513
|
+
...predicate,
|
|
514
|
+
left: rewriteCorrelatedExpression(block, predicate.left, scope, nextAlias),
|
|
515
|
+
right: rewriteCorrelatedExpression(block, predicate.right, scope, nextAlias),
|
|
516
|
+
}));
|
|
386
517
|
const grouped = block.groupBy.length > 0 || block.select.some((item) => containsAggregateCall(item.expression));
|
|
387
518
|
for (const item of block.select) {
|
|
388
519
|
if (!expressionHasCorrelatedSubquery(item.expression))
|
|
389
520
|
continue;
|
|
390
521
|
if (grouped) {
|
|
391
|
-
|
|
522
|
+
const groupedExpressions = new Set(block.groupBy.map((group) => JSON.stringify(group)));
|
|
523
|
+
const outerReferences = correlatedOuterReferences(item.expression);
|
|
524
|
+
if (containsAggregateCall(item.expression) ||
|
|
525
|
+
outerReferences.some((reference) => !groupedExpressions.has(JSON.stringify({ kind: "column", reference })))) {
|
|
526
|
+
throw new TypeError("A grouped correlated select-list subquery must reference only GROUP BY columns and cannot be nested inside an aggregate");
|
|
527
|
+
}
|
|
392
528
|
}
|
|
393
|
-
item.expression =
|
|
529
|
+
item.expression = rewriteCorrelatedExpression(block, item.expression, scope, nextAlias);
|
|
530
|
+
// The decorrelated value is functionally determined by the outer grouping keys. Carrying it
|
|
531
|
+
// as one additional key makes that dependency explicit to both grouped executors.
|
|
532
|
+
if (grouped)
|
|
533
|
+
block.groupBy.push(structuredClone(item.expression));
|
|
394
534
|
}
|
|
395
535
|
assertNoCorrelation(block);
|
|
396
536
|
}
|
|
397
537
|
function containsAggregateCall(expression) {
|
|
398
|
-
|
|
399
|
-
return true;
|
|
400
|
-
return childExpressions(expression).some(containsAggregateCall);
|
|
538
|
+
return hasAggregate(expression);
|
|
401
539
|
}
|
|
402
540
|
function expressionHasCorrelatedSubquery(expression) {
|
|
403
|
-
if (expression.kind === "subquery")
|
|
541
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
404
542
|
return blockReferencesOutside(expression.block);
|
|
405
|
-
|
|
406
|
-
return false;
|
|
543
|
+
}
|
|
407
544
|
return childExpressions(expression).some(expressionHasCorrelatedSubquery);
|
|
408
545
|
}
|
|
409
|
-
/**
|
|
410
|
-
function
|
|
546
|
+
/** Qualified outer references used by correlated scalar subqueries inside one expression. */
|
|
547
|
+
function correlatedOuterReferences(expression) {
|
|
548
|
+
if ((expression.kind === "subquery" || expression.kind === "exists") &&
|
|
549
|
+
blockReferencesOutside(expression.block)) {
|
|
550
|
+
const references = [];
|
|
551
|
+
collectOutsideReferences(expression.block, new Set(), references);
|
|
552
|
+
return references;
|
|
553
|
+
}
|
|
554
|
+
return childExpressions(expression).flatMap(correlatedOuterReferences);
|
|
555
|
+
}
|
|
556
|
+
/** Replaces every supported correlated subquery leaf inside an arbitrary expression. */
|
|
557
|
+
function rewriteCorrelatedExpression(block, expression, scope, nextAlias) {
|
|
558
|
+
if (expression.kind === "exists" && blockReferencesOutside(expression.block)) {
|
|
559
|
+
return decorrelateExistsExpression(block, expression, scope, nextAlias);
|
|
560
|
+
}
|
|
561
|
+
if (expression.kind === "condition") {
|
|
562
|
+
expression.left = rewriteCorrelatedExpression(block, expression.left, scope, nextAlias);
|
|
563
|
+
if (expression.right.kind === "subquery" && blockReferencesOutside(expression.right.block)) {
|
|
564
|
+
if (expression.operator === "IN" || expression.operator === "NOT IN") {
|
|
565
|
+
const value = decorrelateQuantifiedExpression(block, expression.left, expression.right.block, { comparison: "=", quantifier: "any" }, scope, nextAlias, "IN");
|
|
566
|
+
return expression.operator === "NOT IN" ? { kind: "not", operand: value } : value;
|
|
567
|
+
}
|
|
568
|
+
const quantified = parseQuantified(expression.operator);
|
|
569
|
+
if (quantified !== undefined) {
|
|
570
|
+
return decorrelateQuantifiedExpression(block, expression.left, expression.right.block, quantified, scope, nextAlias, "ANY/ALL");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
expression.right = rewriteCorrelatedExpression(block, expression.right, scope, nextAlias);
|
|
574
|
+
return expression;
|
|
575
|
+
}
|
|
411
576
|
if (expression.kind === "subquery" && blockReferencesOutside(expression.block)) {
|
|
412
577
|
return decorrelateScalarSubquery(block, expression, scope, nextAlias);
|
|
413
578
|
}
|
|
414
|
-
if (expression.kind === "binary" ||
|
|
415
|
-
expression.
|
|
416
|
-
expression.
|
|
417
|
-
expression.left = rewriteCorrelatedScalars(block, expression.left, scope, nextAlias);
|
|
418
|
-
expression.right = rewriteCorrelatedScalars(block, expression.right, scope, nextAlias);
|
|
579
|
+
if (expression.kind === "binary" || expression.kind === "logical") {
|
|
580
|
+
expression.left = rewriteCorrelatedExpression(block, expression.left, scope, nextAlias);
|
|
581
|
+
expression.right = rewriteCorrelatedExpression(block, expression.right, scope, nextAlias);
|
|
419
582
|
return expression;
|
|
420
583
|
}
|
|
421
584
|
if (expression.kind === "call") {
|
|
422
|
-
expression.arguments = expression.arguments.map((argument) =>
|
|
585
|
+
expression.arguments = expression.arguments.map((argument) => rewriteCorrelatedExpression(block, argument, scope, nextAlias));
|
|
423
586
|
return expression;
|
|
424
587
|
}
|
|
425
588
|
if (expression.kind === "not") {
|
|
426
|
-
expression.operand =
|
|
589
|
+
expression.operand = rewriteCorrelatedExpression(block, expression.operand, scope, nextAlias);
|
|
427
590
|
return expression;
|
|
428
591
|
}
|
|
429
592
|
if (expression.kind === "case") {
|
|
430
593
|
for (const branch of expression.branches) {
|
|
431
|
-
branch.when =
|
|
432
|
-
branch.then =
|
|
594
|
+
branch.when = rewriteCorrelatedExpression(block, branch.when, scope, nextAlias);
|
|
595
|
+
branch.then = rewriteCorrelatedExpression(block, branch.then, scope, nextAlias);
|
|
433
596
|
}
|
|
434
597
|
if (expression.otherwise !== undefined) {
|
|
435
|
-
expression.otherwise =
|
|
598
|
+
expression.otherwise = rewriteCorrelatedExpression(block, expression.otherwise, scope, nextAlias);
|
|
436
599
|
}
|
|
437
600
|
return expression;
|
|
438
601
|
}
|
|
@@ -444,8 +607,10 @@ function correlationAliasFactory(scope) {
|
|
|
444
607
|
for (;;) {
|
|
445
608
|
sequence += 1;
|
|
446
609
|
const alias = `corr_${String(sequence)}`;
|
|
447
|
-
if (!scope.has(alias))
|
|
610
|
+
if (!scope.has(alias)) {
|
|
611
|
+
scope.add(alias);
|
|
448
612
|
return alias;
|
|
613
|
+
}
|
|
449
614
|
}
|
|
450
615
|
};
|
|
451
616
|
}
|
|
@@ -466,14 +631,24 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
466
631
|
if (!blockReferencesOutside(inner))
|
|
467
632
|
return undefined;
|
|
468
633
|
rejectGroupedInner(inner, "EXISTS");
|
|
469
|
-
|
|
634
|
+
if (!canExtractCorrelation(inner, scope, "EXISTS", true)) {
|
|
635
|
+
const replacement = decorrelateExistsWithProbes(block, { ...node, negated }, scope, nextAlias);
|
|
636
|
+
return {
|
|
637
|
+
left: replacement,
|
|
638
|
+
operator: "IS TRUE",
|
|
639
|
+
right: { kind: "literal", value: true },
|
|
640
|
+
};
|
|
641
|
+
}
|
|
470
642
|
const keys = extractCorrelation(inner, scope, "EXISTS", true);
|
|
471
643
|
const alias = nextAlias();
|
|
472
644
|
const derived = {
|
|
473
645
|
sql: "(correlated exists)",
|
|
474
646
|
base: inner.base,
|
|
475
647
|
joins: inner.joins,
|
|
476
|
-
select: keys.map((key, index) => ({
|
|
648
|
+
select: keys.map((key, index) => ({
|
|
649
|
+
expression: key.inner,
|
|
650
|
+
alias: correlationKeyAlias(index),
|
|
651
|
+
})),
|
|
477
652
|
predicates: inner.predicates,
|
|
478
653
|
// Grouping by the keys deduplicates, so the join multiplies no outer row. The parser's
|
|
479
654
|
// injected LIMIT 1 and any ORDER BY are dropped: existence ignores both.
|
|
@@ -488,7 +663,7 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
488
663
|
if (!negated)
|
|
489
664
|
return "consumed";
|
|
490
665
|
return {
|
|
491
|
-
left: { kind: "column", reference: `${alias}
|
|
666
|
+
left: { kind: "column", reference: `${alias}.${correlationKeyAlias(0)}` },
|
|
492
667
|
operator: "IS NULL",
|
|
493
668
|
right: { kind: "literal", value: null },
|
|
494
669
|
};
|
|
@@ -504,8 +679,11 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
504
679
|
if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
|
|
505
680
|
throw new TypeError("An IN subquery must select exactly one column");
|
|
506
681
|
}
|
|
507
|
-
|
|
508
|
-
const
|
|
682
|
+
const keys = extractCorrelation(inner, scope, "IN", true);
|
|
683
|
+
const general = keys.some((key) => key.operator !== "=");
|
|
684
|
+
if (predicate.operator === "NOT IN" && general) {
|
|
685
|
+
return decorrelateRangeNotIn(block, predicate.left, inner, item.expression, keys, nextAlias);
|
|
686
|
+
}
|
|
509
687
|
if (predicate.operator === "NOT IN") {
|
|
510
688
|
return decorrelateNotIn(block, predicate.left, inner, item.expression, keys, nextAlias);
|
|
511
689
|
}
|
|
@@ -515,21 +693,113 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
515
693
|
base: inner.base,
|
|
516
694
|
joins: inner.joins,
|
|
517
695
|
select: [
|
|
518
|
-
...keys.map((key, index) => ({
|
|
519
|
-
|
|
696
|
+
...keys.map((key, index) => ({
|
|
697
|
+
expression: key.inner,
|
|
698
|
+
alias: correlationKeyAlias(index),
|
|
699
|
+
})),
|
|
700
|
+
{ expression: item.expression, alias: correlationValueAlias },
|
|
520
701
|
],
|
|
521
702
|
predicates: inner.predicates,
|
|
522
703
|
groupBy: [...keys.map((key) => key.inner), item.expression],
|
|
523
704
|
having: [],
|
|
524
705
|
orderBy: [],
|
|
525
706
|
};
|
|
707
|
+
if (general) {
|
|
708
|
+
const join = generalCorrelationJoin(alias, derived, [
|
|
709
|
+
...keys.map((_, index) => ({
|
|
710
|
+
kind: "column",
|
|
711
|
+
reference: `${alias}.${correlationKeyAlias(index)}`,
|
|
712
|
+
})),
|
|
713
|
+
{ kind: "column", reference: `${alias}.${correlationValueAlias}` },
|
|
714
|
+
], [...keys.map((key) => key.outer), predicate.left], [...keys.map((key) => key.operator), "="]);
|
|
715
|
+
join.kind = "semi";
|
|
716
|
+
block.joins.push(join);
|
|
717
|
+
return "consumed";
|
|
718
|
+
}
|
|
526
719
|
pushCorrelationJoin(block, alias, derived, keys, "inner");
|
|
527
720
|
return {
|
|
528
721
|
left: predicate.left,
|
|
529
722
|
operator: "=",
|
|
530
|
-
right: { kind: "column", reference: `${alias}
|
|
723
|
+
right: { kind: "column", reference: `${alias}.${correlationValueAlias}` },
|
|
531
724
|
};
|
|
532
725
|
}
|
|
726
|
+
// outer comparison ANY / ALL (correlated subquery). At top-level WHERE, ANY is existence of a
|
|
727
|
+
// true comparison; ALL is absence of a false or unknown comparison. Empty sets therefore keep
|
|
728
|
+
// their standard false/true answers without materializing a per-row value list.
|
|
729
|
+
const quantified = parseQuantified(predicate.operator);
|
|
730
|
+
if (quantified !== undefined && predicate.right.kind === "subquery") {
|
|
731
|
+
const inner = predicate.right.block;
|
|
732
|
+
if (!blockReferencesOutside(inner))
|
|
733
|
+
return undefined;
|
|
734
|
+
rejectGroupedInner(inner, "ANY/ALL");
|
|
735
|
+
const item = inner.select[0];
|
|
736
|
+
if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
|
|
737
|
+
throw new TypeError("An ANY/ALL subquery must select exactly one column");
|
|
738
|
+
}
|
|
739
|
+
const keys = extractCorrelation(inner, scope, "ANY/ALL", true);
|
|
740
|
+
const alias = nextAlias();
|
|
741
|
+
const derived = {
|
|
742
|
+
sql: "(correlated quantified comparison)",
|
|
743
|
+
base: inner.base,
|
|
744
|
+
joins: inner.joins,
|
|
745
|
+
select: [
|
|
746
|
+
...keys.map((key, index) => ({
|
|
747
|
+
expression: key.inner,
|
|
748
|
+
alias: correlationKeyAlias(index),
|
|
749
|
+
})),
|
|
750
|
+
{ expression: item.expression, alias: correlationValueAlias },
|
|
751
|
+
],
|
|
752
|
+
predicates: inner.predicates,
|
|
753
|
+
groupBy: [...keys.map((key) => key.inner), item.expression],
|
|
754
|
+
having: [],
|
|
755
|
+
orderBy: [],
|
|
756
|
+
};
|
|
757
|
+
const join = generalCorrelationJoin(alias, derived, keys.map((_, index) => ({
|
|
758
|
+
kind: "column",
|
|
759
|
+
reference: `${alias}.${correlationKeyAlias(index)}`,
|
|
760
|
+
})), keys.map((key) => key.outer), keys.map((key) => key.operator));
|
|
761
|
+
const value = {
|
|
762
|
+
kind: "column",
|
|
763
|
+
reference: `${alias}.${correlationValueAlias}`,
|
|
764
|
+
};
|
|
765
|
+
const comparison = {
|
|
766
|
+
kind: "condition",
|
|
767
|
+
operator: quantified.comparison,
|
|
768
|
+
left: structuredClone(predicate.left),
|
|
769
|
+
right: value,
|
|
770
|
+
};
|
|
771
|
+
let quantifiedCondition = comparison;
|
|
772
|
+
if (quantified.quantifier === "all") {
|
|
773
|
+
quantifiedCondition = {
|
|
774
|
+
kind: "logical",
|
|
775
|
+
operator: "or",
|
|
776
|
+
left: {
|
|
777
|
+
kind: "logical",
|
|
778
|
+
operator: "or",
|
|
779
|
+
left: {
|
|
780
|
+
kind: "condition",
|
|
781
|
+
operator: "IS NULL",
|
|
782
|
+
left: structuredClone(predicate.left),
|
|
783
|
+
right: { kind: "literal", value: null },
|
|
784
|
+
},
|
|
785
|
+
right: {
|
|
786
|
+
kind: "condition",
|
|
787
|
+
operator: "IS NULL",
|
|
788
|
+
left: value,
|
|
789
|
+
right: { kind: "literal", value: null },
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
right: { kind: "not", operand: comparison },
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
join.on =
|
|
796
|
+
join.on === undefined
|
|
797
|
+
? quantifiedCondition
|
|
798
|
+
: { kind: "logical", operator: "and", left: join.on, right: quantifiedCondition };
|
|
799
|
+
join.kind = quantified.quantifier === "all" ? "anti" : "semi";
|
|
800
|
+
block.joins.push(join);
|
|
801
|
+
return "consumed";
|
|
802
|
+
}
|
|
533
803
|
// comparison against a correlated scalar aggregate
|
|
534
804
|
if (!comparisonOperators.has(predicate.operator))
|
|
535
805
|
return undefined;
|
|
@@ -566,9 +836,9 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
566
836
|
select: [
|
|
567
837
|
...keys.map((key, index) => ({
|
|
568
838
|
expression: structuredClone(key.inner),
|
|
569
|
-
alias:
|
|
839
|
+
alias: correlationKeyAlias(index),
|
|
570
840
|
})),
|
|
571
|
-
{ expression: structuredClone(item), alias:
|
|
841
|
+
{ expression: structuredClone(item), alias: correlationValueAlias },
|
|
572
842
|
],
|
|
573
843
|
predicates: structuredClone(inner.predicates),
|
|
574
844
|
groupBy: [...keys.map((key) => structuredClone(key.inner)), structuredClone(item)],
|
|
@@ -589,9 +859,9 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
589
859
|
right: tuple([
|
|
590
860
|
...keys.map((_, index) => ({
|
|
591
861
|
kind: "column",
|
|
592
|
-
reference: `${valuesAlias}
|
|
862
|
+
reference: `${valuesAlias}.${correlationKeyAlias(index)}`,
|
|
593
863
|
})),
|
|
594
|
-
{ kind: "column", reference: `${valuesAlias}
|
|
864
|
+
{ kind: "column", reference: `${valuesAlias}.${correlationValueAlias}` },
|
|
595
865
|
]),
|
|
596
866
|
});
|
|
597
867
|
const flagsAlias = nextAlias();
|
|
@@ -602,15 +872,15 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
602
872
|
select: [
|
|
603
873
|
...keys.map((key, index) => ({
|
|
604
874
|
expression: structuredClone(key.inner),
|
|
605
|
-
alias:
|
|
875
|
+
alias: correlationKeyAlias(index),
|
|
606
876
|
})),
|
|
607
877
|
{
|
|
608
878
|
expression: { kind: "call", name: "COUNT", arguments: [{ kind: "wildcard" }] },
|
|
609
|
-
alias:
|
|
879
|
+
alias: correlationCountAlias,
|
|
610
880
|
},
|
|
611
881
|
{
|
|
612
882
|
expression: { kind: "call", name: "COUNT", arguments: [structuredClone(item)] },
|
|
613
|
-
alias:
|
|
883
|
+
alias: correlationNonNullCountAlias,
|
|
614
884
|
},
|
|
615
885
|
],
|
|
616
886
|
predicates: structuredClone(inner.predicates),
|
|
@@ -619,8 +889,14 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
619
889
|
orderBy: [],
|
|
620
890
|
};
|
|
621
891
|
pushCorrelationJoin(block, flagsAlias, flags, keys, "left");
|
|
622
|
-
const total = {
|
|
623
|
-
|
|
892
|
+
const total = {
|
|
893
|
+
kind: "column",
|
|
894
|
+
reference: `${flagsAlias}.${correlationCountAlias}`,
|
|
895
|
+
};
|
|
896
|
+
const nonNull = {
|
|
897
|
+
kind: "column",
|
|
898
|
+
reference: `${flagsAlias}.${correlationNonNullCountAlias}`,
|
|
899
|
+
};
|
|
624
900
|
const safe = {
|
|
625
901
|
kind: "logical",
|
|
626
902
|
operator: "or",
|
|
@@ -648,6 +924,265 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
648
924
|
right: { kind: "literal", value: true },
|
|
649
925
|
};
|
|
650
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* Range-correlated NOT IN uses a general anti-join for exact matches, then joins back one flag row
|
|
929
|
+
* per distinct outer probe tuple. The counts preserve empty-set and NULL poisoning semantics.
|
|
930
|
+
*/
|
|
931
|
+
function decorrelateRangeNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
932
|
+
const valuesAlias = nextAlias();
|
|
933
|
+
const values = {
|
|
934
|
+
sql: "(range-correlated not in values)",
|
|
935
|
+
base: structuredClone(inner.base),
|
|
936
|
+
joins: structuredClone(inner.joins),
|
|
937
|
+
select: [
|
|
938
|
+
...keys.map((key, index) => ({
|
|
939
|
+
expression: structuredClone(key.inner),
|
|
940
|
+
alias: correlationKeyAlias(index),
|
|
941
|
+
})),
|
|
942
|
+
{ expression: structuredClone(item), alias: correlationValueAlias },
|
|
943
|
+
],
|
|
944
|
+
predicates: structuredClone(inner.predicates),
|
|
945
|
+
groupBy: [...keys.map((key) => structuredClone(key.inner)), structuredClone(item)],
|
|
946
|
+
having: [],
|
|
947
|
+
orderBy: [],
|
|
948
|
+
};
|
|
949
|
+
const exactMatch = generalCorrelationJoin(valuesAlias, values, [
|
|
950
|
+
...keys.map((_, index) => ({
|
|
951
|
+
kind: "column",
|
|
952
|
+
reference: `${valuesAlias}.${correlationKeyAlias(index)}`,
|
|
953
|
+
})),
|
|
954
|
+
{ kind: "column", reference: `${valuesAlias}.${correlationValueAlias}` },
|
|
955
|
+
], [...keys.map((key) => structuredClone(key.outer)), structuredClone(probe)], [...keys.map((key) => key.operator), "="]);
|
|
956
|
+
exactMatch.kind = "anti";
|
|
957
|
+
block.joins.push(exactMatch);
|
|
958
|
+
const probes = outerProbeBlock(block, keys);
|
|
959
|
+
const probesAlias = nextAlias();
|
|
960
|
+
const innerRowsAlias = nextAlias();
|
|
961
|
+
const innerRows = {
|
|
962
|
+
sql: "(range-correlated not in rows)",
|
|
963
|
+
base: inner.base,
|
|
964
|
+
joins: inner.joins,
|
|
965
|
+
select: [
|
|
966
|
+
...keys.map((key, index) => ({
|
|
967
|
+
expression: key.inner,
|
|
968
|
+
alias: correlationKeyAlias(index),
|
|
969
|
+
})),
|
|
970
|
+
{ expression: item, alias: correlationValueAlias },
|
|
971
|
+
],
|
|
972
|
+
predicates: inner.predicates,
|
|
973
|
+
groupBy: [],
|
|
974
|
+
having: [],
|
|
975
|
+
orderBy: [],
|
|
976
|
+
};
|
|
977
|
+
const probeReferences = keys.map((_, index) => ({
|
|
978
|
+
kind: "column",
|
|
979
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
980
|
+
}));
|
|
981
|
+
const flags = {
|
|
982
|
+
sql: "(range-correlated not in flags)",
|
|
983
|
+
base: { table: probesAlias, alias: probesAlias, derived: probes },
|
|
984
|
+
joins: [
|
|
985
|
+
generalCorrelationJoin(innerRowsAlias, innerRows, keys.map((_, index) => ({
|
|
986
|
+
kind: "column",
|
|
987
|
+
reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
|
|
988
|
+
})), probeReferences, keys.map(({ operator }) => operator)),
|
|
989
|
+
],
|
|
990
|
+
select: [
|
|
991
|
+
...probeReferences.map((expression, index) => ({
|
|
992
|
+
expression,
|
|
993
|
+
alias: correlationKeyAlias(index),
|
|
994
|
+
})),
|
|
995
|
+
{
|
|
996
|
+
expression: { kind: "call", name: "COUNT", arguments: [{ kind: "wildcard" }] },
|
|
997
|
+
alias: correlationCountAlias,
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
expression: {
|
|
1001
|
+
kind: "call",
|
|
1002
|
+
name: "COUNT",
|
|
1003
|
+
arguments: [{ kind: "column", reference: `${innerRowsAlias}.${correlationValueAlias}` }],
|
|
1004
|
+
},
|
|
1005
|
+
alias: correlationNonNullCountAlias,
|
|
1006
|
+
},
|
|
1007
|
+
],
|
|
1008
|
+
predicates: [],
|
|
1009
|
+
groupBy: probeReferences,
|
|
1010
|
+
having: [],
|
|
1011
|
+
orderBy: [],
|
|
1012
|
+
};
|
|
1013
|
+
const flagsAlias = nextAlias();
|
|
1014
|
+
pushCorrelationJoin(block, flagsAlias, flags, keys.map((key) => ({ ...key, operator: "=" })), "left");
|
|
1015
|
+
const total = {
|
|
1016
|
+
kind: "column",
|
|
1017
|
+
reference: `${flagsAlias}.${correlationCountAlias}`,
|
|
1018
|
+
};
|
|
1019
|
+
const nonNull = {
|
|
1020
|
+
kind: "column",
|
|
1021
|
+
reference: `${flagsAlias}.${correlationNonNullCountAlias}`,
|
|
1022
|
+
};
|
|
1023
|
+
return {
|
|
1024
|
+
left: {
|
|
1025
|
+
kind: "logical",
|
|
1026
|
+
operator: "or",
|
|
1027
|
+
left: {
|
|
1028
|
+
kind: "condition",
|
|
1029
|
+
operator: "IS NULL",
|
|
1030
|
+
left: total,
|
|
1031
|
+
right: { kind: "literal", value: null },
|
|
1032
|
+
},
|
|
1033
|
+
right: {
|
|
1034
|
+
kind: "logical",
|
|
1035
|
+
operator: "and",
|
|
1036
|
+
left: {
|
|
1037
|
+
kind: "condition",
|
|
1038
|
+
operator: "IS NOT NULL",
|
|
1039
|
+
left: structuredClone(probe),
|
|
1040
|
+
right: { kind: "literal", value: null },
|
|
1041
|
+
},
|
|
1042
|
+
right: { kind: "condition", operator: "=", left: total, right: nonNull },
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
operator: "IS TRUE",
|
|
1046
|
+
right: { kind: "literal", value: true },
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Produces the three-valued result of a correlated IN/ANY/ALL expression at any expression
|
|
1051
|
+
* position. Distinct outer probe tuples join the correlated set once, and grouped counts retain
|
|
1052
|
+
* the difference between true, false, unknown, and an empty set. Top-level WHERE predicates keep
|
|
1053
|
+
* their cheaper semi/anti-join rewrites; this path exists for OR, NOT, CASE, and select items.
|
|
1054
|
+
*/
|
|
1055
|
+
function decorrelateQuantifiedExpression(block, probe, inner, quantified, scope, nextAlias, label) {
|
|
1056
|
+
rejectGroupedInner(inner, label);
|
|
1057
|
+
const item = inner.select[0];
|
|
1058
|
+
if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
|
|
1059
|
+
throw new TypeError(label === "IN"
|
|
1060
|
+
? "An IN subquery must select exactly one column"
|
|
1061
|
+
: "An ANY/ALL subquery must select exactly one column");
|
|
1062
|
+
}
|
|
1063
|
+
const keys = extractCorrelation(inner, scope, label, true);
|
|
1064
|
+
const outerExpressions = [
|
|
1065
|
+
...keys.map((key) => structuredClone(key.outer)),
|
|
1066
|
+
structuredClone(probe),
|
|
1067
|
+
];
|
|
1068
|
+
const probes = outerProbeBlockForExpressions(block, outerExpressions);
|
|
1069
|
+
const probesAlias = nextAlias();
|
|
1070
|
+
const probeReferences = outerExpressions.map((_, index) => ({
|
|
1071
|
+
kind: "column",
|
|
1072
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
1073
|
+
}));
|
|
1074
|
+
const innerRowsAlias = nextAlias();
|
|
1075
|
+
const innerRows = {
|
|
1076
|
+
sql: `(nested correlated ${label.toLowerCase()} rows)`,
|
|
1077
|
+
base: inner.base,
|
|
1078
|
+
joins: inner.joins,
|
|
1079
|
+
select: [
|
|
1080
|
+
...keys.map((key, index) => ({
|
|
1081
|
+
expression: key.inner,
|
|
1082
|
+
alias: correlationKeyAlias(index),
|
|
1083
|
+
})),
|
|
1084
|
+
{ expression: item.expression, alias: correlationValueAlias },
|
|
1085
|
+
{ expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
|
|
1086
|
+
],
|
|
1087
|
+
predicates: inner.predicates,
|
|
1088
|
+
groupBy: [],
|
|
1089
|
+
having: [],
|
|
1090
|
+
orderBy: [],
|
|
1091
|
+
};
|
|
1092
|
+
const setJoin = generalCorrelationJoin(innerRowsAlias, innerRows, keys.map((_, index) => ({
|
|
1093
|
+
kind: "column",
|
|
1094
|
+
reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
|
|
1095
|
+
})), probeReferences.slice(0, keys.length), keys.map(({ operator }) => operator));
|
|
1096
|
+
setJoin.kind = "left";
|
|
1097
|
+
const comparison = {
|
|
1098
|
+
kind: "condition",
|
|
1099
|
+
operator: quantified.comparison,
|
|
1100
|
+
left: probeReferences[keys.length] ?? { kind: "literal", value: null },
|
|
1101
|
+
right: { kind: "column", reference: `${innerRowsAlias}.${correlationValueAlias}` },
|
|
1102
|
+
};
|
|
1103
|
+
const decisiveComparison = quantified.quantifier === "any"
|
|
1104
|
+
? structuredClone(comparison)
|
|
1105
|
+
: { kind: "not", operand: structuredClone(comparison) };
|
|
1106
|
+
const truthMarker = {
|
|
1107
|
+
kind: "case",
|
|
1108
|
+
branches: [{ when: decisiveComparison, then: { kind: "literal", value: 1 } }],
|
|
1109
|
+
otherwise: { kind: "literal", value: null },
|
|
1110
|
+
};
|
|
1111
|
+
const flags = {
|
|
1112
|
+
sql: `(nested correlated ${label.toLowerCase()} flags)`,
|
|
1113
|
+
base: { table: probesAlias, alias: probesAlias, derived: probes },
|
|
1114
|
+
joins: [setJoin],
|
|
1115
|
+
select: [
|
|
1116
|
+
...probeReferences.map((expression, index) => ({
|
|
1117
|
+
expression,
|
|
1118
|
+
alias: correlationKeyAlias(index),
|
|
1119
|
+
})),
|
|
1120
|
+
{
|
|
1121
|
+
expression: {
|
|
1122
|
+
kind: "call",
|
|
1123
|
+
name: "COUNT",
|
|
1124
|
+
arguments: [{ kind: "column", reference: `${innerRowsAlias}.${correlationMarkerAlias}` }],
|
|
1125
|
+
},
|
|
1126
|
+
alias: correlationCountAlias,
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
expression: { kind: "call", name: "COUNT", arguments: [comparison] },
|
|
1130
|
+
alias: correlationDecisionCountAlias,
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
expression: { kind: "call", name: "COUNT", arguments: [truthMarker] },
|
|
1134
|
+
alias: correlationTruthCountAlias,
|
|
1135
|
+
},
|
|
1136
|
+
],
|
|
1137
|
+
predicates: [],
|
|
1138
|
+
groupBy: probeReferences,
|
|
1139
|
+
having: [],
|
|
1140
|
+
orderBy: [],
|
|
1141
|
+
};
|
|
1142
|
+
const flagsAlias = nextAlias();
|
|
1143
|
+
pushNullSafeProbeJoin(block, flagsAlias, flags, outerExpressions);
|
|
1144
|
+
const total = {
|
|
1145
|
+
kind: "column",
|
|
1146
|
+
reference: `${flagsAlias}.${correlationCountAlias}`,
|
|
1147
|
+
};
|
|
1148
|
+
const decisions = {
|
|
1149
|
+
kind: "column",
|
|
1150
|
+
reference: `${flagsAlias}.${correlationDecisionCountAlias}`,
|
|
1151
|
+
};
|
|
1152
|
+
const truths = {
|
|
1153
|
+
kind: "column",
|
|
1154
|
+
reference: `${flagsAlias}.${correlationTruthCountAlias}`,
|
|
1155
|
+
};
|
|
1156
|
+
const defaultValue = quantified.quantifier === "all";
|
|
1157
|
+
return {
|
|
1158
|
+
kind: "case",
|
|
1159
|
+
branches: [
|
|
1160
|
+
{
|
|
1161
|
+
when: {
|
|
1162
|
+
kind: "condition",
|
|
1163
|
+
operator: ">",
|
|
1164
|
+
left: truths,
|
|
1165
|
+
right: { kind: "literal", value: 0 },
|
|
1166
|
+
},
|
|
1167
|
+
then: { kind: "literal", value: !defaultValue },
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
when: {
|
|
1171
|
+
kind: "condition",
|
|
1172
|
+
operator: "=",
|
|
1173
|
+
left: total,
|
|
1174
|
+
right: { kind: "literal", value: 0 },
|
|
1175
|
+
},
|
|
1176
|
+
then: { kind: "literal", value: defaultValue },
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
when: { kind: "condition", operator: "<", left: decisions, right: total },
|
|
1180
|
+
then: { kind: "literal", value: null },
|
|
1181
|
+
},
|
|
1182
|
+
],
|
|
1183
|
+
otherwise: { kind: "literal", value: defaultValue },
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
651
1186
|
/**
|
|
652
1187
|
* Rewrites one correlated scalar-aggregate subquery into a left join against the grouped
|
|
653
1188
|
* aggregate, returning the expression that replaces the subquery node (COUNT wraps in COALESCE
|
|
@@ -655,27 +1190,41 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
655
1190
|
*/
|
|
656
1191
|
function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
|
|
657
1192
|
const inner = subquery.block;
|
|
1193
|
+
const passthrough = scalarPassthrough(inner);
|
|
1194
|
+
if (passthrough !== undefined)
|
|
1195
|
+
return passthrough;
|
|
658
1196
|
const item = inner.select[0];
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (inner.groupBy.length > 0 ||
|
|
1197
|
+
if (inner.select.length !== 1 ||
|
|
1198
|
+
item === undefined ||
|
|
1199
|
+
!isAggregateCall(item.expression) ||
|
|
1200
|
+
inner.groupBy.length > 0 ||
|
|
664
1201
|
inner.having.length > 0 ||
|
|
665
1202
|
inner.orderBy.length > 0 ||
|
|
666
|
-
inner.limit !== undefined
|
|
667
|
-
|
|
1203
|
+
inner.limit !== undefined ||
|
|
1204
|
+
inner.offset !== undefined ||
|
|
1205
|
+
!canExtractCorrelation(inner, scope, "scalar", true)) {
|
|
1206
|
+
return decorrelateGeneralScalar(block, inner, scope, nextAlias);
|
|
1207
|
+
}
|
|
1208
|
+
const previewKeys = extractCorrelation(structuredClone(inner), scope, "scalar", true);
|
|
1209
|
+
if (previewKeys.some((key) => key.operator !== "=") &&
|
|
1210
|
+
(item.expression.arguments.length !== 1 || item.expression.aggregateOrderBy !== undefined)) {
|
|
1211
|
+
return decorrelateGeneralScalar(block, inner, scope, nextAlias);
|
|
1212
|
+
}
|
|
1213
|
+
const keys = extractCorrelation(inner, scope, "scalar", true);
|
|
1214
|
+
if (keys.some((key) => key.operator !== "=")) {
|
|
1215
|
+
return decorrelateNonEqualityScalar(block, inner, item.expression, keys, nextAlias);
|
|
668
1216
|
}
|
|
669
|
-
guardWildcard(block);
|
|
670
|
-
const keys = extractCorrelation(inner, scope, "scalar");
|
|
671
1217
|
const alias = nextAlias();
|
|
672
1218
|
const derived = {
|
|
673
1219
|
sql: "(correlated scalar)",
|
|
674
1220
|
base: inner.base,
|
|
675
1221
|
joins: inner.joins,
|
|
676
1222
|
select: [
|
|
677
|
-
...keys.map((key, index) => ({
|
|
678
|
-
|
|
1223
|
+
...keys.map((key, index) => ({
|
|
1224
|
+
expression: key.inner,
|
|
1225
|
+
alias: correlationKeyAlias(index),
|
|
1226
|
+
})),
|
|
1227
|
+
{ expression: item.expression, alias: correlationValueAlias },
|
|
679
1228
|
],
|
|
680
1229
|
predicates: inner.predicates,
|
|
681
1230
|
groupBy: keys.map((key) => key.inner),
|
|
@@ -683,18 +1232,730 @@ function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
|
|
|
683
1232
|
orderBy: [],
|
|
684
1233
|
};
|
|
685
1234
|
pushCorrelationJoin(block, alias, derived, keys, "left");
|
|
686
|
-
let value = {
|
|
687
|
-
|
|
1235
|
+
let value = {
|
|
1236
|
+
kind: "column",
|
|
1237
|
+
reference: `${alias}.${correlationValueAlias}`,
|
|
1238
|
+
};
|
|
1239
|
+
if (item.expression.name === "COUNT") {
|
|
688
1240
|
// COUNT over an empty group is 0, but the unmatched left-join side is NULL.
|
|
689
1241
|
value = { kind: "call", name: "COALESCE", arguments: [value, { kind: "literal", value: 0 }] };
|
|
690
1242
|
}
|
|
691
1243
|
return value;
|
|
692
1244
|
}
|
|
1245
|
+
/** A SELECT without a row source is already the scalar expression from its enclosing scope. */
|
|
1246
|
+
function scalarPassthrough(inner) {
|
|
1247
|
+
const item = inner.select[0];
|
|
1248
|
+
if (inner.base.table !== DUAL_TABLE ||
|
|
1249
|
+
inner.base.derived !== undefined ||
|
|
1250
|
+
inner.joins.length > 0 ||
|
|
1251
|
+
inner.select.length !== 1 ||
|
|
1252
|
+
item === undefined ||
|
|
1253
|
+
inner.predicates.length > 0 ||
|
|
1254
|
+
inner.groupBy.length > 0 ||
|
|
1255
|
+
inner.having.length > 0 ||
|
|
1256
|
+
inner.orderBy.length > 0 ||
|
|
1257
|
+
inner.offset !== undefined ||
|
|
1258
|
+
(inner.limit !== undefined && inner.limit < 1)) {
|
|
1259
|
+
return undefined;
|
|
1260
|
+
}
|
|
1261
|
+
return structuredClone(item.expression);
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Pulls a scalar expression through projection-only derived wrappers. The parser uses these
|
|
1265
|
+
* wrappers to hide ORDER BY support columns, and Kysely's JSON helpers use one to turn an
|
|
1266
|
+
* explicitly selected row into JSON. Row-affecting clauses remain on the deepest block, where
|
|
1267
|
+
* the probe rewrite can apply them once per outer key.
|
|
1268
|
+
*/
|
|
1269
|
+
function scalarShape(inner) {
|
|
1270
|
+
const item = inner.select[0];
|
|
1271
|
+
if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
|
|
1272
|
+
throw new TypeError("A scalar subquery must select exactly one column");
|
|
1273
|
+
}
|
|
1274
|
+
let rows = inner;
|
|
1275
|
+
let expression = structuredClone(item.expression);
|
|
1276
|
+
for (;;) {
|
|
1277
|
+
const derived = rows.base.derived;
|
|
1278
|
+
if (derived === undefined ||
|
|
1279
|
+
rows.joins.length > 0 ||
|
|
1280
|
+
rows.predicates.length > 0 ||
|
|
1281
|
+
rows.groupBy.length > 0 ||
|
|
1282
|
+
rows.having.length > 0 ||
|
|
1283
|
+
rows.orderBy.length > 0 ||
|
|
1284
|
+
rows.limit !== undefined ||
|
|
1285
|
+
rows.limitParameter !== undefined ||
|
|
1286
|
+
rows.offset !== undefined ||
|
|
1287
|
+
rows.offsetParameter !== undefined ||
|
|
1288
|
+
rows.distinctWildcard === true) {
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
expression = inlineDerivedProjection(expression, rows.base.alias, derived);
|
|
1292
|
+
rows = derived;
|
|
1293
|
+
}
|
|
1294
|
+
return { rows, expression };
|
|
1295
|
+
}
|
|
1296
|
+
/** Replaces references to a derived row with the expressions that produced its named columns. */
|
|
1297
|
+
function inlineDerivedProjection(expression, alias, derived) {
|
|
1298
|
+
const rewrite = (node) => {
|
|
1299
|
+
if (node.kind === "column") {
|
|
1300
|
+
const parts = node.reference.split(".");
|
|
1301
|
+
if (parts.length === 2 && parts[0] !== alias)
|
|
1302
|
+
return node;
|
|
1303
|
+
const name = parts.length === 2 ? (parts[1] ?? "") : (parts[0] ?? "");
|
|
1304
|
+
const matches = derived.select.filter((item) => item.alias === name);
|
|
1305
|
+
if (matches.length === 1)
|
|
1306
|
+
return structuredClone(matches[0]?.expression ?? node);
|
|
1307
|
+
return node;
|
|
1308
|
+
}
|
|
1309
|
+
return mapChildExpressions(node, rewrite);
|
|
1310
|
+
};
|
|
1311
|
+
return rewrite(expression);
|
|
1312
|
+
}
|
|
1313
|
+
/** ORDER BY may name a hidden or visible select alias; recover its row-level expression. */
|
|
1314
|
+
function scalarOrderExpression(expression, rows) {
|
|
1315
|
+
if (expression.kind !== "column" || expression.reference.includes(".")) {
|
|
1316
|
+
return structuredClone(expression);
|
|
1317
|
+
}
|
|
1318
|
+
const matches = rows.select.filter((item) => item.alias === expression.reference);
|
|
1319
|
+
return matches.length === 1
|
|
1320
|
+
? structuredClone(matches[0]?.expression ?? expression)
|
|
1321
|
+
: structuredClone(expression);
|
|
1322
|
+
}
|
|
1323
|
+
/** The value a scalar aggregate expression produces for an empty correlated input. */
|
|
1324
|
+
function emptyScalarExpression(expression) {
|
|
1325
|
+
if (isAggregateCall(expression)) {
|
|
1326
|
+
return { kind: "literal", value: expression.name === "COUNT" ? 0 : null };
|
|
1327
|
+
}
|
|
1328
|
+
if (expression.kind === "column" || expression.kind === "wildcard") {
|
|
1329
|
+
return { kind: "literal", value: null };
|
|
1330
|
+
}
|
|
1331
|
+
return mapChildExpressions(expression, emptyScalarExpression);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* General correlated scalar rewrite. Distinct outer probe tuples join the inner rows once,
|
|
1335
|
+
* ORDER BY/LIMIT rank within each probe, and one grouped result joins back by NULL-safe equality.
|
|
1336
|
+
* Aggregate expressions retain their empty-input rules; ordinary scalar rows use the internal
|
|
1337
|
+
* single-value aggregate, which raises the same cardinality error as an uncorrelated scalar.
|
|
1338
|
+
*/
|
|
1339
|
+
function decorrelateGeneralScalar(block, inner, scope, nextAlias) {
|
|
1340
|
+
const shape = scalarShape(inner);
|
|
1341
|
+
const rows = shape.rows;
|
|
1342
|
+
if (rows.groupBy.length > 0 || rows.having.length > 0) {
|
|
1343
|
+
throw new TypeError("A correlated scalar subquery cannot use GROUP BY or HAVING");
|
|
1344
|
+
}
|
|
1345
|
+
if (rows.limitWithTies === true && rows.orderBy.length === 0) {
|
|
1346
|
+
throw new TypeError("A correlated scalar WITH TIES query requires ORDER BY");
|
|
1347
|
+
}
|
|
1348
|
+
const keys = extractCorrelation(rows, scope, "scalar", true);
|
|
1349
|
+
const outerExpressions = keys.map((key) => structuredClone(key.outer));
|
|
1350
|
+
const probes = outerProbeBlockForExpressions(block, outerExpressions);
|
|
1351
|
+
const probesAlias = nextAlias();
|
|
1352
|
+
const probeReferences = outerExpressions.map((_, index) => ({
|
|
1353
|
+
kind: "column",
|
|
1354
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
1355
|
+
}));
|
|
1356
|
+
const finalRowsAlias = nextAlias();
|
|
1357
|
+
const projected = [];
|
|
1358
|
+
const project = (expression, alias) => {
|
|
1359
|
+
projected.push({ expression: structuredClone(expression), alias });
|
|
1360
|
+
return { kind: "column", reference: `${finalRowsAlias}.${alias}` };
|
|
1361
|
+
};
|
|
1362
|
+
const orderInputs = rows.orderBy.map((order, index) => ({
|
|
1363
|
+
reference: project(scalarOrderExpression(order.expression, rows), correlationOrderAlias(index)),
|
|
1364
|
+
direction: order.direction,
|
|
1365
|
+
...(order.nulls === undefined ? {} : { nulls: order.nulls }),
|
|
1366
|
+
}));
|
|
1367
|
+
let inputSequence = 0;
|
|
1368
|
+
const aggregateResult = (expression) => {
|
|
1369
|
+
if (isAggregateCall(expression)) {
|
|
1370
|
+
const arguments_ = expression.arguments.map((argument) => argument.kind === "wildcard"
|
|
1371
|
+
? structuredClone(argument)
|
|
1372
|
+
: project(argument, correlationInputAlias(inputSequence++)));
|
|
1373
|
+
const ownOrder = expression.aggregateOrderBy?.map((order) => ({
|
|
1374
|
+
expression: project(order.expression, correlationInputAlias(inputSequence++)),
|
|
1375
|
+
direction: order.direction,
|
|
1376
|
+
...(order.nulls === undefined ? {} : { nulls: order.nulls }),
|
|
1377
|
+
}));
|
|
1378
|
+
const inheritedOrder = expression.name === "JSON_ARRAYAGG" || expression.name === "STRING_AGG"
|
|
1379
|
+
? orderInputs.map(({ reference, direction, nulls }) => ({
|
|
1380
|
+
expression: structuredClone(reference),
|
|
1381
|
+
direction,
|
|
1382
|
+
...(nulls === undefined ? {} : { nulls }),
|
|
1383
|
+
}))
|
|
1384
|
+
: undefined;
|
|
1385
|
+
const { aggregateOrderBy: ignoredOrder, ...base } = expression;
|
|
1386
|
+
void ignoredOrder;
|
|
1387
|
+
const aggregateOrder = ownOrder !== undefined && ownOrder.length > 0
|
|
1388
|
+
? ownOrder
|
|
1389
|
+
: inheritedOrder !== undefined && inheritedOrder.length > 0
|
|
1390
|
+
? inheritedOrder
|
|
1391
|
+
: undefined;
|
|
1392
|
+
const rewritten = {
|
|
1393
|
+
...base,
|
|
1394
|
+
arguments: arguments_,
|
|
1395
|
+
...(aggregateOrder === undefined ? {} : { aggregateOrderBy: aggregateOrder }),
|
|
1396
|
+
};
|
|
1397
|
+
return rewritten;
|
|
1398
|
+
}
|
|
1399
|
+
if (expression.kind === "column" || expression.kind === "wildcard") {
|
|
1400
|
+
throw new TypeError("A correlated scalar aggregate cannot select an ungrouped inner column");
|
|
1401
|
+
}
|
|
1402
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
1403
|
+
throw new TypeError("A correlated scalar aggregate cannot contain another subquery");
|
|
1404
|
+
}
|
|
1405
|
+
return mapChildExpressions(expression, aggregateResult);
|
|
1406
|
+
};
|
|
1407
|
+
const grouped = hasAggregate(shape.expression);
|
|
1408
|
+
const valueExpression = grouped
|
|
1409
|
+
? aggregateResult(shape.expression)
|
|
1410
|
+
: {
|
|
1411
|
+
kind: "call",
|
|
1412
|
+
name: "MINNOW_SINGLE_VALUE",
|
|
1413
|
+
arguments: [project(shape.expression, correlationValueAlias)],
|
|
1414
|
+
};
|
|
1415
|
+
const innerRowsAlias = nextAlias();
|
|
1416
|
+
const rawRows = {
|
|
1417
|
+
sql: "(correlated scalar input rows)",
|
|
1418
|
+
base: rows.base,
|
|
1419
|
+
joins: rows.joins,
|
|
1420
|
+
select: [
|
|
1421
|
+
...keys.map((key, index) => ({
|
|
1422
|
+
expression: key.inner,
|
|
1423
|
+
alias: correlationKeyAlias(index),
|
|
1424
|
+
})),
|
|
1425
|
+
...projected,
|
|
1426
|
+
],
|
|
1427
|
+
predicates: rows.predicates,
|
|
1428
|
+
groupBy: [],
|
|
1429
|
+
having: [],
|
|
1430
|
+
orderBy: [],
|
|
1431
|
+
};
|
|
1432
|
+
const matched = {
|
|
1433
|
+
sql: "(correlated scalar matched probes)",
|
|
1434
|
+
base: { table: probesAlias, alias: probesAlias, derived: probes },
|
|
1435
|
+
joins: [
|
|
1436
|
+
generalCorrelationJoin(innerRowsAlias, rawRows, keys.map((_, index) => ({
|
|
1437
|
+
kind: "column",
|
|
1438
|
+
reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
|
|
1439
|
+
})), probeReferences, keys.map(({ operator }) => operator)),
|
|
1440
|
+
],
|
|
1441
|
+
select: [
|
|
1442
|
+
...probeReferences.map((expression, index) => ({
|
|
1443
|
+
expression,
|
|
1444
|
+
alias: correlationKeyAlias(index),
|
|
1445
|
+
})),
|
|
1446
|
+
...projected.map(({ alias }) => ({
|
|
1447
|
+
expression: { kind: "column", reference: `${innerRowsAlias}.${alias}` },
|
|
1448
|
+
alias,
|
|
1449
|
+
})),
|
|
1450
|
+
],
|
|
1451
|
+
predicates: [],
|
|
1452
|
+
groupBy: [],
|
|
1453
|
+
having: [],
|
|
1454
|
+
orderBy: [],
|
|
1455
|
+
};
|
|
1456
|
+
let finalRows = matched;
|
|
1457
|
+
if (rows.limit !== undefined ||
|
|
1458
|
+
rows.limitParameter !== undefined ||
|
|
1459
|
+
rows.offset !== undefined ||
|
|
1460
|
+
rows.offsetParameter !== undefined) {
|
|
1461
|
+
const rankedAlias = nextAlias();
|
|
1462
|
+
const rankedTable = nextAlias();
|
|
1463
|
+
const ranked = {
|
|
1464
|
+
table: rankedTable,
|
|
1465
|
+
alias: rankedAlias,
|
|
1466
|
+
windowed: {
|
|
1467
|
+
block: matched,
|
|
1468
|
+
windows: [
|
|
1469
|
+
{
|
|
1470
|
+
alias: correlationRowNumberAlias,
|
|
1471
|
+
name: rows.limitWithTies === true ? "RANK" : "ROW_NUMBER",
|
|
1472
|
+
partitionAliases: keys.map((_, index) => correlationKeyAlias(index)),
|
|
1473
|
+
orderAliases: rows.orderBy.map((order, index) => ({
|
|
1474
|
+
alias: correlationOrderAlias(index),
|
|
1475
|
+
direction: order.direction,
|
|
1476
|
+
...(order.nulls === undefined ? {} : { nulls: order.nulls }),
|
|
1477
|
+
})),
|
|
1478
|
+
},
|
|
1479
|
+
],
|
|
1480
|
+
},
|
|
1481
|
+
};
|
|
1482
|
+
const rowNumber = {
|
|
1483
|
+
kind: "column",
|
|
1484
|
+
reference: `${rankedAlias}.${correlationRowNumberAlias}`,
|
|
1485
|
+
};
|
|
1486
|
+
const offset = rows.offsetParameter === undefined
|
|
1487
|
+
? { kind: "literal", value: rows.offset ?? 0 }
|
|
1488
|
+
: { kind: "parameter", index: rows.offsetParameter };
|
|
1489
|
+
const predicates = [];
|
|
1490
|
+
if ((rows.offset ?? 0) > 0 || rows.offsetParameter !== undefined) {
|
|
1491
|
+
predicates.push({
|
|
1492
|
+
left: rowNumber,
|
|
1493
|
+
operator: ">",
|
|
1494
|
+
right: offset,
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
if (rows.limit !== undefined || rows.limitParameter !== undefined) {
|
|
1498
|
+
const limit = rows.limitParameter === undefined
|
|
1499
|
+
? { kind: "literal", value: rows.limit ?? 0 }
|
|
1500
|
+
: { kind: "parameter", index: rows.limitParameter };
|
|
1501
|
+
predicates.push({
|
|
1502
|
+
left: structuredClone(rowNumber),
|
|
1503
|
+
operator: "<=",
|
|
1504
|
+
right: rows.offset === undefined && rows.offsetParameter === undefined
|
|
1505
|
+
? limit
|
|
1506
|
+
: { kind: "binary", operator: "+", left: structuredClone(offset), right: limit },
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
finalRows = {
|
|
1510
|
+
sql: "(correlated scalar limited rows)",
|
|
1511
|
+
base: ranked,
|
|
1512
|
+
joins: [],
|
|
1513
|
+
select: [
|
|
1514
|
+
...keys.map((_, index) => ({
|
|
1515
|
+
expression: {
|
|
1516
|
+
kind: "column",
|
|
1517
|
+
reference: `${rankedAlias}.${correlationKeyAlias(index)}`,
|
|
1518
|
+
},
|
|
1519
|
+
alias: correlationKeyAlias(index),
|
|
1520
|
+
})),
|
|
1521
|
+
...projected.map(({ alias }) => ({
|
|
1522
|
+
expression: { kind: "column", reference: `${rankedAlias}.${alias}` },
|
|
1523
|
+
alias,
|
|
1524
|
+
})),
|
|
1525
|
+
],
|
|
1526
|
+
predicates,
|
|
1527
|
+
groupBy: [],
|
|
1528
|
+
having: [],
|
|
1529
|
+
orderBy: [],
|
|
1530
|
+
...(rows.limitParameter === undefined
|
|
1531
|
+
? {}
|
|
1532
|
+
: { limitValidationParameters: [rows.limitParameter] }),
|
|
1533
|
+
...(rows.offsetParameter === undefined
|
|
1534
|
+
? {}
|
|
1535
|
+
: { offsetValidationParameters: [rows.offsetParameter] }),
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const finalProbeReferences = keys.map((_, index) => ({
|
|
1539
|
+
kind: "column",
|
|
1540
|
+
reference: `${finalRowsAlias}.${correlationKeyAlias(index)}`,
|
|
1541
|
+
}));
|
|
1542
|
+
const aggregate = {
|
|
1543
|
+
sql: "(correlated scalar result)",
|
|
1544
|
+
base: { table: finalRowsAlias, alias: finalRowsAlias, derived: finalRows },
|
|
1545
|
+
joins: [],
|
|
1546
|
+
select: [
|
|
1547
|
+
...finalProbeReferences.map((expression, index) => ({
|
|
1548
|
+
expression,
|
|
1549
|
+
alias: correlationKeyAlias(index),
|
|
1550
|
+
})),
|
|
1551
|
+
{ expression: valueExpression, alias: correlationValueAlias },
|
|
1552
|
+
{ expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
|
|
1553
|
+
],
|
|
1554
|
+
predicates: [],
|
|
1555
|
+
groupBy: finalProbeReferences,
|
|
1556
|
+
having: [],
|
|
1557
|
+
orderBy: [],
|
|
1558
|
+
};
|
|
1559
|
+
const resultAlias = nextAlias();
|
|
1560
|
+
pushNullSafeProbeJoin(block, resultAlias, aggregate, outerExpressions);
|
|
1561
|
+
const marker = {
|
|
1562
|
+
kind: "column",
|
|
1563
|
+
reference: `${resultAlias}.${correlationMarkerAlias}`,
|
|
1564
|
+
};
|
|
1565
|
+
return {
|
|
1566
|
+
kind: "case",
|
|
1567
|
+
branches: [
|
|
1568
|
+
{
|
|
1569
|
+
when: {
|
|
1570
|
+
kind: "condition",
|
|
1571
|
+
operator: "IS NULL",
|
|
1572
|
+
left: marker,
|
|
1573
|
+
right: { kind: "literal", value: null },
|
|
1574
|
+
},
|
|
1575
|
+
then: grouped
|
|
1576
|
+
? emptyScalarExpression(structuredClone(shape.expression))
|
|
1577
|
+
: { kind: "literal", value: null },
|
|
1578
|
+
},
|
|
1579
|
+
],
|
|
1580
|
+
otherwise: {
|
|
1581
|
+
kind: "column",
|
|
1582
|
+
reference: `${resultAlias}.${correlationValueAlias}`,
|
|
1583
|
+
},
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Aggregates a range-correlated scalar subquery once per distinct outer probe tuple. The probe
|
|
1588
|
+
* table prevents a general join from multiplying outer rows and lets the final join use equality
|
|
1589
|
+
* keys even when the original correlation uses `<`, `>=`, or another comparison.
|
|
1590
|
+
*/
|
|
1591
|
+
function decorrelateNonEqualityScalar(block, inner, aggregate, keys, nextAlias) {
|
|
1592
|
+
if (aggregate.aggregateOrderBy !== undefined) {
|
|
1593
|
+
throw new TypeError("A range-correlated scalar aggregate cannot use aggregate ORDER BY");
|
|
1594
|
+
}
|
|
1595
|
+
const argument = aggregate.arguments[0];
|
|
1596
|
+
if (aggregate.arguments.length !== 1 || argument === undefined) {
|
|
1597
|
+
throw new TypeError("A correlated scalar aggregate must have exactly one argument");
|
|
1598
|
+
}
|
|
1599
|
+
const probes = outerProbeBlock(block, keys);
|
|
1600
|
+
const probesAlias = nextAlias();
|
|
1601
|
+
const innerRowsAlias = nextAlias();
|
|
1602
|
+
const innerRows = {
|
|
1603
|
+
sql: "(range-correlated scalar rows)",
|
|
1604
|
+
base: inner.base,
|
|
1605
|
+
joins: inner.joins,
|
|
1606
|
+
select: [
|
|
1607
|
+
...keys.map((key, index) => ({
|
|
1608
|
+
expression: key.inner,
|
|
1609
|
+
alias: correlationKeyAlias(index),
|
|
1610
|
+
})),
|
|
1611
|
+
{
|
|
1612
|
+
expression: argument.kind === "wildcard" ? { kind: "literal", value: 1 } : argument,
|
|
1613
|
+
alias: correlationValueAlias,
|
|
1614
|
+
},
|
|
1615
|
+
],
|
|
1616
|
+
predicates: inner.predicates,
|
|
1617
|
+
groupBy: [],
|
|
1618
|
+
having: [],
|
|
1619
|
+
orderBy: [],
|
|
1620
|
+
};
|
|
1621
|
+
const joinedAggregate = {
|
|
1622
|
+
...aggregate,
|
|
1623
|
+
arguments: [{ kind: "column", reference: `${innerRowsAlias}.${correlationValueAlias}` }],
|
|
1624
|
+
};
|
|
1625
|
+
const probeReferences = keys.map((_, index) => ({
|
|
1626
|
+
kind: "column",
|
|
1627
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
1628
|
+
}));
|
|
1629
|
+
const aggregateBlock = {
|
|
1630
|
+
sql: "(range-correlated scalar aggregate)",
|
|
1631
|
+
base: { table: probesAlias, alias: probesAlias, derived: probes },
|
|
1632
|
+
joins: [
|
|
1633
|
+
generalCorrelationJoin(innerRowsAlias, innerRows, keys.map((_, index) => ({
|
|
1634
|
+
kind: "column",
|
|
1635
|
+
reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
|
|
1636
|
+
})), probeReferences, keys.map(({ operator }) => operator)),
|
|
1637
|
+
],
|
|
1638
|
+
select: [
|
|
1639
|
+
...probeReferences.map((expression, index) => ({
|
|
1640
|
+
expression,
|
|
1641
|
+
alias: correlationKeyAlias(index),
|
|
1642
|
+
})),
|
|
1643
|
+
{ expression: joinedAggregate, alias: correlationValueAlias },
|
|
1644
|
+
],
|
|
1645
|
+
predicates: [],
|
|
1646
|
+
groupBy: probeReferences,
|
|
1647
|
+
having: [],
|
|
1648
|
+
orderBy: [],
|
|
1649
|
+
};
|
|
1650
|
+
const aggregateAlias = nextAlias();
|
|
1651
|
+
pushCorrelationJoin(block, aggregateAlias, aggregateBlock, keys.map((key) => ({ ...key, operator: "=" })), "left");
|
|
1652
|
+
let value = {
|
|
1653
|
+
kind: "column",
|
|
1654
|
+
reference: `${aggregateAlias}.${correlationValueAlias}`,
|
|
1655
|
+
};
|
|
1656
|
+
if (aggregate.name === "COUNT") {
|
|
1657
|
+
value = { kind: "call", name: "COALESCE", arguments: [value, { kind: "literal", value: 0 }] };
|
|
1658
|
+
}
|
|
1659
|
+
return value;
|
|
1660
|
+
}
|
|
1661
|
+
/** Builds the hidden boolean flag used when correlated EXISTS is nested below OR/NOT/CASE. */
|
|
1662
|
+
function decorrelateExistsExpression(block, exists, scope, nextAlias) {
|
|
1663
|
+
const inner = exists.block;
|
|
1664
|
+
rejectGroupedInner(inner, "EXISTS");
|
|
1665
|
+
if (!canExtractCorrelation(inner, scope, "EXISTS", true)) {
|
|
1666
|
+
return decorrelateExistsWithProbes(block, exists, scope, nextAlias);
|
|
1667
|
+
}
|
|
1668
|
+
const keys = extractCorrelation(inner, scope, "EXISTS", true);
|
|
1669
|
+
if (keys.every(({ operator }) => operator === "=")) {
|
|
1670
|
+
const flagsAlias = nextAlias();
|
|
1671
|
+
const flags = {
|
|
1672
|
+
sql: "(nested correlated exists flags)",
|
|
1673
|
+
base: inner.base,
|
|
1674
|
+
joins: inner.joins,
|
|
1675
|
+
select: [
|
|
1676
|
+
...keys.map((key, index) => ({
|
|
1677
|
+
expression: key.inner,
|
|
1678
|
+
alias: correlationKeyAlias(index),
|
|
1679
|
+
})),
|
|
1680
|
+
{ expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
|
|
1681
|
+
],
|
|
1682
|
+
predicates: inner.predicates,
|
|
1683
|
+
// EXISTS only needs one flag per correlation tuple. This is the direct equality-key path
|
|
1684
|
+
// used by auth scopes such as `all_access OR EXISTS (...)` and cannot multiply outer rows.
|
|
1685
|
+
groupBy: keys.map((key) => key.inner),
|
|
1686
|
+
having: [],
|
|
1687
|
+
orderBy: [],
|
|
1688
|
+
};
|
|
1689
|
+
pushCorrelationJoin(block, flagsAlias, flags, keys, "left");
|
|
1690
|
+
return {
|
|
1691
|
+
kind: "condition",
|
|
1692
|
+
operator: exists.negated ? "IS NULL" : "IS NOT NULL",
|
|
1693
|
+
left: { kind: "column", reference: `${flagsAlias}.${correlationMarkerAlias}` },
|
|
1694
|
+
right: { kind: "literal", value: null },
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
const probes = outerProbeBlock(block, keys);
|
|
1698
|
+
const probesAlias = nextAlias();
|
|
1699
|
+
const innerRowsAlias = nextAlias();
|
|
1700
|
+
const innerRows = {
|
|
1701
|
+
sql: "(nested correlated exists rows)",
|
|
1702
|
+
base: inner.base,
|
|
1703
|
+
joins: inner.joins,
|
|
1704
|
+
select: keys.map((key, index) => ({
|
|
1705
|
+
expression: key.inner,
|
|
1706
|
+
alias: correlationKeyAlias(index),
|
|
1707
|
+
})),
|
|
1708
|
+
predicates: inner.predicates,
|
|
1709
|
+
groupBy: keys.map((key) => key.inner),
|
|
1710
|
+
having: [],
|
|
1711
|
+
orderBy: [],
|
|
1712
|
+
};
|
|
1713
|
+
const probeReferences = keys.map((_, index) => ({
|
|
1714
|
+
kind: "column",
|
|
1715
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
1716
|
+
}));
|
|
1717
|
+
const flags = {
|
|
1718
|
+
sql: "(nested correlated exists flags)",
|
|
1719
|
+
base: { table: probesAlias, alias: probesAlias, derived: probes },
|
|
1720
|
+
joins: [
|
|
1721
|
+
generalCorrelationJoin(innerRowsAlias, innerRows, keys.map((_, index) => ({
|
|
1722
|
+
kind: "column",
|
|
1723
|
+
reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
|
|
1724
|
+
})), probeReferences, keys.map(({ operator }) => operator)),
|
|
1725
|
+
],
|
|
1726
|
+
select: [
|
|
1727
|
+
...probeReferences.map((expression, index) => ({
|
|
1728
|
+
expression,
|
|
1729
|
+
alias: correlationKeyAlias(index),
|
|
1730
|
+
})),
|
|
1731
|
+
{ expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
|
|
1732
|
+
],
|
|
1733
|
+
predicates: [],
|
|
1734
|
+
groupBy: probeReferences,
|
|
1735
|
+
having: [],
|
|
1736
|
+
orderBy: [],
|
|
1737
|
+
};
|
|
1738
|
+
const flagsAlias = nextAlias();
|
|
1739
|
+
pushCorrelationJoin(block, flagsAlias, flags, keys.map((key) => ({ ...key, operator: "=" })), "left");
|
|
1740
|
+
return {
|
|
1741
|
+
kind: "condition",
|
|
1742
|
+
operator: exists.negated ? "IS NULL" : "IS NOT NULL",
|
|
1743
|
+
left: { kind: "column", reference: `${flagsAlias}.${correlationMarkerAlias}` },
|
|
1744
|
+
right: { kind: "literal", value: null },
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Decorrelation fallback for an EXISTS whose outer values occur below another subquery or in a
|
|
1749
|
+
* larger expression instead of as direct inner-column/outer-column predicates. Distinct outer
|
|
1750
|
+
* probe tuples become an ordinary derived source of the EXISTS body, and every outer reference
|
|
1751
|
+
* is rebound to that source. The whole nested tree can then use the normal recursive rewrites.
|
|
1752
|
+
*/
|
|
1753
|
+
function decorrelateExistsWithProbes(block, exists, scope, nextAlias) {
|
|
1754
|
+
const inner = exists.block;
|
|
1755
|
+
const outside = [];
|
|
1756
|
+
collectOutsideReferences(inner, new Set(), outside);
|
|
1757
|
+
const references = [...new Set(outside)];
|
|
1758
|
+
if (references.length === 0) {
|
|
1759
|
+
throw new TypeError("Correlated EXISTS subquery has no usable outer reference");
|
|
1760
|
+
}
|
|
1761
|
+
for (const reference of references) {
|
|
1762
|
+
const parts = reference.split(".");
|
|
1763
|
+
const alias = parts.length === 2 ? parts[0] : undefined;
|
|
1764
|
+
if (alias === undefined || !scope.has(alias)) {
|
|
1765
|
+
throw new TypeError(`Correlated EXISTS references an unavailable outer source: ${reference}`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
const outerExpressions = references.map((reference) => ({
|
|
1769
|
+
kind: "column",
|
|
1770
|
+
reference,
|
|
1771
|
+
}));
|
|
1772
|
+
const probes = outerProbeBlockForExpressions(block, outerExpressions);
|
|
1773
|
+
const probesAlias = nextAlias();
|
|
1774
|
+
const probeReferences = references.map((_, index) => ({
|
|
1775
|
+
kind: "column",
|
|
1776
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
1777
|
+
}));
|
|
1778
|
+
replacePlanReferences(inner, new Map(references.map((reference, index) => [reference, probeReferences[index] ?? null])));
|
|
1779
|
+
inner.joins.push({
|
|
1780
|
+
table: probesAlias,
|
|
1781
|
+
alias: probesAlias,
|
|
1782
|
+
derived: probes,
|
|
1783
|
+
kind: "inner",
|
|
1784
|
+
left: { kind: "literal", value: null },
|
|
1785
|
+
right: { kind: "literal", value: null },
|
|
1786
|
+
on: {
|
|
1787
|
+
kind: "condition",
|
|
1788
|
+
operator: "=",
|
|
1789
|
+
left: { kind: "literal", value: 1 },
|
|
1790
|
+
right: { kind: "literal", value: 1 },
|
|
1791
|
+
},
|
|
1792
|
+
});
|
|
1793
|
+
const flagsAlias = nextAlias();
|
|
1794
|
+
const flags = {
|
|
1795
|
+
sql: "(probe-lifted correlated exists flags)",
|
|
1796
|
+
base: inner.base,
|
|
1797
|
+
joins: inner.joins,
|
|
1798
|
+
select: [
|
|
1799
|
+
...probeReferences.map((expression, index) => ({
|
|
1800
|
+
expression,
|
|
1801
|
+
alias: correlationKeyAlias(index),
|
|
1802
|
+
})),
|
|
1803
|
+
{ expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
|
|
1804
|
+
],
|
|
1805
|
+
predicates: inner.predicates,
|
|
1806
|
+
groupBy: probeReferences,
|
|
1807
|
+
having: [],
|
|
1808
|
+
orderBy: [],
|
|
1809
|
+
};
|
|
1810
|
+
const flagKeys = outerExpressions.map((_, index) => ({
|
|
1811
|
+
kind: "column",
|
|
1812
|
+
reference: `${flagsAlias}.${correlationKeyAlias(index)}`,
|
|
1813
|
+
}));
|
|
1814
|
+
const probeConditions = outerExpressions.map((outer, index) => ({
|
|
1815
|
+
kind: "condition",
|
|
1816
|
+
operator: "IS NOT DISTINCT FROM",
|
|
1817
|
+
left: outer,
|
|
1818
|
+
right: flagKeys[index] ?? { kind: "literal", value: null },
|
|
1819
|
+
}));
|
|
1820
|
+
let probeJoin = probeConditions[0];
|
|
1821
|
+
if (probeJoin === undefined)
|
|
1822
|
+
throw new Error("Correlation probe join is missing its key");
|
|
1823
|
+
for (const condition of probeConditions.slice(1)) {
|
|
1824
|
+
probeJoin = {
|
|
1825
|
+
kind: "logical",
|
|
1826
|
+
operator: "and",
|
|
1827
|
+
left: probeJoin,
|
|
1828
|
+
right: condition,
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
block.joins.push({
|
|
1832
|
+
table: flagsAlias,
|
|
1833
|
+
alias: flagsAlias,
|
|
1834
|
+
derived: flags,
|
|
1835
|
+
kind: "left",
|
|
1836
|
+
left: { kind: "literal", value: null },
|
|
1837
|
+
right: { kind: "literal", value: null },
|
|
1838
|
+
on: probeJoin,
|
|
1839
|
+
});
|
|
1840
|
+
return {
|
|
1841
|
+
kind: "condition",
|
|
1842
|
+
operator: exists.negated ? "IS NULL" : "IS NOT NULL",
|
|
1843
|
+
left: { kind: "column", reference: `${flagsAlias}.${correlationMarkerAlias}` },
|
|
1844
|
+
right: { kind: "literal", value: null },
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
/** Rebinds qualified references everywhere below one plan without touching the probe definition. */
|
|
1848
|
+
function replacePlanReferences(block, replacements) {
|
|
1849
|
+
const rewrite = (expression) => {
|
|
1850
|
+
if (expression.kind === "column") {
|
|
1851
|
+
const replacement = replacements.get(expression.reference);
|
|
1852
|
+
return replacement == null ? expression : structuredClone(replacement);
|
|
1853
|
+
}
|
|
1854
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
1855
|
+
replacePlanReferences(expression.block, replacements);
|
|
1856
|
+
return expression;
|
|
1857
|
+
}
|
|
1858
|
+
if (expression.kind === "window") {
|
|
1859
|
+
expression.partitionBy = expression.partitionBy.map(rewrite);
|
|
1860
|
+
for (const order of expression.orderBy)
|
|
1861
|
+
order.expression = rewrite(order.expression);
|
|
1862
|
+
if (expression.argument !== undefined)
|
|
1863
|
+
expression.argument = rewrite(expression.argument);
|
|
1864
|
+
return expression;
|
|
1865
|
+
}
|
|
1866
|
+
return mapChildExpressions(expression, rewrite);
|
|
1867
|
+
};
|
|
1868
|
+
mapBlockExpressions(block, rewrite);
|
|
1869
|
+
forEachNestedBlock(block, (nested) => replacePlanReferences(nested, replacements));
|
|
1870
|
+
}
|
|
1871
|
+
/** Distinct outer key tuples, preserving the FROM joins needed by every referenced source. */
|
|
1872
|
+
function outerProbeBlock(block, keys) {
|
|
1873
|
+
return outerProbeBlockForExpressions(block, keys.map((key) => key.outer));
|
|
1874
|
+
}
|
|
1875
|
+
function outerProbeBlockForExpressions(block, outerExpressions) {
|
|
1876
|
+
const sources = [block.base, ...block.joins];
|
|
1877
|
+
const sourceIndexes = outerExpressions.flatMap((expression) => {
|
|
1878
|
+
const aliases = [...expressionAliases(expression)];
|
|
1879
|
+
// An unqualified expression can resolve against any visible source. Retaining the whole
|
|
1880
|
+
// prefix is conservative and preserves SQL name resolution without a catalog dependency.
|
|
1881
|
+
if (aliases.length === 0)
|
|
1882
|
+
return [sources.length - 1];
|
|
1883
|
+
return aliases.map((alias) => {
|
|
1884
|
+
const index = sources.findIndex((source) => source.alias === alias);
|
|
1885
|
+
if (index < 0)
|
|
1886
|
+
throw new TypeError(`Cannot resolve correlated outer source: ${alias}`);
|
|
1887
|
+
return index;
|
|
1888
|
+
});
|
|
1889
|
+
});
|
|
1890
|
+
const lastSource = Math.max(...sourceIndexes);
|
|
1891
|
+
const expressions = outerExpressions.map((expression) => structuredClone(expression));
|
|
1892
|
+
return {
|
|
1893
|
+
sql: "(correlation probes)",
|
|
1894
|
+
base: structuredClone(block.base),
|
|
1895
|
+
joins: structuredClone(block.joins.slice(0, lastSource)),
|
|
1896
|
+
select: expressions.map((expression, index) => ({
|
|
1897
|
+
expression,
|
|
1898
|
+
alias: correlationProbeAlias(index),
|
|
1899
|
+
})),
|
|
1900
|
+
predicates: [],
|
|
1901
|
+
groupBy: expressions,
|
|
1902
|
+
having: [],
|
|
1903
|
+
orderBy: [],
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
/** Joins one row per outer probe tuple back with NULL-safe equality. */
|
|
1907
|
+
function pushNullSafeProbeJoin(block, alias, derived, outerExpressions) {
|
|
1908
|
+
const conditions = outerExpressions.map((outer, index) => ({
|
|
1909
|
+
kind: "condition",
|
|
1910
|
+
operator: "IS NOT DISTINCT FROM",
|
|
1911
|
+
left: structuredClone(outer),
|
|
1912
|
+
right: { kind: "column", reference: `${alias}.${correlationKeyAlias(index)}` },
|
|
1913
|
+
}));
|
|
1914
|
+
let on = conditions[0];
|
|
1915
|
+
if (on === undefined)
|
|
1916
|
+
throw new Error("Correlation probe join is missing its key");
|
|
1917
|
+
for (const condition of conditions.slice(1)) {
|
|
1918
|
+
on = { kind: "logical", operator: "and", left: on, right: condition };
|
|
1919
|
+
}
|
|
1920
|
+
block.joins.push({
|
|
1921
|
+
table: alias,
|
|
1922
|
+
alias,
|
|
1923
|
+
derived,
|
|
1924
|
+
kind: "left",
|
|
1925
|
+
left: { kind: "literal", value: null },
|
|
1926
|
+
right: { kind: "literal", value: null },
|
|
1927
|
+
on,
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
/** A general ON join carrying one comparison for each corresponding expression pair. */
|
|
1931
|
+
function generalCorrelationJoin(alias, derived, inner, outer, operators) {
|
|
1932
|
+
let on;
|
|
1933
|
+
inner.forEach((left, index) => {
|
|
1934
|
+
const right = outer[index];
|
|
1935
|
+
const operator = operators[index];
|
|
1936
|
+
if (right === undefined || operator === undefined)
|
|
1937
|
+
return;
|
|
1938
|
+
const comparison = { kind: "condition", operator, left, right };
|
|
1939
|
+
on =
|
|
1940
|
+
on === undefined
|
|
1941
|
+
? comparison
|
|
1942
|
+
: { kind: "logical", operator: "and", left: on, right: comparison };
|
|
1943
|
+
});
|
|
1944
|
+
return {
|
|
1945
|
+
table: alias,
|
|
1946
|
+
alias,
|
|
1947
|
+
derived,
|
|
1948
|
+
kind: "inner",
|
|
1949
|
+
left: { kind: "literal", value: null },
|
|
1950
|
+
right: { kind: "literal", value: null },
|
|
1951
|
+
...(on === undefined ? {} : { on }),
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
693
1954
|
function pushCorrelationJoin(block, alias, derived, keys, kind) {
|
|
694
1955
|
const source = { table: alias, alias, derived };
|
|
695
1956
|
const keyReference = (index) => ({
|
|
696
1957
|
kind: "column",
|
|
697
|
-
reference: `${alias}
|
|
1958
|
+
reference: `${alias}.${correlationKeyAlias(index)}`,
|
|
698
1959
|
});
|
|
699
1960
|
const first = keys[0];
|
|
700
1961
|
if (keys.length === 1 && first?.operator === "=") {
|
|
@@ -766,13 +2027,26 @@ function extractCorrelation(inner, outerScope, label, comparisons = false) {
|
|
|
766
2027
|
const leftover = [];
|
|
767
2028
|
collectOutsideReferences(inner, new Set(), leftover);
|
|
768
2029
|
if (leftover.length > 0) {
|
|
769
|
-
|
|
2030
|
+
const supported = comparisons ? "comparison" : "equality";
|
|
2031
|
+
throw new TypeError(`Correlated ${label} subqueries support only ${supported} conditions between one inner and one outer qualified column (unsupported reference: ${leftover[0] ?? ""})`);
|
|
770
2032
|
}
|
|
771
2033
|
if (keys.length === 0) {
|
|
772
2034
|
throw new TypeError(`Correlated ${label} subquery has no usable correlation condition`);
|
|
773
2035
|
}
|
|
774
2036
|
return keys;
|
|
775
2037
|
}
|
|
2038
|
+
/** Whether the fast correlation-key path can consume every outside reference in this block. */
|
|
2039
|
+
function canExtractCorrelation(inner, outerScope, label, comparisons) {
|
|
2040
|
+
try {
|
|
2041
|
+
extractCorrelation(structuredClone(inner), outerScope, label, comparisons);
|
|
2042
|
+
return true;
|
|
2043
|
+
}
|
|
2044
|
+
catch (error) {
|
|
2045
|
+
if (error instanceof TypeError)
|
|
2046
|
+
return false;
|
|
2047
|
+
throw error;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
776
2050
|
function correlationComparison(predicate, innerScope, outerScope, comparisons) {
|
|
777
2051
|
if (predicate.operator !== "=" &&
|
|
778
2052
|
(!comparisons || !comparisonOperators.has(predicate.operator))) {
|
|
@@ -822,11 +2096,6 @@ function rejectGroupedInner(inner, label) {
|
|
|
822
2096
|
throw new TypeError(`Correlated ${label} subqueries cannot use GROUP BY or HAVING`);
|
|
823
2097
|
}
|
|
824
2098
|
}
|
|
825
|
-
function guardWildcard(block) {
|
|
826
|
-
if (block.select.some((item) => item.expression.kind === "wildcard")) {
|
|
827
|
-
throw new TypeError("Correlated subqueries cannot be combined with SELECT *");
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
2099
|
function assertNoCorrelation(block) {
|
|
831
2100
|
const visit = (expression) => {
|
|
832
2101
|
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
@@ -1233,8 +2502,10 @@ function referencedOutputAliases(block, source, singleSource) {
|
|
|
1233
2502
|
if (expression.otherwise !== undefined)
|
|
1234
2503
|
visit(expression.otherwise);
|
|
1235
2504
|
}
|
|
1236
|
-
else if (expression.kind === "call")
|
|
2505
|
+
else if (expression.kind === "call") {
|
|
1237
2506
|
expression.arguments.forEach(visit);
|
|
2507
|
+
expression.aggregateOrderBy?.forEach((order) => visit(order.expression));
|
|
2508
|
+
}
|
|
1238
2509
|
else if (expression.kind === "list")
|
|
1239
2510
|
expression.items.forEach(visit);
|
|
1240
2511
|
else if (expression.kind === "fts") {
|
|
@@ -1263,6 +2534,8 @@ function referencedOutputAliases(block, source, singleSource) {
|
|
|
1263
2534
|
for (const join of block.joins) {
|
|
1264
2535
|
visit(join.left);
|
|
1265
2536
|
visit(join.right);
|
|
2537
|
+
if (join.on !== undefined)
|
|
2538
|
+
visit(join.on);
|
|
1266
2539
|
}
|
|
1267
2540
|
for (const order of block.orderBy) {
|
|
1268
2541
|
// An ORDER BY reference may name an output alias of the outer block rather than a source
|