@minnowdb/core 0.6.0 → 0.6.2
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/artifact-cache.d.ts +5 -0
- package/dist/engine/artifact-cache.js +64 -13
- package/dist/engine/database.js +294 -12
- package/dist/engine/optimizer.d.ts +7 -0
- package/dist/engine/optimizer.js +855 -84
- package/dist/engine/point-read.d.ts +57 -0
- package/dist/engine/point-read.js +189 -0
- package/dist/engine/query.d.ts +6 -0
- package/dist/engine/query.js +101 -18
- package/dist/engine/vector.js +69 -5
- package/dist/plan/model.d.ts +7 -1
- package/dist/storage/toolkit/record-core.js +104 -75
- package/dist/storage/types.js +34 -6
- package/dist/transactions/index.js +9 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +78 -10
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, parseQuantified, 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);
|
|
@@ -371,16 +489,19 @@ function coalesceOrEqualityLists(block) {
|
|
|
371
489
|
});
|
|
372
490
|
}
|
|
373
491
|
const comparisonOperators = new Set(["=", "!=", "<>", ">", ">=", "<", "<="]);
|
|
374
|
-
const aggregateCallNames = new Set(["COUNT", "SUM", "AVG", "MIN", "MAX"]);
|
|
375
492
|
const correlationKeyAlias = (index) => `\0correlation_key_${String(index)}`;
|
|
376
493
|
const correlationProbeAlias = (index) => `\0correlation_probe_${String(index)}`;
|
|
494
|
+
const correlationInputAlias = (index) => `\0correlation_input_${String(index)}`;
|
|
495
|
+
const correlationOrderAlias = (index) => `\0correlation_order_${String(index)}`;
|
|
377
496
|
const correlationValueAlias = "\0correlation_value";
|
|
378
497
|
const correlationMarkerAlias = "\0correlation_match";
|
|
498
|
+
const correlationRowNumberAlias = "\0correlation_row_number";
|
|
379
499
|
const correlationCountAlias = "\0correlation_count";
|
|
380
500
|
const correlationNonNullCountAlias = "\0correlation_non_null_count";
|
|
381
|
-
|
|
501
|
+
const correlationDecisionCountAlias = "\0correlation_decision_count";
|
|
502
|
+
const correlationTruthCountAlias = "\0correlation_truth_count";
|
|
503
|
+
function decorrelateBlock(block, nextAlias) {
|
|
382
504
|
const scope = new Set([block.base.alias, ...block.joins.map((join) => join.alias)]);
|
|
383
|
-
const nextAlias = correlationAliasFactory(scope);
|
|
384
505
|
const rewritten = [];
|
|
385
506
|
for (const predicate of block.predicates) {
|
|
386
507
|
const replacement = decorrelatePredicate(block, predicate, scope, nextAlias);
|
|
@@ -390,8 +511,8 @@ function decorrelateBlock(block) {
|
|
|
390
511
|
}
|
|
391
512
|
block.predicates = rewritten.map((predicate) => ({
|
|
392
513
|
...predicate,
|
|
393
|
-
left:
|
|
394
|
-
right:
|
|
514
|
+
left: rewriteCorrelatedExpression(block, predicate.left, scope, nextAlias),
|
|
515
|
+
right: rewriteCorrelatedExpression(block, predicate.right, scope, nextAlias),
|
|
395
516
|
}));
|
|
396
517
|
const grouped = block.groupBy.length > 0 || block.select.some((item) => containsAggregateCall(item.expression));
|
|
397
518
|
for (const item of block.select) {
|
|
@@ -405,7 +526,7 @@ function decorrelateBlock(block) {
|
|
|
405
526
|
throw new TypeError("A grouped correlated select-list subquery must reference only GROUP BY columns and cannot be nested inside an aggregate");
|
|
406
527
|
}
|
|
407
528
|
}
|
|
408
|
-
item.expression =
|
|
529
|
+
item.expression = rewriteCorrelatedExpression(block, item.expression, scope, nextAlias);
|
|
409
530
|
// The decorrelated value is functionally determined by the outer grouping keys. Carrying it
|
|
410
531
|
// as one additional key makes that dependency explicit to both grouped executors.
|
|
411
532
|
if (grouped)
|
|
@@ -414,86 +535,69 @@ function decorrelateBlock(block) {
|
|
|
414
535
|
assertNoCorrelation(block);
|
|
415
536
|
}
|
|
416
537
|
function containsAggregateCall(expression) {
|
|
417
|
-
|
|
418
|
-
return true;
|
|
419
|
-
return childExpressions(expression).some(containsAggregateCall);
|
|
538
|
+
return hasAggregate(expression);
|
|
420
539
|
}
|
|
421
540
|
function expressionHasCorrelatedSubquery(expression) {
|
|
422
|
-
if (expression.kind === "subquery")
|
|
541
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
423
542
|
return blockReferencesOutside(expression.block);
|
|
424
|
-
|
|
425
|
-
return false;
|
|
543
|
+
}
|
|
426
544
|
return childExpressions(expression).some(expressionHasCorrelatedSubquery);
|
|
427
545
|
}
|
|
428
546
|
/** Qualified outer references used by correlated scalar subqueries inside one expression. */
|
|
429
547
|
function correlatedOuterReferences(expression) {
|
|
430
|
-
if (expression.kind === "subquery"
|
|
548
|
+
if ((expression.kind === "subquery" || expression.kind === "exists") &&
|
|
549
|
+
blockReferencesOutside(expression.block)) {
|
|
431
550
|
const references = [];
|
|
432
551
|
collectOutsideReferences(expression.block, new Set(), references);
|
|
433
552
|
return references;
|
|
434
553
|
}
|
|
435
554
|
return childExpressions(expression).flatMap(correlatedOuterReferences);
|
|
436
555
|
}
|
|
437
|
-
/** Replaces correlated
|
|
438
|
-
function
|
|
439
|
-
if (expression.kind === "
|
|
440
|
-
return
|
|
441
|
-
}
|
|
442
|
-
if (expression.kind === "binary" ||
|
|
443
|
-
expression.kind === "condition" ||
|
|
444
|
-
expression.kind === "logical") {
|
|
445
|
-
expression.left = rewriteCorrelatedScalars(block, expression.left, scope, nextAlias);
|
|
446
|
-
expression.right = rewriteCorrelatedScalars(block, expression.right, scope, nextAlias);
|
|
447
|
-
return expression;
|
|
448
|
-
}
|
|
449
|
-
if (expression.kind === "call") {
|
|
450
|
-
expression.arguments = expression.arguments.map((argument) => rewriteCorrelatedScalars(block, argument, scope, nextAlias));
|
|
451
|
-
return expression;
|
|
452
|
-
}
|
|
453
|
-
if (expression.kind === "not") {
|
|
454
|
-
expression.operand = rewriteCorrelatedScalars(block, expression.operand, scope, nextAlias);
|
|
455
|
-
return expression;
|
|
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);
|
|
456
560
|
}
|
|
457
|
-
if (expression.kind === "
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
+
}
|
|
464
572
|
}
|
|
573
|
+
expression.right = rewriteCorrelatedExpression(block, expression.right, scope, nextAlias);
|
|
465
574
|
return expression;
|
|
466
575
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
/** Replaces correlated EXISTS nodes nested inside boolean expressions with hidden match flags. */
|
|
470
|
-
function rewriteCorrelatedExists(block, expression, scope, nextAlias) {
|
|
471
|
-
if (expression.kind === "exists" && blockReferencesOutside(expression.block)) {
|
|
472
|
-
return decorrelateExistsExpression(block, expression, scope, nextAlias);
|
|
576
|
+
if (expression.kind === "subquery" && blockReferencesOutside(expression.block)) {
|
|
577
|
+
return decorrelateScalarSubquery(block, expression, scope, nextAlias);
|
|
473
578
|
}
|
|
474
|
-
if (expression.kind === "binary" ||
|
|
475
|
-
expression.
|
|
476
|
-
expression.
|
|
477
|
-
expression.left = rewriteCorrelatedExists(block, expression.left, scope, nextAlias);
|
|
478
|
-
expression.right = rewriteCorrelatedExists(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);
|
|
479
582
|
return expression;
|
|
480
583
|
}
|
|
481
584
|
if (expression.kind === "call") {
|
|
482
|
-
expression.arguments = expression.arguments.map((argument) =>
|
|
585
|
+
expression.arguments = expression.arguments.map((argument) => rewriteCorrelatedExpression(block, argument, scope, nextAlias));
|
|
483
586
|
return expression;
|
|
484
587
|
}
|
|
485
588
|
if (expression.kind === "not") {
|
|
486
|
-
expression.operand =
|
|
589
|
+
expression.operand = rewriteCorrelatedExpression(block, expression.operand, scope, nextAlias);
|
|
487
590
|
return expression;
|
|
488
591
|
}
|
|
489
592
|
if (expression.kind === "case") {
|
|
490
593
|
for (const branch of expression.branches) {
|
|
491
|
-
branch.when =
|
|
492
|
-
branch.then =
|
|
594
|
+
branch.when = rewriteCorrelatedExpression(block, branch.when, scope, nextAlias);
|
|
595
|
+
branch.then = rewriteCorrelatedExpression(block, branch.then, scope, nextAlias);
|
|
493
596
|
}
|
|
494
597
|
if (expression.otherwise !== undefined) {
|
|
495
|
-
expression.otherwise =
|
|
598
|
+
expression.otherwise = rewriteCorrelatedExpression(block, expression.otherwise, scope, nextAlias);
|
|
496
599
|
}
|
|
600
|
+
return expression;
|
|
497
601
|
}
|
|
498
602
|
return expression;
|
|
499
603
|
}
|
|
@@ -503,8 +607,10 @@ function correlationAliasFactory(scope) {
|
|
|
503
607
|
for (;;) {
|
|
504
608
|
sequence += 1;
|
|
505
609
|
const alias = `corr_${String(sequence)}`;
|
|
506
|
-
if (!scope.has(alias))
|
|
610
|
+
if (!scope.has(alias)) {
|
|
611
|
+
scope.add(alias);
|
|
507
612
|
return alias;
|
|
613
|
+
}
|
|
508
614
|
}
|
|
509
615
|
};
|
|
510
616
|
}
|
|
@@ -525,6 +631,14 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
525
631
|
if (!blockReferencesOutside(inner))
|
|
526
632
|
return undefined;
|
|
527
633
|
rejectGroupedInner(inner, "EXISTS");
|
|
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
|
+
}
|
|
528
642
|
const keys = extractCorrelation(inner, scope, "EXISTS", true);
|
|
529
643
|
const alias = nextAlias();
|
|
530
644
|
const derived = {
|
|
@@ -932,6 +1046,143 @@ function decorrelateRangeNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
932
1046
|
right: { kind: "literal", value: true },
|
|
933
1047
|
};
|
|
934
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
|
+
}
|
|
935
1186
|
/**
|
|
936
1187
|
* Rewrites one correlated scalar-aggregate subquery into a left join against the grouped
|
|
937
1188
|
* aggregate, returning the expression that replaces the subquery node (COUNT wraps in COALESCE
|
|
@@ -939,17 +1190,25 @@ function decorrelateRangeNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
939
1190
|
*/
|
|
940
1191
|
function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
|
|
941
1192
|
const inner = subquery.block;
|
|
1193
|
+
const passthrough = scalarPassthrough(inner);
|
|
1194
|
+
if (passthrough !== undefined)
|
|
1195
|
+
return passthrough;
|
|
942
1196
|
const item = inner.select[0];
|
|
943
1197
|
if (inner.select.length !== 1 ||
|
|
944
|
-
item
|
|
945
|
-
!
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
if (inner.groupBy.length > 0 ||
|
|
1198
|
+
item === undefined ||
|
|
1199
|
+
!isAggregateCall(item.expression) ||
|
|
1200
|
+
inner.groupBy.length > 0 ||
|
|
949
1201
|
inner.having.length > 0 ||
|
|
950
1202
|
inner.orderBy.length > 0 ||
|
|
951
|
-
inner.limit !== undefined
|
|
952
|
-
|
|
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);
|
|
953
1212
|
}
|
|
954
1213
|
const keys = extractCorrelation(inner, scope, "scalar", true);
|
|
955
1214
|
if (keys.some((key) => key.operator !== "=")) {
|
|
@@ -983,6 +1242,347 @@ function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
|
|
|
983
1242
|
}
|
|
984
1243
|
return value;
|
|
985
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
|
+
}
|
|
986
1586
|
/**
|
|
987
1587
|
* Aggregates a range-correlated scalar subquery once per distinct outer probe tuple. The probe
|
|
988
1588
|
* table prevents a general join from multiplying outer rows and lets the final join use equality
|
|
@@ -1062,6 +1662,9 @@ function decorrelateNonEqualityScalar(block, inner, aggregate, keys, nextAlias)
|
|
|
1062
1662
|
function decorrelateExistsExpression(block, exists, scope, nextAlias) {
|
|
1063
1663
|
const inner = exists.block;
|
|
1064
1664
|
rejectGroupedInner(inner, "EXISTS");
|
|
1665
|
+
if (!canExtractCorrelation(inner, scope, "EXISTS", true)) {
|
|
1666
|
+
return decorrelateExistsWithProbes(block, exists, scope, nextAlias);
|
|
1667
|
+
}
|
|
1065
1668
|
const keys = extractCorrelation(inner, scope, "EXISTS", true);
|
|
1066
1669
|
if (keys.every(({ operator }) => operator === "=")) {
|
|
1067
1670
|
const flagsAlias = nextAlias();
|
|
@@ -1141,21 +1744,151 @@ function decorrelateExistsExpression(block, exists, scope, nextAlias) {
|
|
|
1141
1744
|
right: { kind: "literal", value: null },
|
|
1142
1745
|
};
|
|
1143
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
|
+
}
|
|
1144
1871
|
/** Distinct outer key tuples, preserving the FROM joins needed by every referenced source. */
|
|
1145
1872
|
function outerProbeBlock(block, keys) {
|
|
1873
|
+
return outerProbeBlockForExpressions(block, keys.map((key) => key.outer));
|
|
1874
|
+
}
|
|
1875
|
+
function outerProbeBlockForExpressions(block, outerExpressions) {
|
|
1146
1876
|
const sources = [block.base, ...block.joins];
|
|
1147
|
-
const sourceIndexes =
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
+
});
|
|
1156
1889
|
});
|
|
1157
1890
|
const lastSource = Math.max(...sourceIndexes);
|
|
1158
|
-
const expressions =
|
|
1891
|
+
const expressions = outerExpressions.map((expression) => structuredClone(expression));
|
|
1159
1892
|
return {
|
|
1160
1893
|
sql: "(correlation probes)",
|
|
1161
1894
|
base: structuredClone(block.base),
|
|
@@ -1170,6 +1903,30 @@ function outerProbeBlock(block, keys) {
|
|
|
1170
1903
|
orderBy: [],
|
|
1171
1904
|
};
|
|
1172
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
|
+
}
|
|
1173
1930
|
/** A general ON join carrying one comparison for each corresponding expression pair. */
|
|
1174
1931
|
function generalCorrelationJoin(alias, derived, inner, outer, operators) {
|
|
1175
1932
|
let on;
|
|
@@ -1278,6 +2035,18 @@ function extractCorrelation(inner, outerScope, label, comparisons = false) {
|
|
|
1278
2035
|
}
|
|
1279
2036
|
return keys;
|
|
1280
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
|
+
}
|
|
1281
2050
|
function correlationComparison(predicate, innerScope, outerScope, comparisons) {
|
|
1282
2051
|
if (predicate.operator !== "=" &&
|
|
1283
2052
|
(!comparisons || !comparisonOperators.has(predicate.operator))) {
|
|
@@ -1733,8 +2502,10 @@ function referencedOutputAliases(block, source, singleSource) {
|
|
|
1733
2502
|
if (expression.otherwise !== undefined)
|
|
1734
2503
|
visit(expression.otherwise);
|
|
1735
2504
|
}
|
|
1736
|
-
else if (expression.kind === "call")
|
|
2505
|
+
else if (expression.kind === "call") {
|
|
1737
2506
|
expression.arguments.forEach(visit);
|
|
2507
|
+
expression.aggregateOrderBy?.forEach((order) => visit(order.expression));
|
|
2508
|
+
}
|
|
1738
2509
|
else if (expression.kind === "list")
|
|
1739
2510
|
expression.items.forEach(visit);
|
|
1740
2511
|
else if (expression.kind === "fts") {
|