@minnowdb/core 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { dateIsoString, dateMilliseconds } from "../date-value.js";
2
- import { childExpressions, expressionAliases, forEachBlockExpression, forEachNestedBlock, isScalarFunctionName, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, volatileScalarFunctionNames, } from "./query.js";
2
+ import { blockHasSubqueries, childExpressions, DUAL_TABLE, expressionAliases, forEachBlockExpression, forEachNestedBlock, hasAggregate, isAggregateCall, isScalarFunctionName, mapBlockExpressions, mapChildExpressions, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, volatileScalarFunctionNames, } from "./query.js";
3
3
  import { isSqlDomainValue } from "./sql-domains.js";
4
4
  /**
5
5
  * Deterministic plan-to-plan rewrites over the shared compiled representation. Every rule
@@ -9,9 +9,117 @@ import { isSqlDomainValue } from "./sql-domains.js";
9
9
  */
10
10
  export function optimizePlan(plan) {
11
11
  const optimized = structuredClone(plan);
12
- 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);
@@ -371,16 +489,19 @@ function coalesceOrEqualityLists(block) {
371
489
  });
372
490
  }
373
491
  const comparisonOperators = new Set(["=", "!=", "<>", ">", ">=", "<", "<="]);
374
- const aggregateCallNames = new Set(["COUNT", "SUM", "AVG", "MIN", "MAX"]);
375
492
  const correlationKeyAlias = (index) => `\0correlation_key_${String(index)}`;
376
493
  const correlationProbeAlias = (index) => `\0correlation_probe_${String(index)}`;
494
+ const correlationInputAlias = (index) => `\0correlation_input_${String(index)}`;
495
+ const correlationOrderAlias = (index) => `\0correlation_order_${String(index)}`;
377
496
  const correlationValueAlias = "\0correlation_value";
378
497
  const correlationMarkerAlias = "\0correlation_match";
498
+ const correlationRowNumberAlias = "\0correlation_row_number";
379
499
  const correlationCountAlias = "\0correlation_count";
380
500
  const correlationNonNullCountAlias = "\0correlation_non_null_count";
