@minnowdb/core 0.5.0 → 0.6.1

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