@quereus/quereus 4.16.0 → 4.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/database-auto-analyze.d.ts +258 -0
- package/dist/src/core/database-auto-analyze.d.ts.map +1 -0
- package/dist/src/core/database-auto-analyze.js +616 -0
- package/dist/src/core/database-auto-analyze.js.map +1 -0
- package/dist/src/core/database-materialized-views.d.ts +42 -1
- package/dist/src/core/database-materialized-views.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views.js +76 -3
- package/dist/src/core/database-materialized-views.js.map +1 -1
- package/dist/src/core/database-transaction.d.ts +17 -0
- package/dist/src/core/database-transaction.d.ts.map +1 -1
- package/dist/src/core/database-transaction.js +37 -0
- package/dist/src/core/database-transaction.js.map +1 -1
- package/dist/src/core/database.d.ts +8 -1
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +135 -5
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
- package/dist/src/planner/analysis/change-scope.js +8 -8
- package/dist/src/planner/analysis/change-scope.js.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.js +27 -14
- package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
- package/dist/src/planner/analysis/expression-fingerprint.d.ts.map +1 -1
- package/dist/src/planner/analysis/expression-fingerprint.js +23 -2
- package/dist/src/planner/analysis/expression-fingerprint.js.map +1 -1
- package/dist/src/planner/analysis/mv-body-functions.d.ts +46 -0
- package/dist/src/planner/analysis/mv-body-functions.d.ts.map +1 -0
- package/dist/src/planner/analysis/mv-body-functions.js +65 -0
- package/dist/src/planner/analysis/mv-body-functions.js.map +1 -0
- package/dist/src/planner/analysis/predicate-shape.d.ts +32 -4
- package/dist/src/planner/analysis/predicate-shape.d.ts.map +1 -1
- package/dist/src/planner/analysis/predicate-shape.js +37 -5
- package/dist/src/planner/analysis/predicate-shape.js.map +1 -1
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts +2 -2
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts.map +1 -1
- package/dist/src/planner/analysis/query-rewrite-matcher.js +46 -6
- package/dist/src/planner/analysis/query-rewrite-matcher.js.map +1 -1
- package/dist/src/planner/analysis/sat-checker.js +2 -5
- package/dist/src/planner/analysis/sat-checker.js.map +1 -1
- package/dist/src/planner/building/select-aggregates.d.ts +17 -5
- package/dist/src/planner/building/select-aggregates.d.ts.map +1 -1
- package/dist/src/planner/building/select-aggregates.js +122 -79
- package/dist/src/planner/building/select-aggregates.js.map +1 -1
- package/dist/src/planner/building/select-modifiers.d.ts +1 -1
- package/dist/src/planner/building/select.d.ts.map +1 -1
- package/dist/src/planner/building/select.js +73 -55
- package/dist/src/planner/building/select.js.map +1 -1
- package/dist/src/planner/cost/index.d.ts +28 -0
- package/dist/src/planner/cost/index.d.ts.map +1 -1
- package/dist/src/planner/cost/index.js +33 -0
- package/dist/src/planner/cost/index.js.map +1 -1
- package/dist/src/planner/nodes/limit-offset.d.ts.map +1 -1
- package/dist/src/planner/nodes/limit-offset.js +3 -2
- package/dist/src/planner/nodes/limit-offset.js.map +1 -1
- package/dist/src/planner/optimizer-tuning.d.ts +10 -6
- package/dist/src/planner/optimizer-tuning.d.ts.map +1 -1
- package/dist/src/planner/optimizer-tuning.js.map +1 -1
- package/dist/src/planner/optimizer.d.ts.map +1 -1
- package/dist/src/planner/optimizer.js +49 -0
- package/dist/src/planner/optimizer.js.map +1 -1
- package/dist/src/planner/rules/access/rule-monotonic-range-access.d.ts.map +1 -1
- package/dist/src/planner/rules/access/rule-monotonic-range-access.js +8 -4
- package/dist/src/planner/rules/access/rule-monotonic-range-access.js.map +1 -1
- package/dist/src/planner/rules/access/rule-select-access-path.d.ts +14 -0
- package/dist/src/planner/rules/access/rule-select-access-path.d.ts.map +1 -1
- package/dist/src/planner/rules/access/rule-select-access-path.js +5 -1
- package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.d.ts +3 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.d.ts.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js +8 -3
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-minmax-index-boundary.d.ts +47 -0
- package/dist/src/planner/rules/aggregate/rule-minmax-index-boundary.d.ts.map +1 -0
- package/dist/src/planner/rules/aggregate/rule-minmax-index-boundary.js +200 -0
- package/dist/src/planner/rules/aggregate/rule-minmax-index-boundary.js.map +1 -0
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.d.ts +29 -0
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.js +55 -16
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.js.map +1 -1
- package/dist/src/planner/rules/join/index-nested-loop.d.ts +96 -22
- package/dist/src/planner/rules/join/index-nested-loop.d.ts.map +1 -1
- package/dist/src/planner/rules/join/index-nested-loop.js +401 -82
- package/dist/src/planner/rules/join/index-nested-loop.js.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts +20 -0
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js +23 -4
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js.map +1 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js +7 -0
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js.map +1 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.d.ts +19 -2
- package/dist/src/planner/rules/join/rule-join-physical-selection.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.js +262 -40
- package/dist/src/planner/rules/join/rule-join-physical-selection.js.map +1 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js +24 -4
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js.map +1 -1
- package/dist/src/planner/rules/join/rule-semi-join-pushdown.d.ts +107 -0
- package/dist/src/planner/rules/join/rule-semi-join-pushdown.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-semi-join-pushdown.js +214 -0
- package/dist/src/planner/rules/join/rule-semi-join-pushdown.js.map +1 -0
- package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.js +2 -1
- package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.js.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.d.ts +21 -0
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.d.ts.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js +24 -6
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js.map +1 -1
- package/dist/src/planner/rules/shared/access-leaf.d.ts +12 -12
- package/dist/src/planner/rules/shared/access-leaf.d.ts.map +1 -1
- package/dist/src/planner/rules/shared/access-leaf.js +3 -14
- package/dist/src/planner/rules/shared/access-leaf.js.map +1 -1
- package/dist/src/planner/stats/catalog-stats.d.ts.map +1 -1
- package/dist/src/planner/stats/catalog-stats.js +6 -10
- package/dist/src/planner/stats/catalog-stats.js.map +1 -1
- package/dist/src/planner/type-utils.d.ts.map +1 -1
- package/dist/src/planner/type-utils.js +3 -5
- package/dist/src/planner/type-utils.js.map +1 -1
- package/dist/src/planner/util/fd-utils.d.ts.map +1 -1
- package/dist/src/planner/util/fd-utils.js +4 -7
- package/dist/src/planner/util/fd-utils.js.map +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/alter-table.js +58 -4
- package/dist/src/runtime/emit/alter-table.js.map +1 -1
- package/dist/src/runtime/emit/binary.d.ts.map +1 -1
- package/dist/src/runtime/emit/binary.js +5 -3
- package/dist/src/runtime/emit/binary.js.map +1 -1
- package/dist/src/runtime/emit/fanout-lookup-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/fanout-lookup-join.js +9 -1
- package/dist/src/runtime/emit/fanout-lookup-join.js.map +1 -1
- package/dist/src/runtime/emit/limit-offset.d.ts.map +1 -1
- package/dist/src/runtime/emit/limit-offset.js +50 -24
- package/dist/src/runtime/emit/limit-offset.js.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts +33 -2
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.js +61 -5
- package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
- package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -1
- package/dist/src/runtime/emit/materialized-view.js +6 -1
- package/dist/src/runtime/emit/materialized-view.js.map +1 -1
- package/dist/src/runtime/emit/ordinal-slice.d.ts.map +1 -1
- package/dist/src/runtime/emit/ordinal-slice.js +8 -0
- package/dist/src/runtime/emit/ordinal-slice.js.map +1 -1
- package/dist/src/schema/derivation.d.ts +15 -1
- package/dist/src/schema/derivation.d.ts.map +1 -1
- package/dist/src/schema/derivation.js.map +1 -1
- package/dist/src/schema/function.d.ts +9 -4
- package/dist/src/schema/function.d.ts.map +1 -1
- package/dist/src/schema/function.js +11 -9
- package/dist/src/schema/function.js.map +1 -1
- package/dist/src/schema/schema.d.ts +14 -3
- package/dist/src/schema/schema.d.ts.map +1 -1
- package/dist/src/schema/schema.js +20 -2
- package/dist/src/schema/schema.js.map +1 -1
- package/dist/src/schema/table.d.ts +27 -0
- package/dist/src/schema/table.d.ts.map +1 -1
- package/dist/src/schema/table.js +55 -0
- package/dist/src/schema/table.js.map +1 -1
- package/dist/src/schema/unique-enforcement.d.ts +6 -13
- package/dist/src/schema/unique-enforcement.d.ts.map +1 -1
- package/dist/src/schema/unique-enforcement.js +8 -17
- package/dist/src/schema/unique-enforcement.js.map +1 -1
- package/dist/src/util/comparison.d.ts +19 -0
- package/dist/src/util/comparison.d.ts.map +1 -1
- package/dist/src/util/comparison.js +22 -0
- package/dist/src/util/comparison.js.map +1 -1
- package/dist/src/vtab/memory/module.d.ts.map +1 -1
- package/dist/src/vtab/memory/module.js +10 -0
- package/dist/src/vtab/memory/module.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Index-nested-loop candidate construction for `rule-join-physical-selection`.
|
|
3
3
|
*
|
|
4
|
-
* When a join's inner
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* When a join's inner side bottoms out in a table-access leaf whose module can
|
|
5
|
+
* answer an equality seek on the join key, the leaf is replaced by an
|
|
6
|
+
* `IndexSeekNode` whose seek keys are column references into the OUTER row.
|
|
7
|
+
* The node above stays the logical `JoinNode`, so the existing nested-loop
|
|
8
|
+
* emitter drives it: for each outer row it installs the outer row slot and
|
|
9
|
+
* re-opens the inner pipeline, and the seek-key expressions resolve through
|
|
10
|
+
* the runtime context by attribute id — the same machinery correlated
|
|
11
11
|
* subqueries already exercise.
|
|
12
12
|
*
|
|
13
13
|
* Join(inner, s, SeqScan(big)) Join(inner, s, IndexSeek(big, keys=[s.k]))
|
|
14
14
|
* ON big.id = s.k ──▶ ON big.id = s.k
|
|
15
15
|
* reads every row of big one seek per row of s
|
|
16
16
|
*
|
|
17
|
+
* Two admission arms for the inner leaf. A WALK leaf (plain full scan, or an
|
|
18
|
+
* ordering-only index walk) enforces nothing, so its `FilterInfo` may be
|
|
19
|
+
* replaced wholesale. A SEEK leaf (`IndexSeekNode`) already enforces the
|
|
20
|
+
* predicates the module claimed — `status = 'x'` below is nowhere else in the
|
|
21
|
+
* tree — so it is admitted by re-OFFERING those predicates to the module
|
|
22
|
+
* alongside the join key and re-applying whatever the module declines:
|
|
23
|
+
*
|
|
24
|
+
* Join(inner, s, IndexSeek(big, [status='x']))
|
|
25
|
+
* ON big.id = s.k
|
|
26
|
+
* ──▶ Join(inner, s, Filter[status='x'](IndexSeek(big, keys=[s.k])))
|
|
27
|
+
* or Join(inner, s, IndexSeek(big, keys=[status='x', s.k])) (composite index)
|
|
28
|
+
*
|
|
29
|
+
* The function takes `outer` / `inner` explicitly rather than reading a
|
|
30
|
+
* JoinNode's `left` / `right`: the caller asks for BOTH orientations of an
|
|
31
|
+
* inner join (seek the right driven by the left, and the mirror) and elects
|
|
32
|
+
* the cheaper — see `ruleJoinPhysicalSelection`. Here "outer" always means the
|
|
33
|
+
* side that will drive, and "inner" the side whose leaf becomes the seek,
|
|
34
|
+
* whichever JoinNode slot each came from.
|
|
35
|
+
*
|
|
17
36
|
* This module only CONSTRUCTS the candidate (recognize the shape, probe the
|
|
18
|
-
* module, cost it); the
|
|
19
|
-
*
|
|
37
|
+
* module, cost it); the cost comparison and the join rebuild live in the
|
|
38
|
+
* caller. Every gate below declines by returning null, which leaves the
|
|
20
39
|
* nested-loop / hash / merge competition unchanged.
|
|
21
40
|
*
|
|
22
41
|
* NOTE: one extra pair of `getBestAccessPlan` probes runs per qualifying
|
|
23
|
-
* equi-join, uncached (see `probeModule`)
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
42
|
+
* equi-join on the walk arm, uncached (see `probeModule`); the seek arm runs
|
|
43
|
+
* ONE probe — its baseline is the displaced seek's recorded cost, a field read
|
|
44
|
+
* rather than a second probe. Cheap for both shipped modules; memoize by
|
|
45
|
+
* (table, offered constraints) only if a third-party module with an expensive
|
|
46
|
+
* planner shows up in optimization profiles — mirrors the rule-key-set-seek
|
|
47
|
+
* tripwire.
|
|
27
48
|
*/
|
|
28
49
|
import { createLogger } from '../../../common/logger.js';
|
|
29
50
|
import { IndexScanNode, IndexSeekNode } from '../../nodes/table-access-nodes.js';
|
|
@@ -33,28 +54,42 @@ import { BinaryOpNode } from '../../nodes/scalar.js';
|
|
|
33
54
|
import { PlanNodeCharacteristics } from '../../framework/characteristics.js';
|
|
34
55
|
import { hasSemanticOrdering } from '../../../util/comparison.js';
|
|
35
56
|
import { sharesSeekKeySpace } from '../../../types/builtin-types.js';
|
|
57
|
+
import { hasRelationalDescendant } from '../../analysis/scalar-subqueries.js';
|
|
36
58
|
import { extractConstraints, createTableInfoFromNode } from '../../analysis/constraint-extractor.js';
|
|
37
|
-
import { selectPhysicalNode } from '../access/rule-select-access-path.js';
|
|
38
|
-
import {
|
|
59
|
+
import { selectPhysicalNode, combineResidualExpressions, effectivePredicateCollation, } from '../access/rule-select-access-path.js';
|
|
60
|
+
import { peelToSeekableAccessLeaf, rebuildChain, buildProbeRequest, } from '../shared/access-leaf.js';
|
|
39
61
|
import { combineResidual } from './equi-pair-extractor.js';
|
|
40
62
|
import { indexNestedLoopJoinCost } from '../../cost/index.js';
|
|
41
63
|
import { validateAccessPlan } from '../../../vtab/best-access-plan.js';
|
|
64
|
+
import { uniqueEnforcementCollations } from '../../../schema/unique-enforcement.js';
|
|
65
|
+
import { collationRefines } from '../../../util/comparison.js';
|
|
42
66
|
const log = createLogger('optimizer:rule:index-nested-loop');
|
|
43
67
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* limit / offset is likewise a directive the seek would not honor.
|
|
68
|
+
* Join keys FIRST. Both the module (`claimFirstPerRole` in the store,
|
|
69
|
+
* `findEqualityMatches` in memory) and `selectPhysicalNode` pick the FIRST
|
|
70
|
+
* role-filling constraint per column, so when a pushed predicate and a join key
|
|
71
|
+
* land on the same column (`where b.id = 5` joined `on b.id = s.k`) this order
|
|
72
|
+
* is what makes the correlated seek the one that wins the column — the whole
|
|
73
|
+
* point of the candidate. Either leftover is then re-applied as a Filter.
|
|
51
74
|
*/
|
|
52
|
-
function
|
|
53
|
-
|
|
75
|
+
function offerConstraints(joinKeys, pushed) {
|
|
76
|
+
return { combined: [...joinKeys, ...pushed], joinKeyCount: joinKeys.length };
|
|
77
|
+
}
|
|
78
|
+
/** Dispatch on the peeled leaf's kind; each arm owns its own gates. */
|
|
79
|
+
function admitLeaf(inner) {
|
|
80
|
+
const leaf = peelToSeekableAccessLeaf(inner);
|
|
54
81
|
if (!leaf) {
|
|
55
|
-
log('decline:
|
|
82
|
+
log('decline: inner does not peel to an access leaf through Alias/Project/Filter');
|
|
56
83
|
return null;
|
|
57
84
|
}
|
|
85
|
+
return leaf instanceof IndexSeekNode ? admitSeekLeaf(leaf) : admitWalkLeaf(leaf);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* WALK arm: an unconstrained every-row walk whose `FilterInfo` may be replaced
|
|
89
|
+
* wholesale — a plain full scan, or an ordering-only index walk (plan='scan').
|
|
90
|
+
* A pushed limit / offset is a directive the seek would not honor.
|
|
91
|
+
*/
|
|
92
|
+
function admitWalkLeaf(leaf) {
|
|
58
93
|
const fi = leaf.filterInfo;
|
|
59
94
|
const isEveryRowWalk = fi.accessPath?.kind === 'fullScan'
|
|
60
95
|
|| (fi.accessPath?.kind === 'index' && fi.accessPath.plan === 'scan');
|
|
@@ -69,7 +104,63 @@ function admitLeaf(right) {
|
|
|
69
104
|
log('decline: leaf emission order is load-bearing (absorbed a Sort)');
|
|
70
105
|
return null;
|
|
71
106
|
}
|
|
72
|
-
return leaf;
|
|
107
|
+
return { leaf, pushed: [] };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* SEEK arm: a leaf whose `FilterInfo` already enforces the predicates recorded
|
|
111
|
+
* in `pushedConstraints` (their residual Filter was dropped on the module's
|
|
112
|
+
* promise). Admitted when that record is complete and re-offerable; the caller
|
|
113
|
+
* re-offers it with the join key and re-applies whatever the module declines,
|
|
114
|
+
* so no recorded predicate can be lost. Same five gates as
|
|
115
|
+
* `rule-key-set-seek`'s `admitSeekLeaf`, for the same reasons.
|
|
116
|
+
*/
|
|
117
|
+
function admitSeekLeaf(leaf) {
|
|
118
|
+
const fi = leaf.filterInfo;
|
|
119
|
+
// Gate 1: a pushed limit / offset is a directive the re-planned seek would not
|
|
120
|
+
// honour, and unlike a predicate it cannot be re-applied by a Filter without
|
|
121
|
+
// changing which rows are dropped.
|
|
122
|
+
if (fi.limit !== undefined || fi.offset !== undefined) {
|
|
123
|
+
log('decline: seek leaf carries a pushed limit/offset');
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
// Gate 2: a seek whose enforced predicate we cannot describe is a seek we
|
|
127
|
+
// cannot safely re-plan.
|
|
128
|
+
if (!leaf.pushedConstraints || leaf.pushedConstraints.length === 0) {
|
|
129
|
+
log('decline: seek leaf records no pushed constraints');
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
// Gate 3: the seek's emission order absorbed a Sort; a re-planned seek emits
|
|
133
|
+
// in its own key order instead.
|
|
134
|
+
if (leaf.orderingLoadBearing) {
|
|
135
|
+
log('decline: seek leaf emission order is load-bearing (absorbed a Sort)');
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
for (const c of leaf.pushedConstraints) {
|
|
139
|
+
// Gate 4: a correlated pushed constraint means this leaf is already somebody
|
|
140
|
+
// else's per-outer-row seek (a lateral seek keyed on an enclosing scope);
|
|
141
|
+
// re-planning it would re-plan their correlation. (The caller's
|
|
142
|
+
// sibling-reference guard already blocks this rule's OWN output.)
|
|
143
|
+
//
|
|
144
|
+
// NOTE: this reads the RECORDED constraint's flag, where rule-key-set-seek's
|
|
145
|
+
// equivalent gate asks whether the leaf SUBTREE is correlated. The two agree
|
|
146
|
+
// only because `stampSeekProvenance` is the sole producer of
|
|
147
|
+
// `pushedConstraints` and records every constraint the seek consumed; a
|
|
148
|
+
// future rule that mints a correlated seek without recording the correlated
|
|
149
|
+
// constraint would slip past this gate. Switch to the subtree test if one
|
|
150
|
+
// ever does.
|
|
151
|
+
if (c.correlated === true) {
|
|
152
|
+
log('decline: seek leaf carries a correlated pushed constraint');
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
// Gate 5: this rule runs PostOptimization, so an expression re-applied here
|
|
156
|
+
// gets no further pass — a relational subquery inside it would reach emit
|
|
157
|
+
// unphysicalized, and would re-execute per outer row besides.
|
|
158
|
+
if (hasRelationalDescendant(c.sourceExpression)) {
|
|
159
|
+
log('decline: seek leaf pushed predicate contains a relational node');
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { leaf, pushed: leaf.pushedConstraints };
|
|
73
164
|
}
|
|
74
165
|
/**
|
|
75
166
|
* Resolve each equi pair to (leaf column index, outer attribute position) and
|
|
@@ -141,17 +232,45 @@ function buildSeekPredicate(leaf, outer, resolved) {
|
|
|
141
232
|
return combineResidual(undefined, conjuncts);
|
|
142
233
|
}
|
|
143
234
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
235
|
+
* What the inner leaf costs TODAY, in the module's own currency. A seek leaf
|
|
236
|
+
* already records the module's answer for its own seek — `filterInfo.
|
|
237
|
+
* indexInfoOutput.estimatedCost` / `estimatedRows` are `accessPlan.cost` /
|
|
238
|
+
* `accessPlan.rows` verbatim (`makeIndexFilterInfo` spreads a base seeded with
|
|
239
|
+
* them and never overrides either) — while an unconstrained walk has to be
|
|
240
|
+
* asked. Comparing the combined seek against a bare scan instead would let a
|
|
241
|
+
* plan WORSE than the seek already in place win. Same baseline rule as
|
|
242
|
+
* rule-key-set-seek's `probeModuleCosts`.
|
|
243
|
+
*
|
|
244
|
+
* NOTE: `estimatedRows` is `makeFullScanFilterInfo`'s `accessPlan.rows || 1000`,
|
|
245
|
+
* so a module that answered its own seek with `rows: 0` (and did not claim every
|
|
246
|
+
* filter, which would have folded the access to an EmptyResult instead) or with
|
|
247
|
+
* no estimate at all reads back here as a 1000-row baseline the module never
|
|
248
|
+
* stated — the seek arm would then admit a candidate the cost gate should have
|
|
249
|
+
* declined. Both shipped modules always return a positive estimate for a seek,
|
|
250
|
+
* so this is unreachable today; if a third-party module reports 0 or omits
|
|
251
|
+
* `rows`, carry the module's own answer on the leaf instead of re-deriving it
|
|
252
|
+
* from the FilterInfo.
|
|
253
|
+
*/
|
|
254
|
+
function displacedPlan(leaf, ask) {
|
|
255
|
+
if (leaf instanceof IndexSeekNode) {
|
|
256
|
+
const out = leaf.filterInfo.indexInfoOutput;
|
|
257
|
+
return { cost: out.estimatedCost, rows: Number(out.estimatedRows) };
|
|
258
|
+
}
|
|
259
|
+
const scan = ask([]);
|
|
260
|
+
return { cost: scan.cost, rows: scan.rows };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Probe the module with the offered constraints and admit the seek only when
|
|
264
|
+
* the module's OWN answers say it beats the plan being displaced (`cost` no
|
|
265
|
+
* higher, `rows` strictly smaller). Comparing module currency to module
|
|
266
|
+
* currency keeps the engine's cost units out of the module's — the same
|
|
267
|
+
* discipline as rule-key-set-seek's break-even.
|
|
149
268
|
*
|
|
150
269
|
* These requests are the ENGINE's, not the user's: a module that answers one
|
|
151
270
|
* with a plan `validateAccessPlan` rejects gets logged and declined, never
|
|
152
271
|
* thrown — the user's own query ran fine before this rule probed.
|
|
153
272
|
*/
|
|
154
|
-
function probeModule(context, leaf,
|
|
273
|
+
function probeModule(context, leaf, offered) {
|
|
155
274
|
const tableSchema = leaf.tableSchema;
|
|
156
275
|
const vtabModule = leaf.source.vtabModule;
|
|
157
276
|
const getBestAccessPlan = vtabModule.getBestAccessPlan;
|
|
@@ -166,11 +285,12 @@ function probeModule(context, leaf, constraints) {
|
|
|
166
285
|
validateAccessPlan(request, plan);
|
|
167
286
|
return plan;
|
|
168
287
|
};
|
|
288
|
+
const { combined, joinKeyCount } = offered;
|
|
169
289
|
let seekPlan;
|
|
170
|
-
let
|
|
290
|
+
let baseline;
|
|
171
291
|
try {
|
|
172
|
-
seekPlan = ask(
|
|
173
|
-
|
|
292
|
+
seekPlan = ask(combined);
|
|
293
|
+
baseline = displacedPlan(leaf, ask);
|
|
174
294
|
}
|
|
175
295
|
catch (e) {
|
|
176
296
|
log('decline: module %s answered a synthesized probe on %s with an invalid plan: %s', tableSchema.vtabModuleName, tableSchema.name, e instanceof Error ? e.message : String(e));
|
|
@@ -180,18 +300,32 @@ function probeModule(context, leaf, constraints) {
|
|
|
180
300
|
log('decline: module did not claim an index seek');
|
|
181
301
|
return null;
|
|
182
302
|
}
|
|
183
|
-
// Every seek column must be one of
|
|
184
|
-
// constraint the seek consumes must be claimed handled — otherwise the
|
|
303
|
+
// Every seek column must be one of the offered constraints — otherwise the
|
|
185
304
|
// module answered a different question than the one we will emit.
|
|
186
|
-
const
|
|
187
|
-
if (!seekPlan.seekColumnIndexes.every(col =>
|
|
188
|
-
log('decline: seek columns extend beyond the
|
|
305
|
+
const offeredCols = new Set(combined.map(c => c.columnIndex));
|
|
306
|
+
if (!seekPlan.seekColumnIndexes.every(col => offeredCols.has(col))) {
|
|
307
|
+
log('decline: seek columns extend beyond the offered constraints');
|
|
189
308
|
return null;
|
|
190
309
|
}
|
|
310
|
+
// At least one seek column must come from a JOIN KEY. Without this the
|
|
311
|
+
// module can answer a seek leaf with the seek it already had: nothing is
|
|
312
|
+
// correlated, nothing is gained, and the rule would rebuild an identical
|
|
313
|
+
// leaf on every visit. (The rebuilt leaf's provenance is re-checked by
|
|
314
|
+
// identity in `tryIndexNestedLoop` — this is the cheap early exit.)
|
|
315
|
+
const joinKeyCols = new Set(combined.slice(0, joinKeyCount).map(c => c.columnIndex));
|
|
316
|
+
if (!seekPlan.seekColumnIndexes.some(col => joinKeyCols.has(col))) {
|
|
317
|
+
log('decline: the seek does not use the join key');
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
// Every JOIN-KEY constraint on a seek column must be claimed handled — the
|
|
321
|
+
// module must be promising to seek on the key we will emit. Pushed
|
|
322
|
+
// constraints need no such claim: a handled one is re-promised by the new
|
|
323
|
+
// seek or reattached by selectPhysicalNode, an unhandled one is re-applied
|
|
324
|
+
// by `reapplyDeclinedPushed` — either answer is honoured.
|
|
191
325
|
const seekCols = new Set(seekPlan.seekColumnIndexes);
|
|
192
|
-
for (let i = 0; i <
|
|
193
|
-
if (seekCols.has(
|
|
194
|
-
log('decline: module claimed a seek column without handling its filter');
|
|
326
|
+
for (let i = 0; i < joinKeyCount; i++) {
|
|
327
|
+
if (seekCols.has(combined[i].columnIndex) && seekPlan.handledFilters[i] !== true) {
|
|
328
|
+
log('decline: module claimed a join-key seek column without handling its filter');
|
|
195
329
|
return null;
|
|
196
330
|
}
|
|
197
331
|
}
|
|
@@ -200,67 +334,249 @@ function probeModule(context, leaf, constraints) {
|
|
|
200
334
|
log('decline: module attached a residualFilter');
|
|
201
335
|
return null;
|
|
202
336
|
}
|
|
203
|
-
// The module's own statement that
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
337
|
+
// The module's own statement that the combined seek beats what it displaces:
|
|
338
|
+
// no dearer, and strictly fewer rows. Rows is the discriminator on purpose —
|
|
339
|
+
// the memory module prices every single-key equality seek identically (cost
|
|
340
|
+
// keyed to the seek-KEY count, not the rows matched), so a pushed
|
|
341
|
+
// `status = 'x'` seek and the join-key seek that would replace it tie on
|
|
342
|
+
// cost and differ only in rows; a strict cost test would leave the seek arm
|
|
343
|
+
// dead on that module for its headline shape.
|
|
344
|
+
if (seekPlan.rows === undefined || baseline.rows === undefined
|
|
345
|
+
|| !(seekPlan.cost <= baseline.cost) || !(seekPlan.rows < baseline.rows)) {
|
|
346
|
+
log('decline: module costs do not favor the seek (seek %s/%s vs displaced %s/%s)', seekPlan.cost, seekPlan.rows, baseline.cost, baseline.rows);
|
|
207
347
|
return null;
|
|
208
348
|
}
|
|
209
349
|
return seekPlan;
|
|
210
350
|
}
|
|
211
351
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
352
|
+
* Re-apply the pushed constraints the module DECLINED (`handledFilters[i]
|
|
353
|
+
* !== true`) as a Filter directly above the rebuilt leaf. `selectPhysicalNode`
|
|
354
|
+
* re-promises the handled ones it consumed (recording them on the new seek's
|
|
355
|
+
* `pushedConstraints`) and reattaches the handled-but-unconsumed ones itself,
|
|
356
|
+
* but — exactly as for a user predicate, where `rule-grow-retrieve` owns the
|
|
357
|
+
* residual — it never touches a constraint the module did not claim. This is
|
|
358
|
+
* the third place, and with it every offered pushed constraint lands in
|
|
359
|
+
* exactly one of: re-promised, reattached, or re-applied here. Join-key
|
|
360
|
+
* leftovers need nothing: the ON condition retained above the join covers them.
|
|
361
|
+
*
|
|
362
|
+
* Folded into every Filter `selectPhysicalNode` may already have wrapped the
|
|
363
|
+
* seek in — it can stack two (a `COARSER_SAFE` collation residual, then a
|
|
364
|
+
* reattach above it) — so the seek carries one residual Filter rather than a
|
|
365
|
+
* stack. `combineResidualExpressions` de-duplicates by identity, so a declined
|
|
366
|
+
* BETWEEN comes back as its single `BetweenNode`.
|
|
367
|
+
*/
|
|
368
|
+
function reapplyDeclinedPushed(rebuilt, seekPlan, offered) {
|
|
369
|
+
const { combined, joinKeyCount } = offered;
|
|
370
|
+
const declined = combined.filter((_c, i) => i >= joinKeyCount && seekPlan.handledFilters[i] !== true);
|
|
371
|
+
if (declined.length === 0)
|
|
372
|
+
return rebuilt;
|
|
373
|
+
// Peel the whole Filter stack, outermost predicate first, so the result is one
|
|
374
|
+
// Filter — the same peel `rebuiltSeek` uses to find the seek underneath.
|
|
375
|
+
let below = rebuilt;
|
|
376
|
+
const existing = [];
|
|
377
|
+
while (below instanceof FilterNode) {
|
|
378
|
+
existing.push(below.predicate);
|
|
379
|
+
below = below.source;
|
|
380
|
+
}
|
|
381
|
+
// Non-empty input ⇒ defined result.
|
|
382
|
+
const predicate = combineResidualExpressions([...existing, ...declined.map(c => c.sourceExpression)]);
|
|
383
|
+
log('re-applying %d module-declined pushed constraint(s) above the seek', declined.length);
|
|
384
|
+
return new FilterNode(below.scope, below, predicate);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Every key the table enforces uniqueness on, each with the collation its
|
|
388
|
+
* columns are enforced under: the primary key (a member's own collation, else
|
|
389
|
+
* its column's — `ddl-generator` reads it the same way), every non-partial
|
|
390
|
+
* UNIQUE constraint (`uniqueEnforcementCollations`: the backing index's
|
|
391
|
+
* per-column COLLATE for an index-derived constraint, else the column's), and
|
|
392
|
+
* every non-partial UNIQUE index (its own per-column collation, else the
|
|
393
|
+
* column's — `appendIndexToTableSchema` normally mirrors these as derived
|
|
394
|
+
* constraints already; listed so a host-authored schema carrying the index
|
|
395
|
+
* alone is not missed). A partial key (`predicate` set) only constrains the
|
|
396
|
+
* rows its WHERE admits and proves nothing about the rest.
|
|
397
|
+
*/
|
|
398
|
+
function enforcedKeys(tableSchema) {
|
|
399
|
+
const columnCollation = (idx) => tableSchema.columns[idx]?.collation;
|
|
400
|
+
const keys = [];
|
|
401
|
+
const pk = tableSchema.primaryKeyDefinition;
|
|
402
|
+
if (pk.length > 0) {
|
|
403
|
+
keys.push({
|
|
404
|
+
columns: pk.map(k => k.index),
|
|
405
|
+
collations: pk.map(k => k.collation ?? columnCollation(k.index)),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
for (const uc of tableSchema.uniqueConstraints ?? []) {
|
|
409
|
+
if (uc.predicate !== undefined || uc.columns.length === 0)
|
|
410
|
+
continue;
|
|
411
|
+
keys.push({ columns: uc.columns, collations: uniqueEnforcementCollations(tableSchema, uc) });
|
|
412
|
+
}
|
|
413
|
+
for (const ix of tableSchema.indexes ?? []) {
|
|
414
|
+
if (!ix.unique || ix.predicate !== undefined || ix.columns.length === 0)
|
|
415
|
+
continue;
|
|
416
|
+
keys.push({
|
|
417
|
+
columns: ix.columns.map(c => c.index),
|
|
418
|
+
collations: ix.columns.map(c => c.collation ?? columnCollation(c.index)),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return keys;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* True when an equality's comparison collation is at least as fine as the
|
|
425
|
+
* key's enforcement collation — every pair of rows the equality treats as
|
|
426
|
+
* equal, the key treats as equal too, so uniqueness forbids a second match.
|
|
427
|
+
* {@link collationRefines} is the shared two-name test; a COARSER predicate
|
|
428
|
+
* fails it, so a `NOCASE` join key over a `BINARY` unique column (which admits
|
|
429
|
+
* `'a'` and `'A'`, two distinct stored rows that both match) declines.
|
|
430
|
+
*
|
|
431
|
+
* The source-shape gate is load-bearing, not defensive. `effectivePredicateCollation`
|
|
432
|
+
* answers BINARY for a shape it does not recognize — a safe DECLINE for its own
|
|
433
|
+
* caller (a BINARY-read predicate over a coarser index over-fetches and keeps its
|
|
434
|
+
* residual Filter) but the STRONGEST possible claim here, where BINARY refines
|
|
435
|
+
* everything. Every `op: '='` constraint today is minted by `extractBinaryConstraint`
|
|
436
|
+
* carrying its own `BinaryOpNode` (OR collapse emits only 'IN' / 'OR_RANGE'), so
|
|
437
|
+
* requiring that shape costs nothing now and makes a future `=` producer decline
|
|
438
|
+
* rather than be proved unique on a default.
|
|
439
|
+
*/
|
|
440
|
+
function equalityRefinesKey(constraint, keyCollation) {
|
|
441
|
+
if (!(constraint.sourceExpression instanceof BinaryOpNode))
|
|
442
|
+
return false;
|
|
443
|
+
return collationRefines(effectivePredicateCollation(constraint), keyCollation);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* An equality pins ONE value per outer row only when its value side is
|
|
447
|
+
* deterministic. A constraint re-applied as a Filter is re-evaluated per inner
|
|
448
|
+
* row, so `b.id = random()` admits a different id on every row and several can
|
|
449
|
+
* pass — nothing upstream gates non-deterministic predicates out of pushdown
|
|
450
|
+
* (`rule-select-access-path` has no such test), so the proof must. A literal
|
|
451
|
+
* has no value expression and trivially qualifies; a correlated column
|
|
452
|
+
* reference is deterministic for the duration of one outer row.
|
|
453
|
+
*/
|
|
454
|
+
function pinsOneValue(constraint) {
|
|
455
|
+
const value = constraint.valueExpr;
|
|
456
|
+
if (value === undefined)
|
|
457
|
+
return true;
|
|
458
|
+
if (Array.isArray(value))
|
|
459
|
+
return false; // an IN list never reaches here (op !== '='); be explicit
|
|
460
|
+
return PlanNodeCharacteristics.isDeterministic(value);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* PROOF — not an estimate — that the inner rebuilt from `combined` yields at
|
|
464
|
+
* most one row per outer row. `FanOutLookupJoinNode`'s `atMostOne-*` branch
|
|
465
|
+
* modes throw `QuereusError(CONSTRAINT)` at runtime on a second row, so the
|
|
466
|
+
* module's `rows: 1` estimate must not stand in: a module that estimates 1 and
|
|
467
|
+
* returns 2 would turn a working query into an error.
|
|
468
|
+
*
|
|
469
|
+
* Premise: every constraint in `combined` is enforced somewhere inside the
|
|
470
|
+
* rebuilt inner. That is total for the `=` constraints this proof reads, by
|
|
471
|
+
* construction rather than by argument — each one is either module-DECLINED, and
|
|
472
|
+
* `reapplyDeclinedPushed` re-applies every declined pushed constraint as a Filter
|
|
473
|
+
* (join-key leftovers need none: the ON condition the caller keeps above the seek
|
|
474
|
+
* covers them), or module-HANDLED, and a handled constraint is either consumed
|
|
475
|
+
* into the seek's bounds or recovered by `selectPhysicalNode`'s
|
|
476
|
+
* `reattachUnconsumedConstraints` — whose `RECLAIMABLE_OPS` set contains `'='`,
|
|
477
|
+
* so no equality can fall through it. A Filter only removes rows, so a constraint
|
|
478
|
+
* enforced by a filter rather than by the index counts just the same. Then: if
|
|
479
|
+
* some enforced key has EVERY column pinned by an equality whose collation
|
|
480
|
+
* refines the key's ({@link equalityRefinesKey}), any two surviving rows agree
|
|
481
|
+
* on the whole key and uniqueness forbids the second. Ranges, IN and OR_RANGE
|
|
482
|
+
* admit many values and never contribute. Over-fetch is harmless — a
|
|
483
|
+
* `COARSER_SAFE` seek's residual Filter re-applies the exact comparison above
|
|
484
|
+
* it — and the finer-index case (`MISMATCH_UNSAFE`) never reaches here: it
|
|
485
|
+
* degrades to a scan in `selectPhysicalNode` and `rebuiltSeek` declines the
|
|
486
|
+
* candidate first. NULL keys need no case: `col = NULL` matches nothing.
|
|
487
|
+
*
|
|
488
|
+
* Exported for direct unit coverage; it is the correctness core of the batched
|
|
489
|
+
* candidate.
|
|
490
|
+
*/
|
|
491
|
+
export function provesAtMostOne(tableSchema, combined) {
|
|
492
|
+
const equalitiesByColumn = new Map();
|
|
493
|
+
for (const c of combined) {
|
|
494
|
+
if (c.op !== '=' || !pinsOneValue(c))
|
|
495
|
+
continue;
|
|
496
|
+
const list = equalitiesByColumn.get(c.columnIndex);
|
|
497
|
+
if (list)
|
|
498
|
+
list.push(c);
|
|
499
|
+
else
|
|
500
|
+
equalitiesByColumn.set(c.columnIndex, [c]);
|
|
501
|
+
}
|
|
502
|
+
if (equalitiesByColumn.size === 0)
|
|
503
|
+
return false;
|
|
504
|
+
// A column pinned by several equalities (a pushed `b.id = 5` beside the join
|
|
505
|
+
// key) is proved by ANY one of them: the surviving rows satisfy all of them,
|
|
506
|
+
// so they are a subset of the rows the refining one admits.
|
|
507
|
+
return enforcedKeys(tableSchema).some(key => key.columns.every((col, i) => (equalitiesByColumn.get(col) ?? []).some(eq => equalityRefinesKey(eq, key.collations[i]))));
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The `IndexSeekNode` under `rebuilt`'s collation-residual Filter, or null when
|
|
511
|
+
* `selectPhysicalNode` produced something else. It degrades to a SeqScan +
|
|
512
|
+
* residual on a collation decline (MISMATCH_UNSAFE — the seek would
|
|
513
|
+
* under-fetch) and to an EmptyResultNode on an "impossible" predicate; both
|
|
514
|
+
* mean no index-nested-loop here. An EmptyResultNode in particular must NOT be
|
|
515
|
+
* adopted — it would be sound only if the join key were provably literal
|
|
516
|
+
* NULL, which a dynamic per-outer-row binding never is.
|
|
517
|
+
*/
|
|
518
|
+
function rebuiltSeek(rebuilt) {
|
|
519
|
+
let probe = rebuilt;
|
|
520
|
+
while (probe instanceof FilterNode)
|
|
521
|
+
probe = probe.source;
|
|
522
|
+
return probe instanceof IndexSeekNode ? probe : null;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Try to build an index-nested-loop candidate: the `inner` subtree rebuilt with
|
|
526
|
+
* its access leaf replaced by an `IndexSeekNode` correlated to `outer`, plus
|
|
214
527
|
* its engine-currency cost. Returns null when any gate declines; the caller's
|
|
215
528
|
* nested-loop / hash / merge competition is then unchanged.
|
|
216
529
|
*
|
|
217
|
-
* `
|
|
218
|
-
*
|
|
219
|
-
* left
|
|
530
|
+
* `joinType` is the type the REBUILT join will have with `outer` on the left
|
|
531
|
+
* (so a mirrored inner join passes 'inner'). `equiPairs` must be non-empty and
|
|
532
|
+
* oriented left=outer / right=inner — the caller flips them for the mirror.
|
|
533
|
+
* `outerRows` is the outer side's row estimate, the same input hash and merge
|
|
534
|
+
* selection use for that side.
|
|
220
535
|
*/
|
|
221
|
-
export function tryIndexNestedLoop(
|
|
536
|
+
export function tryIndexNestedLoop(joinType, outer, inner, equiPairs, outerRows, context) {
|
|
222
537
|
// Only the left-driven join types: the nested-loop emitter installs the left
|
|
223
538
|
// row slot before re-opening the right pipeline (driveFromLeft), which is
|
|
224
539
|
// what makes the correlated seek keys resolve. `right` / `full` drive from
|
|
225
540
|
// the right with no left slot installed. `cross` never reaches here (no
|
|
226
541
|
// condition ⇒ no equi pairs).
|
|
227
|
-
|
|
228
|
-
if (jt !== 'inner' && jt !== 'left' && jt !== 'semi' && jt !== 'anti')
|
|
542
|
+
if (joinType !== 'inner' && joinType !== 'left' && joinType !== 'semi' && joinType !== 'anti')
|
|
229
543
|
return null;
|
|
230
544
|
// Purity gate. Determinism is deliberately NOT gated: the nested loop
|
|
231
545
|
// already re-executes the inner side once per outer row, so replacing a scan
|
|
232
546
|
// with a seek does not change how often a non-deterministic inner runs.
|
|
233
547
|
// (rule-key-set-seek gates determinism because it drains its key source
|
|
234
548
|
// exactly once — a different execution-count contract.)
|
|
235
|
-
if (PlanNodeCharacteristics.subtreeHasSideEffects(
|
|
236
|
-
log('decline:
|
|
549
|
+
if (PlanNodeCharacteristics.subtreeHasSideEffects(inner)) {
|
|
550
|
+
log('decline: inner side has side effects');
|
|
237
551
|
return null;
|
|
238
552
|
}
|
|
239
|
-
const
|
|
240
|
-
if (!
|
|
553
|
+
const admitted = admitLeaf(inner);
|
|
554
|
+
if (!admitted)
|
|
241
555
|
return null;
|
|
242
|
-
const
|
|
556
|
+
const { leaf, pushed } = admitted;
|
|
557
|
+
const resolved = resolvePairs(leaf, outer, equiPairs);
|
|
243
558
|
if (!resolved || resolved.length === 0)
|
|
244
559
|
return null;
|
|
245
560
|
// Synthesize `leaf.col = outer.col` and run it through the same constraint
|
|
246
561
|
// extraction a pushed user predicate gets.
|
|
247
|
-
const predicate = buildSeekPredicate(leaf,
|
|
562
|
+
const predicate = buildSeekPredicate(leaf, outer, resolved);
|
|
248
563
|
const tableSchema = leaf.tableSchema;
|
|
249
564
|
const tInfo = createTableInfoFromNode(leaf.source, `${tableSchema.schemaName}.${tableSchema.name}`);
|
|
250
565
|
const extraction = extractConstraints(predicate, [tInfo]);
|
|
251
|
-
const
|
|
566
|
+
const joinKeys = extraction.constraintsByTable.get(tInfo.relationKey) ?? [];
|
|
252
567
|
// Orientation assertion: every conjunct must have extracted as an equality
|
|
253
568
|
// on an INNER column with a correlated (per-outer-row) binding, with nothing
|
|
254
569
|
// left over. Inner-side-first construction makes this hold today; the check
|
|
255
570
|
// is here so a future extractor change declines loudly instead of seeking on
|
|
256
571
|
// the wrong column.
|
|
257
|
-
if (
|
|
258
|
-
|| !
|
|
572
|
+
if (joinKeys.length !== resolved.length || extraction.residualPredicate !== undefined
|
|
573
|
+
|| !joinKeys.every(c => c.op === '=' && c.correlated === true
|
|
259
574
|
&& tInfo.columnIndexMap.has(c.attributeId))) {
|
|
260
575
|
log('decline: constraint extraction did not return %d oriented correlated equalities', resolved.length);
|
|
261
576
|
return null;
|
|
262
577
|
}
|
|
263
|
-
const
|
|
578
|
+
const offered = offerConstraints(joinKeys, pushed);
|
|
579
|
+
const seekPlan = probeModule(context, leaf, offered);
|
|
264
580
|
if (!seekPlan)
|
|
265
581
|
return null;
|
|
266
582
|
// Reuse the full access-path machinery: collation cover (over the real
|
|
@@ -268,24 +584,27 @@ export function tryIndexNestedLoop(node, equiPairs, outerRows, context) {
|
|
|
268
584
|
// comparison collation is what gets checked), composite seeks, NULL
|
|
269
585
|
// handling, and reattachUnconsumedConstraints all come from
|
|
270
586
|
// selectPhysicalNode.
|
|
271
|
-
const rebuiltLeaf = selectPhysicalNode(leaf.source, seekPlan, [...
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (!(
|
|
282
|
-
log('decline:
|
|
587
|
+
const rebuiltLeaf = selectPhysicalNode(leaf.source, seekPlan, [...offered.combined]);
|
|
588
|
+
const seek = rebuiltSeek(rebuiltLeaf);
|
|
589
|
+
if (!seek) {
|
|
590
|
+
log('decline: selectPhysicalNode did not produce an IndexSeek');
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
// The new seek must really be keyed on the join — its recorded provenance
|
|
594
|
+
// must include a join-key constraint by identity. Otherwise the module
|
|
595
|
+
// re-minted the seek it already had (a pushed equality on the seek column
|
|
596
|
+
// won the per-column pick) and nothing is correlated.
|
|
597
|
+
if (!seek.pushedConstraints?.some(c => joinKeys.includes(c))) {
|
|
598
|
+
log('decline: the rebuilt seek is not keyed on the join');
|
|
283
599
|
return null;
|
|
284
600
|
}
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
601
|
+
const newLeaf = reapplyDeclinedPushed(rebuiltLeaf, seekPlan, offered);
|
|
602
|
+
const newInner = rebuildChain(inner, leaf, newLeaf);
|
|
603
|
+
const perSeekLatencyMs = inner.physical.expectedLatencyMs ?? 0;
|
|
604
|
+
const rowsPerSeek = seekPlan.rows;
|
|
605
|
+
const cost = indexNestedLoopJoinCost(outerRows, rowsPerSeek, perSeekLatencyMs);
|
|
606
|
+
const atMostOne = provesAtMostOne(tableSchema, offered.combined);
|
|
607
|
+
log('candidate: seek %s.%s via %s (%s rows/seek, cost %s for %s outer rows; %d pushed constraint(s) re-offered; at-most-one %s)', tableSchema.name, seekPlan.seekColumnIndexes.map(c => tableSchema.columns[c]?.name).join(','), seekPlan.indexName, seekPlan.rows, cost, outerRows, pushed.length, atMostOne ? 'proved' : 'unproved');
|
|
608
|
+
return { newInner, cost, rowsPerSeek, perSeekLatencyMs, atMostOne };
|
|
290
609
|
}
|
|
291
610
|
//# sourceMappingURL=index-nested-loop.js.map
|