381
- function decorrelateBlock(block) {
501
+ const correlationDecisionCountAlias = "\0correlation_decision_count";
502
+ const correlationTruthCountAlias = "\0correlation_truth_count";
503
+ function decorrelateBlock(block, nextAlias) {
382
504
  const scope = new Set([block.base.alias, ...block.joins.map((join) => join.alias)]);
383
- const nextAlias = correlationAliasFactory(scope);
384
505
  const rewritten = [];
385
506
  for (const predicate of block.predicates) {
386
507
  const replacement = decorrelatePredicate(block, predicate, scope, nextAlias);
@@ -390,8 +511,8 @@ function decorrelateBlock(block) {
390
511
  }
391
512
  block.predicates = rewritten.map((predicate) => ({
392
513
  ...predicate,
393
- left: rewriteCorrelatedExists(block, predicate.left, scope, nextAlias),
394
- right: rewriteCorrelatedExists(block, predicate.right, scope, nextAlias),
514
+ left: rewriteCorrelatedExpression(block, predicate.left, scope, nextAlias),
515
+ right: rewriteCorrelatedExpression(block, predicate.right, scope, nextAlias),
395
516
  }));
396
517
  const grouped = block.groupBy.length > 0 || block.select.some((item) => containsAggregateCall(item.expression));
397
518
  for (const item of block.select) {
@@ -405,7 +526,7 @@ function decorrelateBlock(block) {
405
526
  throw new TypeError("A grouped correlated select-list subquery must reference only GROUP BY columns and cannot be nested inside an aggregate");
406
527
  }
407
528
  }
408
- item.expression = rewriteCorrelatedScalars(block, item.expression, scope, nextAlias);
529
+ item.expression = rewriteCorrelatedExpression(block, item.expression, scope, nextAlias);
409
530
  // The decorrelated value is functionally determined by the outer grouping keys. Carrying it
410
531
  // as one additional key makes that dependency explicit to both grouped executors.
411
532
  if (grouped)
@@ -414,86 +535,69 @@ function decorrelateBlock(block) {
414
535
  assertNoCorrelation(block);
415
536
  }
416
537
  function containsAggregateCall(expression) {
417
- if (expression.kind === "call" && aggregateCallNames.has(expression.name))
418
- return true;
419
- return childExpressions(expression).some(containsAggregateCall);
538
+ return hasAggregate(expression);
420
539
  }
421
540
  function expressionHasCorrelatedSubquery(expression) {
422
- if (expression.kind === "subquery")
541
+ if (expression.kind === "subquery" || expression.kind === "exists") {
423
542
  return blockReferencesOutside(expression.block);
424
- if (expression.kind === "exists")
425
- return false;
543
+ }
426
544
  return childExpressions(expression).some(expressionHasCorrelatedSubquery);
427
545
  }
428
546
  /** Qualified outer references used by correlated scalar subqueries inside one expression. */
429
547
  function correlatedOuterReferences(expression) {
430
- if (expression.kind === "subquery" && blockReferencesOutside(expression.block)) {
548
+ if ((expression.kind === "subquery" || expression.kind === "exists") &&
549
+ blockReferencesOutside(expression.block)) {
431
550
  const references = [];
432
551
  collectOutsideReferences(expression.block, new Set(), references);
433
552
  return references;
434
553
  }
435
554
  return childExpressions(expression).flatMap(correlatedOuterReferences);
436
555
  }
437
- /** Replaces correlated scalar subqueries inside one select expression with decorrelated joins. */
438
- function rewriteCorrelatedScalars(block, expression, scope, nextAlias) {
439
- if (expression.kind === "subquery" && blockReferencesOutside(expression.block)) {
440
- return decorrelateScalarSubquery(block, expression, scope, nextAlias);
441
- }
442
- if (expression.kind === "binary" ||
443
- expression.kind === "condition" ||
444
- expression.kind === "logical") {
445
- expression.left = rewriteCorrelatedScalars(block, expression.left, scope, nextAlias);
446
- expression.right = rewriteCorrelatedScalars(block, expression.right, scope, nextAlias);
447
- return expression;
448
- }
449
- if (expression.kind === "call") {
450
- expression.arguments = expression.arguments.map((argument) => rewriteCorrelatedScalars(block, argument, scope, nextAlias));
451
- return expression;
452
- }
453
- if (expression.kind === "not") {
454
- expression.operand = rewriteCorrelatedScalars(block, expression.operand, scope, nextAlias);
455
- return expression;
556
+ /** Replaces every supported correlated subquery leaf inside an arbitrary expression. */
557
+ function rewriteCorrelatedExpression(block, expression, scope, nextAlias) {
558
+ if (expression.kind === "exists" && blockReferencesOutside(expression.block)) {
559
+ return decorrelateExistsExpression(block, expression, scope, nextAlias);
456
560
  }
457
- if (expression.kind === "case") {
458
- for (const branch of expression.branches) {
459
- branch.when = rewriteCorrelatedScalars(block, branch.when, scope, nextAlias);
460
- branch.then = rewriteCorrelatedScalars(block, branch.then, scope, nextAlias);
461
- }
462
- if (expression.otherwise !== undefined) {
463
- expression.otherwise = rewriteCorrelatedScalars(block, expression.otherwise, scope, nextAlias);
561
+ if (expression.kind === "condition") {
562
+ expression.left = rewriteCorrelatedExpression(block, expression.left, scope, nextAlias);
563
+ if (expression.right.kind === "subquery" && blockReferencesOutside(expression.right.block)) {
564
+ if (expression.operator === "IN" || expression.operator === "NOT IN") {
565
+ const value = decorrelateQuantifiedExpression(block, expression.left, expression.right.block, { comparison: "=", quantifier: "any" }, scope, nextAlias, "IN");
566
+ return expression.operator === "NOT IN" ? { kind: "not", operand: value } : value;
567
+ }
568
+ const quantified = parseQuantified(expression.operator);
569
+ if (quantified !== undefined) {
570
+ return decorrelateQuantifiedExpression(block, expression.left, expression.right.block, quantified, scope, nextAlias, "ANY/ALL");
571
+ }
464
572
  }
573
+ expression.right = rewriteCorrelatedExpression(block, expression.right, scope, nextAlias);
465
574
  return expression;
466
575
  }
467
- return expression;
468
- }
469
- /** Replaces correlated EXISTS nodes nested inside boolean expressions with hidden match flags. */
470
- function rewriteCorrelatedExists(block, expression, scope, nextAlias) {
471
- if (expression.kind === "exists" && blockReferencesOutside(expression.block)) {
472
- return decorrelateExistsExpression(block, expression, scope, nextAlias);
576
+ if (expression.kind === "subquery" && blockReferencesOutside(expression.block)) {
577
+ return decorrelateScalarSubquery(block, expression, scope, nextAlias);
473
578
  }
474
- if (expression.kind === "binary" ||
475
- expression.kind === "condition" ||
476
- expression.kind === "logical") {
477
- expression.left = rewriteCorrelatedExists(block, expression.left, scope, nextAlias);
478
- expression.right = rewriteCorrelatedExists(block, expression.right, scope, nextAlias);
579
+ if (expression.kind === "binary" || expression.kind === "logical") {
580
+ expression.left = rewriteCorrelatedExpression(block, expression.left, scope, nextAlias);
581
+ expression.right = rewriteCorrelatedExpression(block, expression.right, scope, nextAlias);
479
582
  return expression;
480
583
  }
481
584
  if (expression.kind === "call") {
482
- expression.arguments = expression.arguments.map((argument) => rewriteCorrelatedExists(block, argument, scope, nextAlias));
585
+ expression.arguments = expression.arguments.map((argument) => rewriteCorrelatedExpression(block, argument, scope, nextAlias));
483
586
  return expression;
484
587
  }
485
588
  if (expression.kind === "not") {
486
- expression.operand = rewriteCorrelatedExists(block, expression.operand, scope, nextAlias);
589
+ expression.operand = rewriteCorrelatedExpression(block, expression.operand, scope, nextAlias);
487
590
  return expression;
488
591
  }
489
592
  if (expression.kind === "case") {
490
593
  for (const branch of expression.branches) {
491
- branch.when = rewriteCorrelatedExists(block, branch.when, scope, nextAlias);
492
- branch.then = rewriteCorrelatedExists(block, branch.then, scope, nextAlias);
594
+ branch.when = rewriteCorrelatedExpression(block, branch.when, scope, nextAlias);
595
+ branch.then = rewriteCorrelatedExpression(block, branch.then, scope, nextAlias);
493
596
  }
494
597
  if (expression.otherwise !== undefined) {
495
- expression.otherwise = rewriteCorrelatedExists(block, expression.otherwise, scope, nextAlias);
598
+ expression.otherwise = rewriteCorrelatedExpression(block, expression.otherwise, scope, nextAlias);
496
599
  }
600
+ return expression;
497
601
  }
498
602
  return expression;
499
603
  }
@@ -503,8 +607,10 @@ function correlationAliasFactory(scope) {
503
607
  for (;;) {
504
608
  sequence += 1;
505
609
  const alias = `corr_${String(sequence)}`;
506
- if (!scope.has(alias))
610
+ if (!scope.has(alias)) {
611
+ scope.add(alias);
507
612
  return alias;
613
+ }
508
614
  }
509
615
  };
510
616
  }
@@ -525,6 +631,14 @@ function decorrelatePredicate(block, predicate, scope, nextAlias) {
525
631
  if (!blockReferencesOutside(inner))
526
632
  return undefined;
527
633
  rejectGroupedInner(inner, "EXISTS");
634
+ if (!canExtractCorrelation(inner, scope, "EXISTS", true)) {
635
+ const replacement = decorrelateExistsWithProbes(block, { ...node, negated }, scope, nextAlias);
636
+ return {
637
+ left: replacement,
638
+ operator: "IS TRUE",
639
+ right: { kind: "literal", value: true },
640
+ };
641
+ }
528
642
  const keys = extractCorrelation(inner, scope, "EXISTS", true);
529
643
  const alias = nextAlias();
530
644
  const derived = {
@@ -932,6 +1046,143 @@ function decorrelateRangeNotIn(block, probe, inner, item, keys, nextAlias) {
932
1046
  right: { kind: "literal", value: true },
933
1047
  };
934
1048
  }
1049
+ /**
1050
+ * Produces the three-valued result of a correlated IN/ANY/ALL expression at any expression
1051
+ * position. Distinct outer probe tuples join the correlated set once, and grouped counts retain
1052
+ * the difference between true, false, unknown, and an empty set. Top-level WHERE predicates keep
1053
+ * their cheaper semi/anti-join rewrites; this path exists for OR, NOT, CASE, and select items.
1054
+ */
1055
+ function decorrelateQuantifiedExpression(block, probe, inner, quantified, scope, nextAlias, label) {
1056
+ rejectGroupedInner(inner, label);
1057
+ const item = inner.select[0];
1058
+ if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
1059
+ throw new TypeError(label === "IN"
1060
+ ? "An IN subquery must select exactly one column"
1061
+ : "An ANY/ALL subquery must select exactly one column");
1062
+ }
1063
+ const keys = extractCorrelation(inner, scope, label, true);
1064
+ const outerExpressions = [
1065
+ ...keys.map((key) => structuredClone(key.outer)),
1066
+ structuredClone(probe),
1067
+ ];
1068
+ const probes = outerProbeBlockForExpressions(block, outerExpressions);
1069
+ const probesAlias = nextAlias();
1070
+ const probeReferences = outerExpressions.map((_, index) => ({
1071
+ kind: "column",
1072
+ reference: `${probesAlias}.${correlationProbeAlias(index)}`,
1073
+ }));
1074
+ const innerRowsAlias = nextAlias();
1075
+ const innerRows = {
1076
+ sql: `(nested correlated ${label.toLowerCase()} rows)`,
1077
+ base: inner.base,
1078
+ joins: inner.joins,
1079
+ select: [
1080
+ ...keys.map((key, index) => ({
1081
+ expression: key.inner,
1082
+ alias: correlationKeyAlias(index),
1083
+ })),
1084
+ { expression: item.expression, alias: correlationValueAlias },
1085
+ { expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
1086
+ ],
1087
+ predicates: inner.predicates,
1088
+ groupBy: [],
1089
+ having: [],
1090
+ orderBy: [],
1091
+ };
1092
+ const setJoin = generalCorrelationJoin(innerRowsAlias, innerRows, keys.map((_, index) => ({
1093
+ kind: "column",
1094
+ reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
1095
+ })), probeReferences.slice(0, keys.length), keys.map(({ operator }) => operator));
1096
+ setJoin.kind = "left";
1097
+ const comparison = {
1098
+ kind: "condition",
1099
+ operator: quantified.comparison,
1100
+ left: probeReferences[keys.length] ?? { kind: "literal", value: null },
1101
+ right: { kind: "column", reference: `${innerRowsAlias}.${correlationValueAlias}` },
1102
+ };
1103
+ const decisiveComparison = quantified.quantifier === "any"
1104
+ ? structuredClone(comparison)
1105
+ : { kind: "not", operand: structuredClone(comparison) };
1106
+ const truthMarker = {
1107
+ kind: "case",
1108
+ branches: [{ when: decisiveComparison, then: { kind: "literal", value: 1 } }],
1109
+ otherwise: { kind: "literal", value: null },
1110
+ };
1111
+ const flags = {
1112
+ sql: `(nested correlated ${label.toLowerCase()} flags)`,
1113
+ base: { table: probesAlias, alias: probesAlias, derived: probes },
1114
+ joins: [setJoin],
1115
+ select: [
1116
+ ...probeReferences.map((expression, index) => ({
1117
+ expression,
1118
+ alias: correlationKeyAlias(index),
1119
+ })),
1120
+ {
1121
+ expression: {
1122
+ kind: "call",
1123
+ name: "COUNT",
1124
+ arguments: [{ kind: "column", reference: `${innerRowsAlias}.${correlationMarkerAlias}` }],
1125
+ },
1126
+ alias: correlationCountAlias,
1127
+ },
1128
+ {
1129
+ expression: { kind: "call", name: "COUNT", arguments: [comparison] },
1130
+ alias: correlationDecisionCountAlias,
1131
+ },
1132
+ {
1133
+ expression: { kind: "call", name: "COUNT", arguments: [truthMarker] },
1134
+ alias: correlationTruthCountAlias,
1135
+ },
1136
+ ],
1137
+ predicates: [],
1138
+ groupBy: probeReferences,
1139
+ having: [],
1140
+ orderBy: [],
1141
+ };
1142
+ const flagsAlias = nextAlias();
1143
+ pushNullSafeProbeJoin(block, flagsAlias, flags, outerExpressions);
1144
+ const total = {
1145
+ kind: "column",
1146
+ reference: `${flagsAlias}.${correlationCountAlias}`,
1147
+ };
1148
+ const decisions = {
1149
+ kind: "column",
1150
+ reference: `${flagsAlias}.${correlationDecisionCountAlias}`,
1151
+ };
1152
+ const truths = {
1153
+ kind: "column",
1154
+ reference: `${flagsAlias}.${correlationTruthCountAlias}`,
1155
+ };
1156
+ const defaultValue = quantified.quantifier === "all";
1157
+ return {
1158
+ kind: "case",
1159
+ branches: [
1160
+ {
1161
+ when: {
1162
+ kind: "condition",
1163
+ operator: ">",
1164
+ left: truths,
1165
+ right: { kind: "literal", value: 0 },
1166
+ },
1167
+ then: { kind: "literal", value: !defaultValue },
1168
+ },
1169
+ {
1170
+ when: {
1171
+ kind: "condition",
1172
+ operator: "=",
1173
+ left: total,
1174
+ right: { kind: "literal", value: 0 },
1175
+ },
1176
+ then: { kind: "literal", value: defaultValue },
1177
+ },
1178
+ {
1179
+ when: { kind: "condition", operator: "<", left: decisions, right: total },
1180
+ then: { kind: "literal", value: null },
1181
+ },
1182
+ ],
1183
+ otherwise: { kind: "literal", value: defaultValue },
1184
+ };
1185
+ }
935
1186
  /**
936
1187
  * Rewrites one correlated scalar-aggregate subquery into a left join against the grouped
937
1188
  * aggregate, returning the expression that replaces the subquery node (COUNT wraps in COALESCE
@@ -939,17 +1190,25 @@ function decorrelateRangeNotIn(block, probe, inner, item, keys, nextAlias) {
939
1190
  */
940
1191
  function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
941
1192
  const inner = subquery.block;
1193
+ const passthrough = scalarPassthrough(inner);
1194
+ if (passthrough !== undefined)
1195
+ return passthrough;
942
1196
  const item = inner.select[0];
943
1197
  if (inner.select.length !== 1 ||
944
- item?.expression.kind !== "call" ||
945
- !aggregateCallNames.has(item.expression.name)) {
946
- throw new TypeError("A correlated scalar subquery must select exactly one aggregate expression");
947
- }
948
- if (inner.groupBy.length > 0 ||
1198
+ item === undefined ||
1199
+ !isAggregateCall(item.expression) ||
1200
+ inner.groupBy.length > 0 ||
949
1201
  inner.having.length > 0 ||
950
1202
  inner.orderBy.length > 0 ||
951
- inner.limit !== undefined) {
952
- 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);
953
1212
  }
954
1213
  const keys = extractCorrelation(inner, scope, "scalar", true);
955
1214
  if (keys.some((key) => key.operator !== "=")) {
@@ -983,6 +1242,347 @@ function decorrelateScalarSubquery(block, subquery, scope, nextAlias) {
983
1242
  }
984
1243
  return value;
985
1244
  }
1245
+ /** A SELECT without a row source is already the scalar expression from its enclosing scope. */
1246
+ function scalarPassthrough(inner) {
1247
+ const item = inner.select[0];
1248
+ if (inner.base.table !== DUAL_TABLE ||
1249
+ inner.base.derived !== undefined ||
1250
+ inner.joins.length > 0 ||
1251
+ inner.select.length !== 1 ||
1252
+ item === undefined ||
1253
+ inner.predicates.length > 0 ||
1254
+ inner.groupBy.length > 0 ||
1255
+ inner.having.length > 0 ||
1256
+ inner.orderBy.length > 0 ||
1257
+ inner.offset !== undefined ||
1258
+ (inner.limit !== undefined && inner.limit < 1)) {
1259
+ return undefined;
1260
+ }
1261
+ return structuredClone(item.expression);
1262
+ }
1263
+ /**
1264
+ * Pulls a scalar expression through projection-only derived wrappers. The parser uses these
1265
+ * wrappers to hide ORDER BY support columns, and Kysely's JSON helpers use one to turn an
1266
+ * explicitly selected row into JSON. Row-affecting clauses remain on the deepest block, where
1267
+ * the probe rewrite can apply them once per outer key.
1268
+ */
1269
+ function scalarShape(inner) {
1270
+ const item = inner.select[0];
1271
+ if (inner.select.length !== 1 || item === undefined || item.expression.kind === "wildcard") {
1272
+ throw new TypeError("A scalar subquery must select exactly one column");
1273
+ }
1274
+ let rows = inner;
1275
+ let expression = structuredClone(item.expression);
1276
+ for (;;) {
1277
+ const derived = rows.base.derived;
1278
+ if (derived === undefined ||
1279
+ rows.joins.length > 0 ||
1280
+ rows.predicates.length > 0 ||
1281
+ rows.groupBy.length > 0 ||
1282
+ rows.having.length > 0 ||
1283
+ rows.orderBy.length > 0 ||
1284
+ rows.limit !== undefined ||
1285
+ rows.limitParameter !== undefined ||
1286
+ rows.offset !== undefined ||
1287
+ rows.offsetParameter !== undefined ||
1288
+ rows.distinctWildcard === true) {
1289
+ break;
1290
+ }
1291
+ expression = inlineDerivedProjection(expression, rows.base.alias, derived);
1292
+ rows = derived;
1293
+ }
1294
+ return { rows, expression };
1295
+ }
1296
+ /** Replaces references to a derived row with the expressions that produced its named columns. */
1297
+ function inlineDerivedProjection(expression, alias, derived) {
1298
+ const rewrite = (node) => {
1299
+ if (node.kind === "column") {
1300
+ const parts = node.reference.split(".");
1301
+ if (parts.length === 2 && parts[0] !== alias)
1302
+ return node;
1303
+ const name = parts.length === 2 ? (parts[1] ?? "") : (parts[0] ?? "");
1304
+ const matches = derived.select.filter((item) => item.alias === name);
1305
+ if (matches.length === 1)
1306
+ return structuredClone(matches[0]?.expression ?? node);
1307
+ return node;
1308
+ }
1309
+ return mapChildExpressions(node, rewrite);
1310
+ };
1311
+ return rewrite(expression);
1312
+ }
1313
+ /** ORDER BY may name a hidden or visible select alias; recover its row-level expression. */
1314
+ function scalarOrderExpression(expression, rows) {
1315
+ if (expression.kind !== "column" || expression.reference.includes(".")) {
1316
+ return structuredClone(expression);
1317
+ }
1318
+ const matches = rows.select.filter((item) => item.alias === expression.reference);
1319
+ return matches.length === 1
1320
+ ? structuredClone(matches[0]?.expression ?? expression)
1321
+ : structuredClone(expression);
1322
+ }
1323
+ /** The value a scalar aggregate expression produces for an empty correlated input. */
1324
+ function emptyScalarExpression(expression) {
1325
+ if (isAggregateCall(expression)) {
1326
+ return { kind: "literal", value: expression.name === "COUNT" ? 0 : null };
1327
+ }
1328
+ if (expression.kind === "column" || expression.kind === "wildcard") {
1329
+ return { kind: "literal", value: null };
1330
+ }
1331
+ return mapChildExpressions(expression, emptyScalarExpression);
1332
+ }
1333
+ /**
1334
+ * General correlated scalar rewrite. Distinct outer probe tuples join the inner rows once,
1335
+ * ORDER BY/LIMIT rank within each probe, and one grouped result joins back by NULL-safe equality.
1336
+ * Aggregate expressions retain their empty-input rules; ordinary scalar rows use the internal
1337
+ * single-value aggregate, which raises the same cardinality error as an uncorrelated scalar.
1338
+ */
1339
+ function decorrelateGeneralScalar(block, inner, scope, nextAlias) {
1340
+ const shape = scalarShape(inner);
1341
+ const rows = shape.rows;
1342
+ if (rows.groupBy.length > 0 || rows.having.length > 0) {
1343
+ throw new TypeError("A correlated scalar subquery cannot use GROUP BY or HAVING");
1344
+ }
1345
+ if (rows.limitWithTies === true && rows.orderBy.length === 0) {
1346
+ throw new TypeError("A correlated scalar WITH TIES query requires ORDER BY");
1347
+ }
1348
+ const keys = extractCorrelation(rows, scope, "scalar", true);
1349
+ const outerExpressions = keys.map((key) => structuredClone(key.outer));
1350
+ const probes = outerProbeBlockForExpressions(block, outerExpressions);
1351
+ const probesAlias = nextAlias();
1352
+ const probeReferences = outerExpressions.map((_, index) => ({
1353
+ kind: "column",
1354
+ reference: `${probesAlias}.${correlationProbeAlias(index)}`,
1355
+ }));
1356
+ const finalRowsAlias = nextAlias();
1357
+ const projected = [];
1358
+ const project = (expression, alias) => {
1359
+ projected.push({ expression: structuredClone(expression), alias });
1360
+ return { kind: "column", reference: `${finalRowsAlias}.${alias}` };
1361
+ };
1362
+ const orderInputs = rows.orderBy.map((order, index) => ({
1363
+ reference: project(scalarOrderExpression(order.expression, rows), correlationOrderAlias(index)),
1364
+ direction: order.direction,
1365
+ ...(order.nulls === undefined ? {} : { nulls: order.nulls }),
1366
+ }));
1367
+ let inputSequence = 0;
1368
+ const aggregateResult = (expression) => {
1369
+ if (isAggregateCall(expression)) {
1370
+ const arguments_ = expression.arguments.map((argument) => argument.kind === "wildcard"
1371
+ ? structuredClone(argument)
1372
+ : project(argument, correlationInputAlias(inputSequence++)));
1373
+ const ownOrder = expression.aggregateOrderBy?.map((order) => ({
1374
+ expression: project(order.expression, correlationInputAlias(inputSequence++)),
1375
+ direction: order.direction,
1376
+ ...(order.nulls === undefined ? {} : { nulls: order.nulls }),
1377
+ }));
1378
+ const inheritedOrder = expression.name === "JSON_ARRAYAGG" || expression.name === "STRING_AGG"
1379
+ ? orderInputs.map(({ reference, direction, nulls }) => ({
1380
+ expression: structuredClone(reference),
1381
+ direction,
1382
+ ...(nulls === undefined ? {} : { nulls }),
1383
+ }))
1384
+ : undefined;
1385
+ const { aggregateOrderBy: ignoredOrder, ...base } = expression;
1386
+ void ignoredOrder;
1387
+ const aggregateOrder = ownOrder !== undefined && ownOrder.length > 0
1388
+ ? ownOrder
1389
+ : inheritedOrder !== undefined && inheritedOrder.length > 0
1390
+ ? inheritedOrder
1391
+ : undefined;
1392
+ const rewritten = {
1393
+ ...base,
1394
+ arguments: arguments_,
1395
+ ...(aggregateOrder === undefined ? {} : { aggregateOrderBy: aggregateOrder }),
1396
+ };
1397
+ return rewritten;
1398
+ }
1399
+ if (expression.kind === "column" || expression.kind === "wildcard") {
1400
+ throw new TypeError("A correlated scalar aggregate cannot select an ungrouped inner column");
1401
+ }
1402
+ if (expression.kind === "subquery" || expression.kind === "exists") {
1403
+ throw new TypeError("A correlated scalar aggregate cannot contain another subquery");
1404
+ }
1405
+ return mapChildExpressions(expression, aggregateResult);
1406
+ };
1407
+ const grouped = hasAggregate(shape.expression);
1408
+ const valueExpression = grouped
1409
+ ? aggregateResult(shape.expression)
1410
+ : {
1411
+ kind: "call",
1412
+ name: "MINNOW_SINGLE_VALUE",
1413
+ arguments: [project(shape.expression, correlationValueAlias)],
1414
+ };
1415
+ const innerRowsAlias = nextAlias();
1416
+ const rawRows = {
1417
+ sql: "(correlated scalar input rows)",
1418
+ base: rows.base,
1419
+ joins: rows.joins,
1420
+ select: [
1421
+ ...keys.map((key, index) => ({
1422
+ expression: key.inner,
1423
+ alias: correlationKeyAlias(index),
1424
+ })),
1425
+ ...projected,
1426
+ ],
1427
+ predicates: rows.predicates,
1428
+ groupBy: [],
1429
+ having: [],
1430
+ orderBy: [],
1431
+ };
1432
+ const matched = {
1433
+ sql: "(correlated scalar matched probes)",
1434
+ base: { table: probesAlias, alias: probesAlias, derived: probes },
1435
+ joins: [
1436
+ generalCorrelationJoin(innerRowsAlias, rawRows, keys.map((_, index) => ({
1437
+ kind: "column",
1438
+ reference: `${innerRowsAlias}.${correlationKeyAlias(index)}`,
1439
+ })), probeReferences, keys.map(({ operator }) => operator)),
1440
+ ],
1441
+ select: [
1442
+ ...probeReferences.map((expression, index) => ({
1443
+ expression,
1444
+ alias: correlationKeyAlias(index),
1445
+ })),
1446
+ ...projected.map(({ alias }) => ({
1447
+ expression: { kind: "column", reference: `${innerRowsAlias}.${alias}` },
1448
+ alias,
1449
+ })),
1450
+ ],
1451
+ predicates: [],
1452
+ groupBy: [],
1453
+ having: [],
1454
+ orderBy: [],
1455
+ };
1456
+ let finalRows = matched;
1457
+ if (rows.limit !== undefined ||
1458
+ rows.limitParameter !== undefined ||
1459
+ rows.offset !== undefined ||
1460
+ rows.offsetParameter !== undefined) {
1461
+ const rankedAlias = nextAlias();
1462
+ const rankedTable = nextAlias();
1463
+ const ranked = {
1464
+ table: rankedTable,
1465
+ alias: rankedAlias,
1466
+ windowed: {
1467
+ block: matched,
1468
+ windows: [
1469
+ {
1470
+ alias: correlationRowNumberAlias,
1471
+ name: rows.limitWithTies === true ? "RANK" : "ROW_NUMBER",
1472
+ partitionAliases: keys.map((_, index) => correlationKeyAlias(index)),
1473
+ orderAliases: rows.orderBy.map((order, index) => ({
1474
+ alias: correlationOrderAlias(index),
1475
+ direction: order.direction,
1476
+ ...(order.nulls === undefined ? {} : { nulls: order.nulls }),
1477
+ })),
1478
+ },
1479
+ ],
1480
+ },
1481
+ };
1482
+ const rowNumber = {
1483
+ kind: "column",
1484
+ reference: `${rankedAlias}.${correlationRowNumberAlias}`,
1485
+ };
1486
+ const offset = rows.offsetParameter === undefined
1487
+ ? { kind: "literal", value: rows.offset ?? 0 }
1488
+ : { kind: "parameter", index: rows.offsetParameter };
1489
+ const predicates = [];
1490
+ if ((rows.offset ?? 0) > 0 || rows.offsetParameter !== undefined) {
1491
+ predicates.push({
1492
+ left: rowNumber,
1493
+ operator: ">",
1494
+ right: offset,
1495
+ });
1496
+ }
1497
+ if (rows.limit !== undefined || rows.limitParameter !== undefined) {
1498
+ const limit = rows.limitParameter === undefined
1499
+ ? { kind: "literal", value: rows.limit ?? 0 }
1500
+ : { kind: "parameter", index: rows.limitParameter };
1501
+ predicates.push({
1502
+ left: structuredClone(rowNumber),
1503
+ operator: "<=",
1504
+ right: rows.offset === undefined && rows.offsetParameter === undefined
1505
+ ? limit
1506
+ : { kind: "binary", operator: "+", left: structuredClone(offset), right: limit },
1507
+ });
1508
+ }
1509
+ finalRows = {
1510
+ sql: "(correlated scalar limited rows)",
1511
+ base: ranked,
1512
+ joins: [],
1513
+ select: [
1514
+ ...keys.map((_, index) => ({
1515
+ expression: {
1516
+ kind: "column",
1517
+ reference: `${rankedAlias}.${correlationKeyAlias(index)}`,
1518
+ },
1519
+ alias: correlationKeyAlias(index),
1520
+ })),
1521
+ ...projected.map(({ alias }) => ({
1522
+ expression: { kind: "column", reference: `${rankedAlias}.${alias}` },
1523
+ alias,
1524
+ })),
1525
+ ],
1526
+ predicates,
1527
+ groupBy: [],
1528
+ having: [],
1529
+ orderBy: [],
1530
+ ...(rows.limitParameter === undefined
1531
+ ? {}
1532
+ : { limitValidationParameters: [rows.limitParameter] }),
1533
+ ...(rows.offsetParameter === undefined
1534
+ ? {}
1535
+ : { offsetValidationParameters: [rows.offsetParameter] }),
1536
+ };
1537
+ }
1538
+ const finalProbeReferences = keys.map((_, index) => ({
1539
+ kind: "column",
1540
+ reference: `${finalRowsAlias}.${correlationKeyAlias(index)}`,
1541
+ }));
1542
+ const aggregate = {
1543
+ sql: "(correlated scalar result)",
1544
+ base: { table: finalRowsAlias, alias: finalRowsAlias, derived: finalRows },
1545
+ joins: [],
1546
+ select: [
1547
+ ...finalProbeReferences.map((expression, index) => ({
1548
+ expression,
1549
+ alias: correlationKeyAlias(index),
1550
+ })),
1551
+ { expression: valueExpression, alias: correlationValueAlias },
1552
+ { expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
1553
+ ],
1554
+ predicates: [],
1555
+ groupBy: finalProbeReferences,
1556
+ having: [],
1557
+ orderBy: [],
1558
+ };
1559
+ const resultAlias = nextAlias();
1560
+ pushNullSafeProbeJoin(block, resultAlias, aggregate, outerExpressions);
1561
+ const marker = {
1562
+ kind: "column",
1563
+ reference: `${resultAlias}.${correlationMarkerAlias}`,
1564
+ };
1565
+ return {
1566
+ kind: "case",
1567
+ branches: [
1568
+ {
1569
+ when: {
1570
+ kind: "condition",
1571
+ operator: "IS NULL",
1572
+ left: marker,
1573
+ right: { kind: "literal", value: null },
1574
+ },
1575
+ then: grouped
1576
+ ? emptyScalarExpression(structuredClone(shape.expression))
1577
+ : { kind: "literal", value: null },
1578
+ },
1579
+ ],
1580
+ otherwise: {
1581
+ kind: "column",
1582
+ reference: `${resultAlias}.${correlationValueAlias}`,
1583
+ },
1584
+ };
1585
+ }
986
1586
  /**
987
1587
  * Aggregates a range-correlated scalar subquery once per distinct outer probe tuple. The probe
988
1588
  * table prevents a general join from multiplying outer rows and lets the final join use equality
@@ -1062,6 +1662,9 @@ function decorrelateNonEqualityScalar(block, inner, aggregate, keys, nextAlias)
1062
1662
  function decorrelateExistsExpression(block, exists, scope, nextAlias) {
1063
1663
  const inner = exists.block;
1064
1664
  rejectGroupedInner(inner, "EXISTS");
1665
+ if (!canExtractCorrelation(inner, scope, "EXISTS", true)) {
1666
+ return decorrelateExistsWithProbes(block, exists, scope, nextAlias);
1667
+ }
1065
1668
  const keys = extractCorrelation(inner, scope, "EXISTS", true);
1066
1669
  if (keys.every(({ operator }) => operator === "=")) {
1067
1670
  const flagsAlias = nextAlias();
@@ -1141,21 +1744,151 @@ function decorrelateExistsExpression(block, exists, scope, nextAlias) {
1141
1744
  right: { kind: "literal", value: null },
1142
1745
  };
1143
1746
  }
1747
+ /**
1748
+ * Decorrelation fallback for an EXISTS whose outer values occur below another subquery or in a
1749
+ * larger expression instead of as direct inner-column/outer-column predicates. Distinct outer
1750
+ * probe tuples become an ordinary derived source of the EXISTS body, and every outer reference
1751
+ * is rebound to that source. The whole nested tree can then use the normal recursive rewrites.
1752
+ */
1753
+ function decorrelateExistsWithProbes(block, exists, scope, nextAlias) {
1754
+ const inner = exists.block;
1755
+ const outside = [];
1756
+ collectOutsideReferences(inner, new Set(), outside);
1757
+ const references = [...new Set(outside)];
1758
+ if (references.length === 0) {
1759
+ throw new TypeError("Correlated EXISTS subquery has no usable outer reference");
1760
+ }
1761
+ for (const reference of references) {
1762
+ const parts = reference.split(".");
1763
+ const alias = parts.length === 2 ? parts[0] : undefined;
1764
+ if (alias === undefined || !scope.has(alias)) {
1765
+ throw new TypeError(`Correlated EXISTS references an unavailable outer source: ${reference}`);
1766
+ }
1767
+ }
1768
+ const outerExpressions = references.map((reference) => ({
1769
+ kind: "column",
1770
+ reference,
1771
+ }));
1772
+ const probes = outerProbeBlockForExpressions(block, outerExpressions);
1773
+ const probesAlias = nextAlias();
1774
+ const probeReferences = references.map((_, index) => ({
1775
+ kind: "column",
1776
+ reference: `${probesAlias}.${correlationProbeAlias(index)}`,
1777
+ }));
1778
+ replacePlanReferences(inner, new Map(references.map((reference, index) => [reference, probeReferences[index] ?? null])));
1779
+ inner.joins.push({
1780
+ table: probesAlias,
1781
+ alias: probesAlias,
1782
+ derived: probes,
1783
+ kind: "inner",
1784
+ left: { kind: "literal", value: null },
1785
+ right: { kind: "literal", value: null },
1786
+ on: {
1787
+ kind: "condition",
1788
+ operator: "=",
1789
+ left: { kind: "literal", value: 1 },
1790
+ right: { kind: "literal", value: 1 },
1791
+ },
1792
+ });
1793
+ const flagsAlias = nextAlias();
1794
+ const flags = {
1795
+ sql: "(probe-lifted correlated exists flags)",
1796
+ base: inner.base,
1797
+ joins: inner.joins,
1798
+ select: [
1799
+ ...probeReferences.map((expression, index) => ({
1800
+ expression,
1801
+ alias: correlationKeyAlias(index),
1802
+ })),
1803
+ { expression: { kind: "literal", value: 1 }, alias: correlationMarkerAlias },
1804
+ ],
1805
+ predicates: inner.predicates,
1806
+ groupBy: probeReferences,
1807
+ having: [],
1808
+ orderBy: [],
1809
+ };
1810
+ const flagKeys = outerExpressions.map((_, index) => ({
1811
+ kind: "column",
1812
+ reference: `${flagsAlias}.${correlationKeyAlias(index)}`,
1813
+ }));
1814
+ const probeConditions = outerExpressions.map((outer, index) => ({
1815
+ kind: "condition",
1816
+ operator: "IS NOT DISTINCT FROM",
1817
+ left: outer,
1818
+ right: flagKeys[index] ?? { kind: "literal", value: null },
1819
+ }));
1820
+ let probeJoin = probeConditions[0];
1821
+ if (probeJoin === undefined)
1822
+ throw new Error("Correlation probe join is missing its key");
1823
+ for (const condition of probeConditions.slice(1)) {
1824
+ probeJoin = {
1825
+ kind: "logical",
1826
+ operator: "and",
1827
+ left: probeJoin,
1828
+ right: condition,
1829
+ };
1830
+ }
1831
+ block.joins.push({
1832
+ table: flagsAlias,
1833
+ alias: flagsAlias,
1834
+ derived: flags,
1835
+ kind: "left",
1836
+ left: { kind: "literal", value: null },
1837
+ right: { kind: "literal", value: null },
1838
+ on: probeJoin,
1839
+ });
1840
+ return {
1841
+ kind: "condition",
1842
+ operator: exists.negated ? "IS NULL" : "IS NOT NULL",
1843
+ left: { kind: "column", reference: `${flagsAlias}.${correlationMarkerAlias}` },
1844
+ right: { kind: "literal", value: null },
1845
+ };
1846
+ }
1847
+ /** Rebinds qualified references everywhere below one plan without touching the probe definition. */
1848
+ function replacePlanReferences(block, replacements) {
1849
+ const rewrite = (expression) => {
1850
+ if (expression.kind === "column") {
1851
+ const replacement = replacements.get(expression.reference);
1852
+ return replacement == null ? expression : structuredClone(replacement);
1853
+ }
1854
+ if (expression.kind === "subquery" || expression.kind === "exists") {
1855
+ replacePlanReferences(expression.block, replacements);
1856
+ return expression;
1857
+ }
1858
+ if (expression.kind === "window") {
1859
+ expression.partitionBy = expression.partitionBy.map(rewrite);
1860
+ for (const order of expression.orderBy)
1861
+ order.expression = rewrite(order.expression);
1862
+ if (expression.argument !== undefined)
1863
+ expression.argument = rewrite(expression.argument);
1864
+ return expression;
1865
+ }
1866
+ return mapChildExpressions(expression, rewrite);
1867
+ };
1868
+ mapBlockExpressions(block, rewrite);
1869
+ forEachNestedBlock(block, (nested) => replacePlanReferences(nested, replacements));
1870
+ }
1144
1871
  /** Distinct outer key tuples, preserving the FROM joins needed by every referenced source. */
1145
1872
  function outerProbeBlock(block, keys) {
1873
+ return outerProbeBlockForExpressions(block, keys.map((key) => key.outer));
1874
+ }
1875
+ function outerProbeBlockForExpressions(block, outerExpressions) {
1146
1876
  const sources = [block.base, ...block.joins];
1147
- const sourceIndexes = 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;
1877
+ const sourceIndexes = outerExpressions.flatMap((expression) => {
1878
+ const aliases = [...expressionAliases(expression)];
1879
+ // An unqualified expression can resolve against any visible source. Retaining the whole
1880
+ // prefix is conservative and preserves SQL name resolution without a catalog dependency.
1881
+ if (aliases.length === 0)
1882
+ return [sources.length - 1];
1883
+ return aliases.map((alias) => {
1884
+ const index = sources.findIndex((source) => source.alias === alias);
1885
+ if (index < 0)
1886
+ throw new TypeError(`Cannot resolve correlated outer source: ${alias}`);
1887
+ return index;
1888
+ });
1156
1889
  });
1157
1890
  const lastSource = Math.max(...sourceIndexes);
1158
- const expressions = keys.map((key) => structuredClone(key.outer));
1891
+ const expressions = outerExpressions.map((expression) => structuredClone(expression));
1159
1892
  return {
1160
1893
  sql: "(correlation probes)",
1161
1894
  base: structuredClone(block.base),
@@ -1170,6 +1903,30 @@ function outerProbeBlock(block, keys) {
1170
1903
  orderBy: [],
1171
1904
  };
1172
1905
  }
1906
+ /** Joins one row per outer probe tuple back with NULL-safe equality. */
1907
+ function pushNullSafeProbeJoin(block, alias, derived, outerExpressions) {
1908
+ const conditions = outerExpressions.map((outer, index) => ({
1909
+ kind: "condition",
1910
+ operator: "IS NOT DISTINCT FROM",
1911
+ left: structuredClone(outer),
1912
+ right: { kind: "column", reference: `${alias}.${correlationKeyAlias(index)}` },
1913
+ }));
1914
+ let on = conditions[0];
1915
+ if (on === undefined)
1916
+ throw new Error("Correlation probe join is missing its key");
1917
+ for (const condition of conditions.slice(1)) {
1918
+ on = { kind: "logical", operator: "and", left: on, right: condition };
1919
+ }
1920
+ block.joins.push({
1921
+ table: alias,
1922
+ alias,
1923
+ derived,
1924
+ kind: "left",
1925
+ left: { kind: "literal", value: null },
1926
+ right: { kind: "literal", value: null },
1927
+ on,
1928
+ });
1929
+ }
1173
1930
  /** A general ON join carrying one comparison for each corresponding expression pair. */
1174
1931
  function generalCorrelationJoin(alias, derived, inner, outer, operators) {
1175
1932
  let on;
@@ -1278,6 +2035,18 @@ function extractCorrelation(inner, outerScope, label, comparisons = false) {
1278
2035
  }
1279
2036
  return keys;
1280
2037
  }
2038
+ /** Whether the fast correlation-key path can consume every outside reference in this block. */
2039
+ function canExtractCorrelation(inner, outerScope, label, comparisons) {
2040
+ try {
2041
+ extractCorrelation(structuredClone(inner), outerScope, label, comparisons);
2042
+ return true;
2043
+ }
2044
+ catch (error) {
2045
+ if (error instanceof TypeError)
2046
+ return false;
2047
+ throw error;
2048
+ }
2049
+ }
1281
2050
  function correlationComparison(predicate, innerScope, outerScope, comparisons) {
1282
2051
  if (predicate.operator !== "=" &&
1283
2052
  (!comparisons || !comparisonOperators.has(predicate.operator))) {
@@ -1733,8 +2502,10 @@ function referencedOutputAliases(block, source, singleSource) {
1733
2502
  if (expression.otherwise !== undefined)
1734
2503
  visit(expression.otherwise);
1735
2504
  }
1736
- else if (expression.kind === "call")
2505
+ else if (expression.kind === "call") {
1737
2506
  expression.arguments.forEach(visit);
2507
+ expression.aggregateOrderBy?.forEach((order) => visit(order.expression));
2508
+ }
1738
2509
  else if (expression.kind === "list")
1739
2510
  expression.items.forEach(visit);
1740
2511
  else if (expression.kind === "fts") {