@minnowdb/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- 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 +516 -74
- 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.js +540 -38
- package/dist/engine/query.d.ts +5 -278
- package/dist/engine/query.js +78 -32
- 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 +301 -39
- 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 +218 -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/sql-feature-matrix.json +69 -19
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 { childExpressions, expressionAliases, forEachBlockExpression, forEachNestedBlock, isScalarFunctionName, 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
|
|
@@ -60,7 +60,7 @@ function optimizeBlock(block) {
|
|
|
60
60
|
pruneDerivedProjections(block);
|
|
61
61
|
combineDerivedLimit(block);
|
|
62
62
|
}
|
|
63
|
-
/** Converts
|
|
63
|
+
/** Converts correlated LATERAL derived tables into ordinary set-at-a-time joins. */
|
|
64
64
|
function decorrelateLateralSources(block) {
|
|
65
65
|
if (block.base.lateral === true) {
|
|
66
66
|
throw new TypeError("LATERAL needs a source to its left");
|
|
@@ -85,14 +85,14 @@ function decorrelateLateralSources(block) {
|
|
|
85
85
|
inner.limit !== undefined) {
|
|
86
86
|
throw new TypeError("Correlated LATERAL queries cannot use grouping, HAVING, ORDER BY, or LIMIT");
|
|
87
87
|
}
|
|
88
|
-
const keys = extractCorrelation(inner, available, "LATERAL");
|
|
88
|
+
const keys = extractCorrelation(inner, available, "LATERAL", true);
|
|
89
89
|
const keyAliases = keys.map((_, index) => `\u0000lateral_key_${String(index + 1)}`);
|
|
90
90
|
keys.forEach((key, index) => {
|
|
91
91
|
inner.select.push({ expression: key.inner, alias: keyAliases[index] ?? "" });
|
|
92
92
|
});
|
|
93
93
|
const correlations = keys.map((key, index) => ({
|
|
94
94
|
kind: "condition",
|
|
95
|
-
operator: key.operator,
|
|
95
|
+
operator: reverseComparison(key.operator),
|
|
96
96
|
left: key.outer,
|
|
97
97
|
right: { kind: "column", reference: `${join.alias}.${keyAliases[index] ?? ""}` },
|
|
98
98
|
}));
|
|
@@ -372,6 +372,12 @@ function coalesceOrEqualityLists(block) {
|
|
|
372
372
|
}
|
|
373
373
|
const comparisonOperators = new Set(["=", "!=", "<>", ">", ">=", "<", "<="]);
|
|
374
374
|
const aggregateCallNames = new Set(["COUNT", "SUM", "AVG", "MIN", "MAX"]);
|
|
375
|
+
const correlationKeyAlias = (index) => `\0correlation_key_${String(index)}`;
|
|
376
|
+
const correlationProbeAlias = (index) => `\0correlation_probe_${String(index)}`;
|
|
377
|
+
const correlationValueAlias = "\0correlation_value";
|
|
378
|
+
const correlationMarkerAlias = "\0correlation_match";
|
|
379
|
+
const correlationCountAlias = "\0correlation_count";
|
|
380
|
+
const correlationNonNullCountAlias = "\0correlation_non_null_count";
|
|
375
381
|
function decorrelateBlock(block) {
|
|
376
382
|
const scope = new Set([block.base.alias, ...block.joins.map((join) => join.alias)]);
|
|
377
383
|
const nextAlias = correlationAliasFactory(scope);
|
|
@@ -382,15 +388,28 @@ function decorrelateBlock(block) {
|
|
|
382
388
|
continue;
|
|
383
389
|
rewritten.push(replacement ?? predicate);
|
|
384
390
|
}
|
|
385
|
-
block.predicates = rewritten
|
|
391
|
+
block.predicates = rewritten.map((predicate) => ({
|
|
392
|
+
...predicate,
|
|
393
|
+
left: rewriteCorrelatedExists(block, predicate.left, scope, nextAlias),
|
|
394
|
+
right: rewriteCorrelatedExists(block, predicate.right, scope, nextAlias),
|
|
395
|
+
}));
|
|
386
396
|
const grouped = block.groupBy.length > 0 || block.select.some((item) => containsAggregateCall(item.expression));
|
|
387
397
|
for (const item of block.select) {
|
|
388
398
|
if (!expressionHasCorrelatedSubquery(item.expression))
|
|
389
399
|
continue;
|
|
390
400
|
if (grouped) {
|
|
391
|
-
|
|
401
|
+
const groupedExpressions = new Set(block.groupBy.map((group) => JSON.stringify(group)));
|
|
402
|
+
const outerReferences = correlatedOuterReferences(item.expression);
|
|
403
|
+
if (containsAggregateCall(item.expression) ||
|
|
404
|
+
outerReferences.some((reference) => !groupedExpressions.has(JSON.stringify({ kind: "column", reference })))) {
|
|
405
|
+
throw new TypeError("A grouped correlated select-list subquery must reference only GROUP BY columns and cannot be nested inside an aggregate");
|
|
406
|
+
}
|
|
392
407
|
}
|
|
393
408
|
item.expression = rewriteCorrelatedScalars(block, item.expression, scope, nextAlias);
|
|
409
|
+
// The decorrelated value is functionally determined by the outer grouping keys. Carrying it
|
|
410
|
+
// as one additional key makes that dependency explicit to both grouped executors.
|
|
411
|
+
if (grouped)
|
|
412
|
+
block.groupBy.push(structuredClone(item.expression));
|
|
394
413
|
}
|
|
395
414
|
assertNoCorrelation(block);
|
|
396
415
|
}
|
|
@@ -406,6 +425,15 @@ function expressionHasCorrelatedSubquery(expression) {
|
|
|
406
425
|
return false;
|
|
407
426
|
return childExpressions(expression).some(expressionHasCorrelatedSubquery);
|
|
408
427
|
}
|
|
428
|
+
/** Qualified outer references used by correlated scalar subqueries inside one expression. */
|
|
429
|
+
function correlatedOuterReferences(expression) {
|
|
430
|
+
if (expression.kind === "subquery" && blockReferencesOutside(expression.block)) {
|
|
431
|
+
const references = [];
|
|
432
|
+
collectOutsideReferences(expression.block, new Set(), references);
|
|
433
|
+
return references;
|
|
434
|
+
}
|
|
435
|
+
return childExpressions(expression).flatMap(correlatedOuterReferences);
|
|
436
|
+
}
|
|
409
437
|
/** Replaces correlated scalar subqueries inside one select expression with decorrelated joins. */
|
|
410
438
|
function rewriteCorrelatedScalars(block, expression, scope, nextAlias) {
|
|
411
439
|
if (expression.kind === "subquery" && blockReferencesOutside(expression.block)) {
|
|
@@ -438,6 +466,37 @@ function rewriteCorrelatedScalars(block, expression, scope, nextAlias) {
|
|
|
438
466
|
}
|
|
439
467
|
return expression;
|
|
440
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);
|
|
473
|
+
}
|
|
474
|
+
if (expression.kind === "binary" ||
|
|
475
|
+
expression.kind === "condition" ||
|
|
476
|
+
expression.kind === "logical") {
|
|
477
|
+
expression.left = rewriteCorrelatedExists(block, expression.left, scope, nextAlias);
|
|
478
|
+
expression.right = rewriteCorrelatedExists(block, expression.right, scope, nextAlias);
|
|
479
|
+
return expression;
|
|
480
|
+
}
|
|
481
|
+
if (expression.kind === "call") {
|
|
482
|
+
expression.arguments = expression.arguments.map((argument) => rewriteCorrelatedExists(block, argument, scope, nextAlias));
|
|
483
|
+
return expression;
|
|
484
|
+
}
|
|
485
|
+
if (expression.kind === "not") {
|
|
486
|
+
expression.operand = rewriteCorrelatedExists(block, expression.operand, scope, nextAlias);
|
|
487
|
+
return expression;
|
|
488
|
+
}
|
|
489
|
+
if (expression.kind === "case") {
|
|
490
|
+
for (const branch of expression.branches) {
|
|
491
|
+
branch.when = rewriteCorrelatedExists(block, branch.when, scope, nextAlias);
|
|
492
|
+
branch.then = rewriteCorrelatedExists(block, branch.then, scope, nextAlias);
|
|
493
|
+
}
|
|
494
|
+
if (expression.otherwise !== undefined) {
|
|
495
|
+
expression.otherwise = rewriteCorrelatedExists(block, expression.otherwise, scope, nextAlias);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return expression;
|
|
499
|
+
}
|
|
441
500
|
function correlationAliasFactory(scope) {
|
|
442
501
|
let sequence = 0;
|
|
443
502
|
return () => {
|
|
@@ -466,14 +525,16 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
466
525
|
if (!blockReferencesOutside(inner))
|
|
467
526
|
return undefined;
|
|
468
527
|
rejectGroupedInner(inner, "EXISTS");
|
|
469
|
-
guardWildcard(block);
|
|
470
528
|
const keys = extractCorrelation(inner, scope, "EXISTS", true);
|
|
471
529
|
const alias = nextAlias();
|
|
472
530
|
const derived = {
|
|
473
531
|
sql: "(correlated exists)",
|
|
474
532
|
base: inner.base,
|
|
475
533
|
joins: inner.joins,
|
|
476
|
-
select: keys.map((key, index) => ({
|
|
534
|
+
select: keys.map((key, index) => ({
|
|
535
|
+
expression: key.inner,
|
|
536
|
+
alias: correlationKeyAlias(index),
|
|
537
|
+
})),
|
|
477
538
|
predicates: inner.predicates,
|
|
478
539
|
// Grouping by the keys deduplicates, so the join multiplies no outer row. The parser's
|
|
479
540
|
// injected LIMIT 1 and any ORDER BY are dropped: existence ignores both.
|
|
@@ -488,7 +549,7 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
488
549
|
if (!negated)
|
|
489
550
|
return "consumed";
|
|
490
551
|
return {
|
|
491
|
-
left: { kind: "column", reference: `${alias}
|
|
552
|
+
left: { kind: "column", reference: `${alias}.${correlationKeyAlias(0)}` },
|
|
492
553
|
operator: "IS NULL",
|
|
493
554
|
right: { kind: "literal", value: null },
|
|
494
555
|
};
|
|
@@ -504,8 +565,11 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
504
565
|
if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
|
|
505
566
|
throw new TypeError("An IN subquery must select exactly one column");
|
|
506
567
|
}
|
|
507
|
-
|
|
508
|
-
const
|
|
568
|
+
const keys = extractCorrelation(inner, scope, "IN", true);
|
|
569
|
+
const general = keys.some((key) => key.operator !== "=");
|
|
570
|
+
if (predicate.operator === "NOT IN" && general) {
|
|
571
|
+
return decorrelateRangeNotIn(block, predicate.left, inner, item.expression, keys, nextAlias);
|
|
572
|
+
}
|
|
509
573
|
if (predicate.operator === "NOT IN") {
|
|
510
574
|
return decorrelateNotIn(block, predicate.left, inner, item.expression, keys, nextAlias);
|
|
511
575
|
}
|
|
@@ -515,21 +579,113 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
|
|
|
515
579
|
base: inner.base,
|
|
516
580
|
joins: inner.joins,
|
|
517
581
|
select: [
|
|
518
|
-
...keys.map((key, index) => ({
|
|
519
|
-
|
|
582
|
+
...keys.map((key, index) => ({
|
|
583
|
+
expression: key.inner,
|
|
584
|
+
alias: correlationKeyAlias(index),
|
|
585
|
+
})),
|
|
586
|
+
{ expression: item.expression, alias: correlationValueAlias },
|
|
520
587
|
],
|
|
521
588
|
predicates: inner.predicates,
|
|
522
589
|
groupBy: [...keys.map((key) => key.inner), item.expression],
|
|
523
590
|
having: [],
|
|
524
591
|
orderBy: [],
|
|
525
592
|
};
|
|
593
|
+
if (general) {
|
|
594
|
+
const join = generalCorrelationJoin(alias, derived, [
|
|
595
|
+
...keys.map((_, index) => ({
|
|
596
|
+
kind: "column",
|
|
597
|
+
reference: `${alias}.${correlationKeyAlias(index)}`,
|
|
598
|
+
})),
|
|
599
|
+
{ kind: "column", reference: `${alias}.${correlationValueAlias}` },
|
|
600
|
+
], [...keys.map((key) => key.outer), predicate.left], [...keys.map((key) => key.operator), "="]);
|
|
601
|
+
join.kind = "semi";
|
|
602
|
+
block.joins.push(join);
|
|
603
|
+
return "consumed";
|
|
604
|
+
}
|
|
526
605
|
pushCorrelationJoin(block, alias, derived, keys, "inner");
|
|
527
606
|
return {
|
|
528
607
|
left: predicate.left,
|
|
529
608
|
operator: "=",
|
|
530
|
-
right: { kind: "column", reference: `${alias}
|
|
609
|
+
right: { kind: "column", reference: `${alias}.${correlationValueAlias}` },
|
|
531
610
|
};
|
|
532
611
|
}
|
|
612
|
+
// outer comparison ANY / ALL (correlated subquery). At top-level WHERE, ANY is existence of a
|
|
613
|
+
// true comparison; ALL is absence of a false or unknown comparison. Empty sets therefore keep
|
|
614
|
+
// their standard false/true answers without materializing a per-row value list.
|
|
615
|
+
const quantified = parseQuantified(predicate.operator);
|
|
616
|
+
if (quantified !== undefined && predicate.right.kind === "subquery") {
|
|
617
|
+
const inner = predicate.right.block;
|
|
618
|
+
if (!blockReferencesOutside(inner))
|
|
619
|
+
return undefined;
|
|
620
|
+
rejectGroupedInner(inner, "ANY/ALL");
|
|
621
|
+
const item = inner.select[0];
|
|
622
|
+
if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
|
|
623
|
+
throw new TypeError("An ANY/ALL subquery must select exactly one column");
|
|
624
|
+
}
|
|
625
|
+
const keys = extractCorrelation(inner, scope, "ANY/ALL", true);
|
|
626
|
+
const alias = nextAlias();
|
|
627
|
+
const derived = {
|
|
628
|
+
sql: "(correlated quantified comparison)",
|
|
629
|
+
base: inner.base,
|
|
630
|
+
joins: inner.joins,
|
|
631
|
+
select: [
|
|
632
|
+
...keys.map((key, index) => ({
|
|
633
|
+
expression: key.inner,
|
|
634
|
+
alias: correlationKeyAlias(index),
|
|
635
|
+
})),
|
|
636
|
+
{ expression: item.expression, alias: correlationValueAlias },
|
|
637
|
+
],
|
|
638
|
+
predicates: inner.predicates,
|
|
639
|
+
groupBy: [...keys.map((key) => key.inner), item.expression],
|
|
640
|
+
having: [],
|
|
641
|
+
orderBy: [],
|
|
642
|
+
};
|
|
643
|
+
const join = generalCorrelationJoin(alias, derived, keys.map((_, index) => ({
|
|
644
|
+
kind: "column",
|
|
645
|
+
reference: `${alias}.${correlationKeyAlias(index)}`,
|
|
646
|
+
})), keys.map((key) => key.outer), keys.map((key) => key.operator));
|
|
647
|
+
const value = {
|
|
648
|
+
kind: "column",
|
|
649
|
+
reference: `${alias}.${correlationValueAlias}`,
|
|
650
|
+
};
|
|
651
|
+
const comparison = {
|
|
652
|
+
kind: "condition",
|
|
653
|
+
operator: quantified.comparison,
|
|
654
|
+
left: structuredClone(predicate.left),
|
|
655
|
+
right: value,
|
|
656
|
+
};
|
|
657
|
+
let quantifiedCondition = comparison;
|
|
658
|
+
if (quantified.quantifier === "all") {
|
|
659
|
+
quantifiedCondition = {
|
|
660
|
+
kind: "logical",
|
|
661
|
+
operator: "or",
|
|
662
|
+
left: {
|
|
663
|
+
kind: "logical",
|
|
664
|
+
operator: "or",
|
|
665
|
+
left: {
|
|
666
|
+
kind: "condition",
|
|
667
|
+
operator: "IS NULL",
|
|
668
|
+
left: structuredClone(predicate.left),
|
|
669
|
+
right: { kind: "literal", value: null },
|
|
670
|
+
},
|
|
671
|
+
right: {
|
|
672
|
+
kind: "condition",
|
|
673
|
+
operator: "IS NULL",
|
|
674
|
+
left: value,
|
|
675
|
+
right: { kind: "literal", value: null },
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
right: { kind: "not", operand: comparison },
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
join.on =
|
|
682
|
+
join.on === undefined
|
|
683
|
+
? quantifiedCondition
|
|
684
|
+
: { kind: "logical", operator: "and", left: join.on, right: quantifiedCondition };
|
|
685
|
+
join.kind = quantified.quantifier === "all" ? "anti" : "semi";
|
|
686
|
+
block.joins.push(join);
|
|
687
|
+
return "consumed";
|
|
688
|
+
}
|
|
533
689
|
// comparison against a correlated scalar aggregate
|
|
534
690
|
if (!comparisonOperators.has(predicate.operator))
|
|
535
691
|
return undefined;
|
|
@@ -566,9 +722,9 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
566
722
|
select: [
|
|
567
723
|
...keys.map((key, index) => ({
|
|
568
724
|
expression: structuredClone(key.inner),
|
|
569
|
-
alias:
|
|
725
|
+
alias: correlationKeyAlias(index),
|
|
570
726
|
})),
|
|
571
|
-
{ expression: structuredClone(item), alias:
|
|
727
|
+
{ expression: structuredClone(item), alias: correlationValueAlias },
|
|
572
728
|
],
|
|
573
729
|
predicates: structuredClone(inner.predicates),
|
|
574
730
|
groupBy: [...keys.map((key) => structuredClone(key.inner)), structuredClone(item)],
|
|
@@ -589,9 +745,9 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
589
745
|
right: tuple([
|
|
590
746
|
...keys.map((_, index) => ({
|
|
591
747
|
kind: "column",
|
|
592
|
-
reference: `${valuesAlias}
|
|
748
|
+
reference: `${valuesAlias}.${correlationKeyAlias(index)}`,
|
|
593
749
|
})),
|
|
594
|
-
{ kind: "column", reference: `${valuesAlias}
|
|
750
|
+
{ kind: "column", reference: `${valuesAlias}.${correlationValueAlias}` },
|
|
595
751
|
]),
|
|
596
752
|
});
|
|
597
753
|
const flagsAlias = nextAlias();
|
|
@@ -602,15 +758,15 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
602
758
|
select: [
|
|
603
759
|
...keys.map((key, index) => ({
|
|
604
760
|
expression: structuredClone(key.inner),
|
|
605
|
-
alias:
|
|
761
|
+
alias: correlationKeyAlias(index),
|
|
606
762
|
})),
|
|
607
763
|
{
|
|
608
764
|
expression: { kind: "call", name: "COUNT", arguments: [{ kind: "wildcard" }] },
|
|
609
|
-
alias:
|
|
765
|
+
alias: correlationCountAlias,
|
|
610
766
|
},
|
|
611
767
|
{
|
|
612
768
|
expression: { kind: "call", name: "COUNT", arguments: [structuredClone(item)] },
|
|
613
|
-
alias:
|
|
769
|
+
alias: correlationNonNullCountAlias,
|
|
614
770
|
},
|
|
615
771
|
],
|
|
616
772
|
predicates: structuredClone(inner.predicates),
|
|
@@ -619,8 +775,14 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
619
775
|
orderBy: [],
|
|
620
776
|
};
|
|
621
777
|
pushCorrelationJoin(block, flagsAlias, flags, keys, "left");
|
|
622
|
-
const total = {
|
|
623
|
-
|
|
778
|
+
const total = {
|
|
779
|
+
kind: "column",
|
|
780
|
+
reference: `${flagsAlias}.${correlationCountAlias}`,
|
|
781
|
+
};
|
|
782
|
+
const nonNull = {
|
|
783
|
+
kind: "column",
|
|
784
|
+
reference: `${flagsAlias}.${correlationNonNullCountAlias}`,
|
|
785
|
+
};
|
|
624
786
|
const safe = {
|
|
625
787
|
kind: "logical",
|
|
626
788
|
operator: "or",
|
|
@@ -648,6 +810,128 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
648
810
|
right: { kind: "literal", value: true },
|
|
649
811
|
};
|
|
650
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Range-correlated NOT IN uses a general anti-join for exact matches, then joins back one flag row
|
|
815
|
+
* per distinct outer probe tuple. The counts preserve empty-set and NULL poisoning semantics.
|
|
816
|
+
*/
|
|
817
|
+
function decorrelateRangeNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
818
|
+
const valuesAlias = nextAlias();
|
|
819
|
+
const values = {
|
|
820
|
+
sql: "(range-correlated not in values)",
|
|
821
|
+
base: structuredClone(inner.base),
|
|
822
|
+
joins: structuredClone(inner.joins),
|
|
823
|
+
select: [
|
|
824
|
+
...keys.map((key, index) => ({
|
|
825
|
+
expression: structuredClone(key.inner),
|
|
826
|
+
alias: correlationKeyAlias(index),
|
|
827
|
+
})),
|
|
828
|
+
{ expression: structuredClone(item), alias: correlationValueAlias },
|
|
829
|
+
],
|
|
830
|
+
predicates: structuredClone(inner.predicates),
|
|
831
|
+
groupBy: [...keys.map((key) => structuredClone(key.inner)), structuredClone(item)],
|
|
832
|
+
having: [],
|
|
833
|
+
orderBy: [],
|
|
834
|
+
};
|
|
835
|
+
const exactMatch = generalCorrelationJoin(valuesAlias, values, [
|
|
836
|
+
...keys.map((_, index) => ({
|
|
837
|
+
kind: "column",
|
|
838
|
+
reference: `${valuesAlias}.${correlationKeyAlias(index)}`,
|
|
839
|
+
})),
|
|
840
|
+
{ kind: "column", reference: `${valuesAlias}.${correlationValueAlias}` },
|
|
841
|
+
], [...keys.map((key) => structuredClone(key.outer)), structuredClone(probe)], [...keys.map((key) => key.operator), "="]);
|
|
842
|
+
exactMatch.kind = "anti";
|
|
843
|
+
block.joins.push(exactMatch);
|
|
844
|
+
const probes = outerProbeBlock(block, keys);
|
|
845
|
+
const probesAlias = nextAlias();
|
|
846
|
+
const innerRowsAlias = nextAlias();
|
|
847
|
+
const innerRows = {
|
|
848
|
+
sql: "(range-correlated not in rows)",
|
|
849
|
+
base: inner.base,
|
|
850
|
+
joins: inner.joins,
|
|
851
|
+
select: [
|
|
852
|
+
...keys.map((key, index) => ({
|
|
853
|
+
expression: key.inner,
|
|
854
|
+
alias: correlationKeyAlias(index),
|
|
855
|
+
})),
|
|
856
|
+
{ expression: item, alias: correlationValueAlias },
|
|
857
|
+
],
|
|
858
|
+
predicates: inner.predicates,
|
|
859
|
+
groupBy: [],
|
|
860
|
+
having: [],
|
|
861
|
+
orderBy: [],
|
|
862
|
+
};
|
|
863
|
+
const probeReferences = keys.map((_, index) => ({
|
|
864
|
+
kind: "column",
|
|
865
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
866
|
+
}));
|
|
867
|
+
const flags = {
|
|
868
|
+
sql: "(range-correlated not in flags)",
|
|
869
|
+
base: { table: probesAlias, alias: probesAlias, derived: probes },
|
|
870
|
+
joins: [
|
|
871
|
+
generalCorrelationJoin(innerRowsAlias, innerRows, keys.map((_, index) => ({
|
|
872
|
+
kind: "column",
|
|
873
|
+
reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
|
|
874
|
+
})), probeReferences, keys.map(({ operator }) => operator)),
|
|
875
|
+
],
|
|
876
|
+
select: [
|
|
877
|
+
...probeReferences.map((expression, index) => ({
|
|
878
|
+
expression,
|
|
879
|
+
alias: correlationKeyAlias(index),
|
|
880
|
+
})),
|
|
881
|
+
{
|
|
882
|
+
expression: { kind: "call", name: "COUNT", arguments: [{ kind: "wildcard" }] },
|
|
883
|
+
alias: correlationCountAlias,
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
expression: {
|
|
887
|
+
kind: "call",
|
|
888
|
+
name: "COUNT",
|
|
889
|
+
arguments: [{ kind: "column", reference: `${innerRowsAlias}.${correlationValueAlias}` }],
|
|
890
|
+
},
|
|
891
|
+
alias: correlationNonNullCountAlias,
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
predicates: [],
|
|
895
|
+
groupBy: probeReferences,
|
|
896
|
+
having: [],
|
|
897
|
+
orderBy: [],
|
|
898
|
+
};
|
|
899
|
+
const flagsAlias = nextAlias();
|
|
900
|
+
pushCorrelationJoin(block, flagsAlias, flags, keys.map((key) => ({ ...key, operator: "=" })), "left");
|
|
901
|
+
const total = {
|
|
902
|
+
kind: "column",
|
|
903
|
+
reference: `${flagsAlias}.${correlationCountAlias}`,
|
|
904
|
+
};
|
|
905
|
+
const nonNull = {
|
|
906
|
+
kind: "column",
|
|
907
|
+
reference: `${flagsAlias}.${correlationNonNullCountAlias}`,
|
|
908
|
+
};
|
|
909
|
+
return {
|
|
910
|
+
left: {
|
|
911
|
+
kind: "logical",
|
|
912
|
+
operator: "or",
|
|
913
|
+
left: {
|
|
914
|
+
kind: "condition",
|
|
915
|
+
operator: "IS NULL",
|
|
916
|
+
left: total,
|
|
917
|
+
right: { kind: "literal", value: null },
|
|
918
|
+
},
|
|
919
|
+
right: {
|
|
920
|
+
kind: "logical",
|
|
921
|
+
operator: "and",
|
|
922
|
+
left: {
|
|
923
|
+
kind: "condition",
|
|
924
|
+
operator: "IS NOT NULL",
|
|
925
|
+
left: structuredClone(probe),
|
|
926
|
+
right: { kind: "literal", value: null },
|
|
927
|
+
},
|
|
928
|
+
right: { kind: "condition", operator: "=", left: total, right: nonNull },
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
operator: "IS TRUE",
|
|
932
|
+
right: { kind: "literal", value: true },
|
|
933
|
+
};
|
|
934
|
+
}
|
|
651
935
|
/**
|
|
652
936
|
* Rewrites one correlated scalar-aggregate subquery into a left join against the grouped
|
|
653
937
|
* aggregate, returning the expression that replaces the subquery node (COUNT wraps in COALESCE
|
|
@@ -656,8 +940,9 @@ function decorrelateNotIn(block, probe, inner, item, keys, nextAlias) {
|
|
|
656
940
|
function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
|
|
657
941
|
const inner = subquery.block;
|
|
658
942
|
const item = inner.select[0];
|
|
659
|
-
|
|
660
|
-
|
|
943
|
+
if (inner.select.length !== 1 ||
|
|
944
|
+
item?.expression.kind !== "call" ||
|
|
945
|
+
!aggregateCallNames.has(item.expression.name)) {
|
|
661
946
|
throw new TypeError("A correlated scalar subquery must select exactly one aggregate expression");
|
|
662
947
|
}
|
|
663
948
|
if (inner.groupBy.length > 0 ||
|
|
@@ -666,16 +951,21 @@ function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
|
|
|
666
951
|
inner.limit !== undefined) {
|
|
667
952
|
throw new TypeError("A correlated scalar subquery cannot use GROUP BY, HAVING, ORDER BY, or LIMIT");
|
|
668
953
|
}
|
|
669
|
-
|
|
670
|
-
|
|
954
|
+
const keys = extractCorrelation(inner, scope, "scalar", true);
|
|
955
|
+
if (keys.some((key) => key.operator !== "=")) {
|
|
956
|
+
return decorrelateNonEqualityScalar(block, inner, item.expression, keys, nextAlias);
|
|
957
|
+
}
|
|
671
958
|
const alias = nextAlias();
|
|
672
959
|
const derived = {
|
|
673
960
|
sql: "(correlated scalar)",
|
|
674
961
|
base: inner.base,
|
|
675
962
|
joins: inner.joins,
|
|
676
963
|
select: [
|
|
677
|
-
...keys.map((key, index) => ({
|
|
678
|
-
|
|
964
|
+
...keys.map((key, index) => ({
|
|
965
|
+
expression: key.inner,
|
|
966
|
+
alias: correlationKeyAlias(index),
|
|
967
|
+
})),
|
|
968
|
+
{ expression: item.expression, alias: correlationValueAlias },
|
|
679
969
|
],
|
|
680
970
|
predicates: inner.predicates,
|
|
681
971
|
groupBy: keys.map((key) => key.inner),
|
|
@@ -683,18 +973,232 @@ function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
|
|
|
683
973
|
orderBy: [],
|
|
684
974
|
};
|
|
685
975
|
pushCorrelationJoin(block, alias, derived, keys, "left");
|
|
686
|
-
let value = {
|
|
687
|
-
|
|
976
|
+
let value = {
|
|
977
|
+
kind: "column",
|
|
978
|
+
reference: `${alias}.${correlationValueAlias}`,
|
|
979
|
+
};
|
|
980
|
+
if (item.expression.name === "COUNT") {
|
|
688
981
|
// COUNT over an empty group is 0, but the unmatched left-join side is NULL.
|
|
689
982
|
value = { kind: "call", name: "COALESCE", arguments: [value, { kind: "literal", value: 0 }] };
|
|
690
983
|
}
|
|
691
984
|
return value;
|
|
692
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Aggregates a range-correlated scalar subquery once per distinct outer probe tuple. The probe
|
|
988
|
+
* table prevents a general join from multiplying outer rows and lets the final join use equality
|
|
989
|
+
* keys even when the original correlation uses `<`, `>=`, or another comparison.
|
|
990
|
+
*/
|
|
991
|
+
function decorrelateNonEqualityScalar(block, inner, aggregate, keys, nextAlias) {
|
|
992
|
+
if (aggregate.aggregateOrderBy !== undefined) {
|
|
993
|
+
throw new TypeError("A range-correlated scalar aggregate cannot use aggregate ORDER BY");
|
|
994
|
+
}
|
|
995
|
+
const argument = aggregate.arguments[0];
|
|
996
|
+
if (aggregate.arguments.length !== 1 || argument === undefined) {
|
|
997
|
+
throw new TypeError("A correlated scalar aggregate must have exactly one argument");
|
|
998
|
+
}
|
|
999
|
+
const probes = outerProbeBlock(block, keys);
|
|
1000
|
+
const probesAlias = nextAlias();
|
|
1001
|
+
const innerRowsAlias = nextAlias();
|
|
1002
|
+
const innerRows = {
|
|
1003
|
+
sql: "(range-correlated scalar rows)",
|
|
1004
|
+
base: inner.base,
|
|
1005
|
+
joins: inner.joins,
|
|
1006
|
+
select: [
|
|
1007
|
+
...keys.map((key, index) => ({
|
|
1008
|
+
expression: key.inner,
|
|
1009
|
+
alias: correlationKeyAlias(index),
|
|
1010
|
+
})),
|
|
1011
|
+
{
|
|
1012
|
+
expression: argument.kind === "wildcard" ? { kind: "literal", value: 1 } : argument,
|
|
1013
|
+
alias: correlationValueAlias,
|
|
1014
|
+
},
|
|
1015
|
+
],
|
|
1016
|
+
predicates: inner.predicates,
|
|
1017
|
+
groupBy: [],
|
|
1018
|
+
having: [],
|
|
1019
|
+
orderBy: [],
|
|
1020
|
+
};
|
|
1021
|
+
const joinedAggregate = {
|
|
1022
|
+
...aggregate,
|
|
1023
|
+
arguments: [{ kind: "column", reference: `${innerRowsAlias}.${correlationValueAlias}` }],
|
|
1024
|
+
};
|
|
1025
|
+
const probeReferences = keys.map((_, index) => ({
|
|
1026
|
+
kind: "column",
|
|
1027
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
1028
|
+
}));
|
|
1029
|
+
const aggregateBlock = {
|
|
1030
|
+
sql: "(range-correlated scalar aggregate)",
|
|
1031
|
+
base: { table: probesAlias, alias: probesAlias, derived: probes },
|
|
1032
|
+
joins: [
|
|
1033
|
+
generalCorrelationJoin(innerRowsAlias, innerRows, keys.map((_, index) => ({
|
|
1034
|
+
kind: "column",
|
|
1035
|
+
reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
|
|
1036
|
+
})), probeReferences, keys.map(({ operator }) => operator)),
|
|
1037
|
+
],
|
|
1038
|
+
select: [
|
|
1039
|
+
...probeReferences.map((expression, index) => ({
|
|
1040
|
+
expression,
|
|
1041
|
+
alias: correlationKeyAlias(index),
|
|
1042
|
+
})),
|
|
1043
|
+
{ expression: joinedAggregate, alias: correlationValueAlias },
|
|
1044
|
+
],
|
|
1045
|
+
predicates: [],
|
|
1046
|
+
groupBy: probeReferences,
|
|
1047
|
+
having: [],
|
|
1048
|
+
orderBy: [],
|
|
1049
|
+
};
|
|
1050
|
+
const aggregateAlias = nextAlias();
|
|
1051
|
+
pushCorrelationJoin(block, aggregateAlias, aggregateBlock, keys.map((key) => ({ ...key, operator: "=" })), "left");
|
|
1052
|
+
let value = {
|
|
1053
|
+
kind: "column",
|
|
1054
|
+
reference: `${aggregateAlias}.${correlationValueAlias}`,
|
|
1055
|
+
};
|
|
1056
|
+
if (aggregate.name === "COUNT") {
|
|
1057
|
+
value = { kind: "call", name: "COALESCE", arguments: [value, { kind: "literal", value: 0 }] };
|
|
1058
|
+
}
|
|
1059
|
+
return value;
|
|
1060
|
+
}
|
|
1061
|
+
/** Builds the hidden boolean flag used when correlated EXISTS is nested below OR/NOT/CASE. */
|
|
1062
|
+
function decorrelateExistsExpression(block, exists, scope, nextAlias) {
|
|
1063
|
+
const inner = exists.block;
|
|
1064
|
+
rejectGroupedInner(inner, "EXISTS");
|
|
1065
|
+
const keys = extractCorrelation(inner, scope, "EXISTS", true);
|
|
1066
|
+
if (keys.every(({ operator }) => operator === "=")) {
|
|
1067
|
+
const flagsAlias = nextAlias();
|
|
1068
|
+
const flags = {
|
|
1069
|
+
sql: "(nested correlated exists flags)",
|
|
1070
|
+
base: inner.base,
|
|
1071
|
+
joins: inner.joins,
|
|
1072
|
+
select: [
|
|
1073
|
+
...keys.map((key, index) => ({
|
|
1074
|
+
expression: key.inner,
|
|
1075
|
+
alias: correlationKeyAlias(index),
|
|
1076
|
+
})),
|
|
1077
|
+
{ expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
|
|
1078
|
+
],
|
|
1079
|
+
predicates: inner.predicates,
|
|
1080
|
+
// EXISTS only needs one flag per correlation tuple. This is the direct equality-key path
|
|
1081
|
+
// used by auth scopes such as `all_access OR EXISTS (...)` and cannot multiply outer rows.
|
|
1082
|
+
groupBy: keys.map((key) => key.inner),
|
|
1083
|
+
having: [],
|
|
1084
|
+
orderBy: [],
|
|
1085
|
+
};
|
|
1086
|
+
pushCorrelationJoin(block, flagsAlias, flags, keys, "left");
|
|
1087
|
+
return {
|
|
1088
|
+
kind: "condition",
|
|
1089
|
+
operator: exists.negated ? "IS NULL" : "IS NOT NULL",
|
|
1090
|
+
left: { kind: "column", reference: `${flagsAlias}.${correlationMarkerAlias}` },
|
|
1091
|
+
right: { kind: "literal", value: null },
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
const probes = outerProbeBlock(block, keys);
|
|
1095
|
+
const probesAlias = nextAlias();
|
|
1096
|
+
const innerRowsAlias = nextAlias();
|
|
1097
|
+
const innerRows = {
|
|
1098
|
+
sql: "(nested correlated exists rows)",
|
|
1099
|
+
base: inner.base,
|
|
1100
|
+
joins: inner.joins,
|
|
1101
|
+
select: keys.map((key, index) => ({
|
|
1102
|
+
expression: key.inner,
|
|
1103
|
+
alias: correlationKeyAlias(index),
|
|
1104
|
+
})),
|
|
1105
|
+
predicates: inner.predicates,
|
|
1106
|
+
groupBy: keys.map((key) => key.inner),
|
|
1107
|
+
having: [],
|
|
1108
|
+
orderBy: [],
|
|
1109
|
+
};
|
|
1110
|
+
const probeReferences = keys.map((_, index) => ({
|
|
1111
|
+
kind: "column",
|
|
1112
|
+
reference: `${probesAlias}.${correlationProbeAlias(index)}`,
|
|
1113
|
+
}));
|
|
1114
|
+
const flags = {
|
|
1115
|
+
sql: "(nested correlated exists flags)",
|
|
1116
|
+
base: { table: probesAlias, alias: probesAlias, derived: probes },
|
|
1117
|
+
joins: [
|
|
1118
|
+
generalCorrelationJoin(innerRowsAlias, innerRows, keys.map((_, index) => ({
|
|
1119
|
+
kind: "column",
|
|
1120
|
+
reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
|
|
1121
|
+
})), probeReferences, keys.map(({ operator }) => operator)),
|
|
1122
|
+
],
|
|
1123
|
+
select: [
|
|
1124
|
+
...probeReferences.map((expression, index) => ({
|
|
1125
|
+
expression,
|
|
1126
|
+
alias: correlationKeyAlias(index),
|
|
1127
|
+
})),
|
|
1128
|
+
{ expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
|
|
1129
|
+
],
|
|
1130
|
+
predicates: [],
|
|
1131
|
+
groupBy: probeReferences,
|
|
1132
|
+
having: [],
|
|
1133
|
+
orderBy: [],
|
|
1134
|
+
};
|
|
1135
|
+
const flagsAlias = nextAlias();
|
|
1136
|
+
pushCorrelationJoin(block, flagsAlias, flags, keys.map((key) => ({ ...key, operator: "=" })), "left");
|
|
1137
|
+
return {
|
|
1138
|
+
kind: "condition",
|
|
1139
|
+
operator: exists.negated ? "IS NULL" : "IS NOT NULL",
|
|
1140
|
+
left: { kind: "column", reference: `${flagsAlias}.${correlationMarkerAlias}` },
|
|
1141
|
+
right: { kind: "literal", value: null },
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
/** Distinct outer key tuples, preserving the FROM joins needed by every referenced source. */
|
|
1145
|
+
function outerProbeBlock(block, keys) {
|
|
1146
|
+
const sources = [block.base, ...block.joins];
|
|
1147
|
+
const sourceIndexes = keys.map((key) => {
|
|
1148
|
+
if (key.outer.kind !== "column") {
|
|
1149
|
+
throw new TypeError("Correlation probes require qualified outer columns");
|
|
1150
|
+
}
|
|
1151
|
+
const alias = key.outer.reference.split(".")[0] ?? "";
|
|
1152
|
+
const index = sources.findIndex((source) => source.alias === alias);
|
|
1153
|
+
if (index < 0)
|
|
1154
|
+
throw new TypeError(`Cannot resolve correlated outer source: ${alias}`);
|
|
1155
|
+
return index;
|
|
1156
|
+
});
|
|
1157
|
+
const lastSource = Math.max(...sourceIndexes);
|
|
1158
|
+
const expressions = keys.map((key) => structuredClone(key.outer));
|
|
1159
|
+
return {
|
|
1160
|
+
sql: "(correlation probes)",
|
|
1161
|
+
base: structuredClone(block.base),
|
|
1162
|
+
joins: structuredClone(block.joins.slice(0, lastSource)),
|
|
1163
|
+
select: expressions.map((expression, index) => ({
|
|
1164
|
+
expression,
|
|
1165
|
+
alias: correlationProbeAlias(index),
|
|
1166
|
+
})),
|
|
1167
|
+
predicates: [],
|
|
1168
|
+
groupBy: expressions,
|
|
1169
|
+
having: [],
|
|
1170
|
+
orderBy: [],
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
/** A general ON join carrying one comparison for each corresponding expression pair. */
|
|
1174
|
+
function generalCorrelationJoin(alias, derived, inner, outer, operators) {
|
|
1175
|
+
let on;
|
|
1176
|
+
inner.forEach((left, index) => {
|
|
1177
|
+
const right = outer[index];
|
|
1178
|
+
const operator = operators[index];
|
|
1179
|
+
if (right === undefined || operator === undefined)
|
|
1180
|
+
return;
|
|
1181
|
+
const comparison = { kind: "condition", operator, left, right };
|
|
1182
|
+
on =
|
|
1183
|
+
on === undefined
|
|
1184
|
+
? comparison
|
|
1185
|
+
: { kind: "logical", operator: "and", left: on, right: comparison };
|
|
1186
|
+
});
|
|
1187
|
+
return {
|
|
1188
|
+
table: alias,
|
|
1189
|
+
alias,
|
|
1190
|
+
derived,
|
|
1191
|
+
kind: "inner",
|
|
1192
|
+
left: { kind: "literal", value: null },
|
|
1193
|
+
right: { kind: "literal", value: null },
|
|
1194
|
+
...(on === undefined ? {} : { on }),
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
693
1197
|
function pushCorrelationJoin(block, alias, derived, keys, kind) {
|
|
694
1198
|
const source = { table: alias, alias, derived };
|
|
695
1199
|
const keyReference = (index) => ({
|
|
696
1200
|
kind: "column",
|
|
697
|
-
reference: `${alias}
|
|
1201
|
+
reference: `${alias}.${correlationKeyAlias(index)}`,
|
|
698
1202
|
});
|
|
699
1203
|
const first = keys[0];
|
|
700
1204
|
if (keys.length === 1 && first?.operator === "=") {
|
|
@@ -766,7 +1270,8 @@ function extractCorrelation(inner, outerScope, label, comparisons = false) {
|
|
|
766
1270
|
const leftover = [];
|
|
767
1271
|
collectOutsideReferences(inner, new Set(), leftover);
|
|
768
1272
|
if (leftover.length > 0) {
|
|
769
|
-
|
|
1273
|
+
const supported = comparisons ? "comparison" : "equality";
|
|
1274
|
+
throw new TypeError(`Correlated ${label} subqueries support only ${supported} conditions between one inner and one outer qualified column (unsupported reference: ${leftover[0] ?? ""})`);
|
|
770
1275
|
}
|
|
771
1276
|
if (keys.length === 0) {
|
|
772
1277
|
throw new TypeError(`Correlated ${label} subquery has no usable correlation condition`);
|
|
@@ -822,11 +1327,6 @@ function rejectGroupedInner(inner, label) {
|
|
|
822
1327
|
throw new TypeError(`Correlated ${label} subqueries cannot use GROUP BY or HAVING`);
|
|
823
1328
|
}
|
|
824
1329
|
}
|
|
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
1330
|
function assertNoCorrelation(block) {
|
|
831
1331
|
const visit = (expression) => {
|
|
832
1332
|
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
@@ -1263,6 +1763,8 @@ function referencedOutputAliases(block, source, singleSource) {
|
|
|
1263
1763
|
for (const join of block.joins) {
|
|
1264
1764
|
visit(join.left);
|
|
1265
1765
|
visit(join.right);
|
|
1766
|
+
if (join.on !== undefined)
|
|
1767
|
+
visit(join.on);
|
|
1266
1768
|
}
|
|
1267
1769
|
for (const order of block.orderBy) {
|
|
1268
1770
|
// An ORDER BY reference may name an output alias of the outer block rather than a source
|