@omgbase/oqx 0.14.0 → 0.14.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/CHANGELOG.md +64 -0
- package/README.md +139 -6
- package/dist/adapters/indexed.d.ts +13 -1
- package/dist/adapters/indexed.d.ts.map +1 -1
- package/dist/adapters/indexed.js +55 -24
- package/dist/adapters/indexed.js.map +1 -1
- package/dist/context.d.ts +24 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +55 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +268 -35
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/optimize/analysis.d.ts +45 -0
- package/dist/optimize/analysis.d.ts.map +1 -0
- package/dist/optimize/analysis.js +194 -0
- package/dist/optimize/analysis.js.map +1 -0
- package/dist/optimize/driver.d.ts +8 -0
- package/dist/optimize/driver.d.ts.map +1 -0
- package/dist/optimize/driver.js +35 -0
- package/dist/optimize/driver.js.map +1 -0
- package/dist/optimize/hash-index.d.ts +34 -0
- package/dist/optimize/hash-index.d.ts.map +1 -0
- package/dist/optimize/hash-index.js +79 -0
- package/dist/optimize/hash-index.js.map +1 -0
- package/dist/optimize/index.d.ts +8 -0
- package/dist/optimize/index.d.ts.map +1 -0
- package/dist/optimize/index.js +11 -0
- package/dist/optimize/index.js.map +1 -0
- package/dist/optimize/ir.d.ts +84 -0
- package/dist/optimize/ir.d.ts.map +1 -0
- package/dist/optimize/ir.js +60 -0
- package/dist/optimize/ir.js.map +1 -0
- package/dist/optimize/rules.d.ts +8 -0
- package/dist/optimize/rules.d.ts.map +1 -0
- package/dist/optimize/rules.js +136 -0
- package/dist/optimize/rules.js.map +1 -0
- package/package.json +1 -1
- package/src/adapters/indexed.ts +60 -24
- package/src/context.ts +24 -0
- package/src/engine.ts +303 -33
- package/src/index.ts +6 -1
- package/src/optimize/analysis.ts +183 -0
- package/src/optimize/driver.ts +37 -0
- package/src/optimize/hash-index.ts +91 -0
- package/src/optimize/index.ts +16 -0
- package/src/optimize/ir.ts +128 -0
- package/src/optimize/rules.ts +136 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The optimizer driver: applies a rule set to a block's logical plan until no
|
|
2
|
+
// rule changes it (a fixpoint, bounded), and caches the result per AST node —
|
|
3
|
+
// the analysis is syntactic, so a plan depends only on the node, its static
|
|
4
|
+
// depth, the run's binding count and the rule set.
|
|
5
|
+
|
|
6
|
+
import type { OpNode } from "../ast.ts";
|
|
7
|
+
import type { BlockPlan, Rule, RuleContext } from "./ir.ts";
|
|
8
|
+
import { logicalBlock } from "./ir.ts";
|
|
9
|
+
|
|
10
|
+
const MAX_PASSES = 8;
|
|
11
|
+
|
|
12
|
+
/** Rewrite `logicalBlock(node, depth)` to a fixpoint of `rules`. */
|
|
13
|
+
export function optimizeBlock(node: OpNode, depth: number, ctx: RuleContext, rules: readonly Rule[]): BlockPlan {
|
|
14
|
+
let plan = logicalBlock(node, depth);
|
|
15
|
+
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
16
|
+
let changed = false;
|
|
17
|
+
for (const rule of rules) {
|
|
18
|
+
const next = rule(plan, ctx);
|
|
19
|
+
if (next !== null && next !== plan) { plan = next; changed = true; }
|
|
20
|
+
}
|
|
21
|
+
if (!changed) break;
|
|
22
|
+
}
|
|
23
|
+
return plan;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Cached { depth: number; bindingCount: number; rules: readonly Rule[]; plan: BlockPlan }
|
|
27
|
+
const cache = new WeakMap<OpNode, Cached>();
|
|
28
|
+
|
|
29
|
+
/** `optimizeBlock`, memoized on the node (re-planned when depth, binding count
|
|
30
|
+
* or rule set differ from the cached entry). */
|
|
31
|
+
export function planFor(node: OpNode, depth: number, ctx: RuleContext, rules: readonly Rule[]): BlockPlan {
|
|
32
|
+
const hit = cache.get(node);
|
|
33
|
+
if (hit && hit.depth === depth && hit.bindingCount === ctx.bindingCount && hit.rules === rules) return hit.plan;
|
|
34
|
+
const plan = optimizeBlock(node, depth, ctx, rules);
|
|
35
|
+
cache.set(node, { depth, bindingCount: ctx.bindingCount, rules, plan });
|
|
36
|
+
return plan;
|
|
37
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// A hash index over the rows of one collection, keyed under OQX equality
|
|
2
|
+
// (SEMANTICS §5) — the only key discipline that makes an index probe return
|
|
3
|
+
// exactly the rows a `local == value` scan would keep:
|
|
4
|
+
//
|
|
5
|
+
// • absent is one value: `undefined` and `null` share a bucket;
|
|
6
|
+
// • numbers are doubles: `-0` meets `0`; `NaN` equals nothing, so a row whose
|
|
7
|
+
// key is `NaN` is never indexed and a probe for `NaN` finds nothing;
|
|
8
|
+
// • strings, booleans and bigints key by value with a type tag, so `"5"`,
|
|
9
|
+
// `5` and `true` stay apart;
|
|
10
|
+
// • objects, arrays, functions and symbols key by REFERENCE — the reference
|
|
11
|
+
// engine's `===` — never structurally.
|
|
12
|
+
//
|
|
13
|
+
// Buckets hold row POSITIONS in ascending order, so a probe yields rows in the
|
|
14
|
+
// receiver's order (§3 and §13 — a block's rows are the receiver's, in order).
|
|
15
|
+
|
|
16
|
+
/** A pre-built equality index a `DataContext` may expose for a collection
|
|
17
|
+
* (see `DataContext.indexFor`): `lookup(value)` returns the ascending positions
|
|
18
|
+
* (into `toRows(collection)`) of the rows whose key equals `value` under §5.
|
|
19
|
+
*
|
|
20
|
+
* `lookupRows`, when present, answers the same probe with the ROWS themselves —
|
|
21
|
+
* exactly the rows `lookup(value)` would select, in receiver order — without
|
|
22
|
+
* the engine ever materializing `toRows(collection)`. A store-backed context
|
|
23
|
+
* implements it over its own indexes (one indexed statement per probe); the
|
|
24
|
+
* engine prefers it for a statically stable receiver, so the collection is
|
|
25
|
+
* never read whole. A throw is the query's error (as a `get` that throws is). */
|
|
26
|
+
export interface RowIndex {
|
|
27
|
+
lookup(value: unknown): readonly number[];
|
|
28
|
+
lookupRows?(value: unknown): Iterable<unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const NONE: readonly number[] = Object.freeze([]);
|
|
32
|
+
|
|
33
|
+
/** The engine's own `RowIndex`: `add` every row's key in row order, then probe. */
|
|
34
|
+
export class HashIndex implements RowIndex {
|
|
35
|
+
private prim = new Map<string, number[]>();
|
|
36
|
+
private refs = new Map<unknown, number[]>();
|
|
37
|
+
private count = 0;
|
|
38
|
+
|
|
39
|
+
/** Record that the row at `position` has key `value`. Positions must be
|
|
40
|
+
* added in ascending order (row order). A `NaN` key is not indexed. */
|
|
41
|
+
add(value: unknown, position: number): void {
|
|
42
|
+
const k = primitiveKey(value);
|
|
43
|
+
if (k === null) return; // NaN: equals nothing
|
|
44
|
+
const map = k === undefined ? this.refs : this.prim;
|
|
45
|
+
const key = k === undefined ? value : k;
|
|
46
|
+
let bucket = map.get(key);
|
|
47
|
+
if (!bucket) map.set(key, (bucket = []));
|
|
48
|
+
bucket.push(position);
|
|
49
|
+
this.count++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lookup(value: unknown): readonly number[] {
|
|
53
|
+
const k = primitiveKey(value);
|
|
54
|
+
if (k === null) return NONE;
|
|
55
|
+
return (k === undefined ? this.refs.get(value) : this.prim.get(k)) ?? NONE;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** How many rows were indexed (rows with a `NaN` key are not). */
|
|
59
|
+
get size(): number {
|
|
60
|
+
return this.count;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The tagged bucket key of a primitive, `null` for `NaN` (no key: it equals
|
|
65
|
+
* nothing), `undefined` for a reference-keyed value (object, array, function,
|
|
66
|
+
* symbol — compared by identity). Mirrors `semantics.equals`: `===` after
|
|
67
|
+
* normalizing `undefined` to `null`. */
|
|
68
|
+
export function primitiveKey(v: unknown): string | null | undefined {
|
|
69
|
+
switch (typeof v) {
|
|
70
|
+
case "undefined": return "∅"; // absent ≡ absent
|
|
71
|
+
case "object": return v === null ? "∅" : undefined;
|
|
72
|
+
case "number": return Number.isNaN(v) ? null : `n${v}`; // `${-0}` is "0": -0 meets 0
|
|
73
|
+
case "string": return `s${v}`;
|
|
74
|
+
case "boolean": return v ? "t" : "f";
|
|
75
|
+
case "bigint": return `i${v}`;
|
|
76
|
+
default: return undefined; // function, symbol: reference identity
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Intersect two ascending position lists, ascending. */
|
|
81
|
+
export function intersectPositions(a: readonly number[], b: readonly number[]): readonly number[] {
|
|
82
|
+
const out: number[] = [];
|
|
83
|
+
let i = 0, j = 0;
|
|
84
|
+
while (i < a.length && j < b.length) {
|
|
85
|
+
const x = a[i]!, y = b[j]!;
|
|
86
|
+
if (x === y) { out.push(x); i++; j++; }
|
|
87
|
+
else if (x < y) i++;
|
|
88
|
+
else j++;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The optimizer: a logical plan per nested block, rules that rewrite it, a
|
|
2
|
+
// driver that applies them to a fixpoint, and the equality-faithful hash index
|
|
3
|
+
// the hash-probe rule executes with. See `rules.ts` for the catalog and each
|
|
4
|
+
// rule's semantics argument; README "Performance: relational patterns" for the
|
|
5
|
+
// user-facing summary.
|
|
6
|
+
|
|
7
|
+
export type { BlockPlan, Correlation, Rule, RuleContext } from "./ir.ts";
|
|
8
|
+
export { logicalBlock, conjunction, conjuncts, residualWithout, lookupOrder } from "./ir.ts";
|
|
9
|
+
export { optimizeBlock, planFor } from "./driver.ts";
|
|
10
|
+
export { correlatedEqualityProbe, stableReceiver, invariantBlock, cardinalityOnly, DEFAULT_RULES } from "./rules.ts";
|
|
11
|
+
export type { RowIndex } from "./hash-index.ts";
|
|
12
|
+
export { HashIndex, primitiveKey, intersectPositions } from "./hash-index.ts";
|
|
13
|
+
export {
|
|
14
|
+
exprReadsScopeIn, whereReadsScopeIn, opReadsScopeIn, exprReadsCurrentScope,
|
|
15
|
+
exprRaiseFree, whereRaiseFree, opRaiseFree, opHasLifts, whereHasLifts, localPath, SCOPE_INTRINSICS,
|
|
16
|
+
} from "./analysis.ts";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The optimizer's logical representation of one nested block (an `OpNode`):
|
|
2
|
+
// where its rows come from, which predicates correlate them with the enclosing
|
|
3
|
+
// rows, what remains to be checked per row, how they are shaped, and how the
|
|
4
|
+
// consumer reduces them. Rules rewrite a `BlockPlan`; the engine executes it.
|
|
5
|
+
//
|
|
6
|
+
// A plan starts as a faithful transcription of the AST (`logicalBlock`): every
|
|
7
|
+
// predicate residual, nothing correlated, nothing memoized. Rules move work
|
|
8
|
+
// from the per-row path into the per-run or per-probe path only when the
|
|
9
|
+
// semantics argument in `rules.ts` holds; the engine's fallbacks (an index that
|
|
10
|
+
// cannot be built, a receiver that is not stable) always degrade to the plain
|
|
11
|
+
// scan the plan started from.
|
|
12
|
+
|
|
13
|
+
import type { Consumer, Expr, Follow, OpNode, OrderSpec, SelectItem, Where } from "../ast.ts";
|
|
14
|
+
|
|
15
|
+
/** One hoisted equality `local == outer` from the block's top-level `&&`
|
|
16
|
+
* conjunction: `local` reads only the block's row (an identifier or member
|
|
17
|
+
* chain; `path` is its property path, `[]` for `$value`), `outer` reads nothing
|
|
18
|
+
* from the block's row (outer references, bindings, literals, arithmetic over
|
|
19
|
+
* them) and cannot raise. */
|
|
20
|
+
export interface Correlation {
|
|
21
|
+
readonly local: Expr;
|
|
22
|
+
readonly outer: Expr;
|
|
23
|
+
readonly path: readonly string[];
|
|
24
|
+
/** Position of the equality among `conjuncts(where)`, so a probe that
|
|
25
|
+
* answers ONE correlation (a store-backed `lookupRows`) can put the others
|
|
26
|
+
* back in their place as residual conjuncts. */
|
|
27
|
+
readonly index: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The plan for one block. `receiver`/`from`/`projection`/`orderBy`/`bound`/
|
|
31
|
+
* `consumer`/`follow`/`distinct` transcribe the AST; `correlated`/`residual`
|
|
32
|
+
* split its `where`; `receiverStable`, `invariant` and `fromCardinality` are the
|
|
33
|
+
* annotations rules add. */
|
|
34
|
+
export interface BlockPlan {
|
|
35
|
+
readonly node: OpNode;
|
|
36
|
+
/** Scope depth of the ENCLOSING scope (root = 0): the receiver is read there;
|
|
37
|
+
* the block's rows are scopes at `depth + 1`. */
|
|
38
|
+
readonly depth: number;
|
|
39
|
+
readonly consumer: Consumer;
|
|
40
|
+
readonly receiver: Expr;
|
|
41
|
+
readonly from: readonly Expr[];
|
|
42
|
+
readonly where: Where | null;
|
|
43
|
+
readonly projection: { readonly select: readonly SelectItem[]; readonly values: boolean };
|
|
44
|
+
readonly orderBy: readonly OrderSpec[] | null;
|
|
45
|
+
readonly distinct: boolean;
|
|
46
|
+
readonly bound: { readonly limit: Expr | undefined; readonly offset: Expr | undefined };
|
|
47
|
+
readonly follow: Follow | null;
|
|
48
|
+
/** Equalities answered by an index probe on the receiver (in conjunct order). */
|
|
49
|
+
readonly correlated: readonly Correlation[];
|
|
50
|
+
/** The conjuncts still evaluated per row once the probe has selected its
|
|
51
|
+
* bucket; equals `where` when nothing is correlated. */
|
|
52
|
+
readonly residual: Where | null;
|
|
53
|
+
/** The receiver reads nothing from any enclosing row (only the root scope
|
|
54
|
+
* and bindings), so its rows are the same for every enclosing row and can be
|
|
55
|
+
* materialized and indexed once per run. */
|
|
56
|
+
readonly receiverStable: boolean;
|
|
57
|
+
/** The whole block reads nothing from any enclosing row and has no side
|
|
58
|
+
* effects on one (no lifts): its value is the same for every enclosing row
|
|
59
|
+
* and is computed once per run. */
|
|
60
|
+
readonly invariant: boolean;
|
|
61
|
+
/** `exists`/`none`/`count` may be answered from the cardinality of the
|
|
62
|
+
* accessed rows when no residual predicate remains (nothing per row to
|
|
63
|
+
* evaluate, order, dedup or project). */
|
|
64
|
+
readonly fromCardinality: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a rule may consult besides the plan. */
|
|
68
|
+
export interface RuleContext {
|
|
69
|
+
/** How many positional bindings this run has (a `${i}` with `i` at or past
|
|
70
|
+
* it raises when evaluated, so it is not raise-free). */
|
|
71
|
+
readonly bindingCount: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A rewrite: return an improved plan, or `null` when the rule does not apply
|
|
75
|
+
* (or has already been applied). Rules must be idempotent on their own output. */
|
|
76
|
+
export type Rule = (plan: BlockPlan, ctx: RuleContext) => BlockPlan | null;
|
|
77
|
+
|
|
78
|
+
/** The faithful transcription of a block: a scan with every predicate residual. */
|
|
79
|
+
export function logicalBlock(node: OpNode, depth: number): BlockPlan {
|
|
80
|
+
const sub = node.sub;
|
|
81
|
+
return {
|
|
82
|
+
node,
|
|
83
|
+
depth,
|
|
84
|
+
consumer: node.op,
|
|
85
|
+
receiver: node.receiver,
|
|
86
|
+
from: sub.from,
|
|
87
|
+
where: sub.where,
|
|
88
|
+
projection: { select: sub.select, values: sub.values ?? false },
|
|
89
|
+
orderBy: sub.orderBy,
|
|
90
|
+
distinct: node.distinct ?? false,
|
|
91
|
+
bound: { limit: sub.limit, offset: sub.offset },
|
|
92
|
+
follow: sub.follow,
|
|
93
|
+
correlated: [],
|
|
94
|
+
residual: sub.where,
|
|
95
|
+
receiverStable: false,
|
|
96
|
+
invariant: false,
|
|
97
|
+
fromCardinality: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Rebuild a conjunction from its remaining parts. */
|
|
102
|
+
export function conjunction(parts: readonly Where[]): Where | null {
|
|
103
|
+
return parts.length === 0 ? null : parts.length === 1 ? parts[0]! : { kind: "and", parts: [...parts] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The top-level `&&` conjuncts of a where tree (a non-`and` tree is one). */
|
|
107
|
+
export function conjuncts(where: Where): readonly Where[] {
|
|
108
|
+
return where.kind === "and" ? where.parts : [where];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The residual when exactly ONE correlation is answered by a probe (a context
|
|
112
|
+
* index's `lookupRows`): every other conjunct, the remaining equalities
|
|
113
|
+
* included, stays in its original place. A kept equality is true by
|
|
114
|
+
* construction for the rows the probe selects and, being two reads, can neither
|
|
115
|
+
* raise nor bind, so the scan's strict left-to-right order and outcome hold. */
|
|
116
|
+
export function residualWithout(plan: BlockPlan, answered: Correlation): Where | null {
|
|
117
|
+
return plan.where ? conjunction(conjuncts(plan.where).filter((_, i) => i !== answered.index)) : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The order in which a single-correlation probe tries the plan's correlations:
|
|
121
|
+
* those whose outer side varies with the enclosing row first (a literal selects
|
|
122
|
+
* the same rows for every enclosing row and narrows nothing), each group in
|
|
123
|
+
* conjunct order. */
|
|
124
|
+
export function lookupOrder(plan: BlockPlan): readonly Correlation[] {
|
|
125
|
+
const all = plan.correlated;
|
|
126
|
+
if (all.length < 2) return all;
|
|
127
|
+
return [...all.filter((c) => c.outer.kind !== "lit"), ...all.filter((c) => c.outer.kind === "lit")];
|
|
128
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The rule catalog. Each rule states its precondition and the argument that the
|
|
2
|
+
// rewritten plan is indistinguishable from the scan it replaces — same rows in
|
|
3
|
+
// the same order, same lifts, same error (same message) at the same point, or
|
|
4
|
+
// no error — under SEMANTICS §13's strict left-to-right `&&`.
|
|
5
|
+
|
|
6
|
+
import type { Expr, Where } from "../ast.ts";
|
|
7
|
+
import {
|
|
8
|
+
exprRaiseFree, exprReadsCurrentScope, exprReadsScopeIn, localPath, opHasLifts, opReadsScopeIn,
|
|
9
|
+
whereHasLifts, whereRaiseFree,
|
|
10
|
+
} from "./analysis.ts";
|
|
11
|
+
import type { Correlation, Rule, RuleContext } from "./ir.ts";
|
|
12
|
+
import { conjunction, conjuncts } from "./ir.ts";
|
|
13
|
+
|
|
14
|
+
// ---- rule 1: correlated equality → hash probe ------------------------------------
|
|
15
|
+
//
|
|
16
|
+
// Precondition. The block has no `from` re-projection (its rows ARE the
|
|
17
|
+
// receiver's rows) and no `follow`; its `where` has no lift anywhere (a lift
|
|
18
|
+
// into an enclosing scope by one row could change what the outer side reads
|
|
19
|
+
// for the next row); some top-level conjunct k is `local == outer` (either
|
|
20
|
+
// side) with `local` an identifier/member chain on the block's row and `outer`
|
|
21
|
+
// a raise-free expression reading nothing from the block's row; and every
|
|
22
|
+
// conjunct left of k is raise-free (no call, no `single`, no lift, no bound, no
|
|
23
|
+
// binding past the run's count).
|
|
24
|
+
//
|
|
25
|
+
// Argument. The scan evaluates, per receiver row r in order, c1(r) && … &&
|
|
26
|
+
// ck(r) && …, short-circuiting. The probe selects exactly the rows with
|
|
27
|
+
// local(r) == outer (the index reproduces §5 equality, `outer` is evaluated
|
|
28
|
+
// once because nothing in the block can change what it reads) and evaluates the
|
|
29
|
+
// residual conjunction over them, in receiver order. For a selected row every
|
|
30
|
+
// conjunct is evaluated as the scan would (ck is dropped: it is true by
|
|
31
|
+
// construction and, being two reads, cannot raise or bind). For a row the probe
|
|
32
|
+
// rejects, the scan would have evaluated c1..c(k-1) — which cannot raise or
|
|
33
|
+
// bind — then ck, false, and stopped: no result, no error, no lift. So the two
|
|
34
|
+
// paths keep the same rows, raise the same first error, and bind the same
|
|
35
|
+
// lifts. A probe that cannot be built at run time (a receiver that is not
|
|
36
|
+
// stable, an index whose construction throws) falls back to the scan itself.
|
|
37
|
+
|
|
38
|
+
export const correlatedEqualityProbe: Rule = (plan, ctx) => {
|
|
39
|
+
if (plan.correlated.length > 0 || plan.from.length > 0 || plan.follow || !plan.where) return null;
|
|
40
|
+
if (whereHasLifts(plan.where)) return null;
|
|
41
|
+
const correlated: Correlation[] = [];
|
|
42
|
+
const residual: Where[] = [];
|
|
43
|
+
let prefixRaiseFree = true;
|
|
44
|
+
conjuncts(plan.where).forEach((part, index) => {
|
|
45
|
+
const eq = prefixRaiseFree ? asCorrelation(part, index, ctx) : null;
|
|
46
|
+
if (eq) correlated.push(eq);
|
|
47
|
+
else {
|
|
48
|
+
residual.push(part);
|
|
49
|
+
prefixRaiseFree &&= whereRaiseFree(part, ctx);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (correlated.length === 0) return null;
|
|
53
|
+
return { ...plan, correlated, residual: conjunction(residual) };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** `local == outer` / `outer == local` as a correlation, else null. */
|
|
57
|
+
function asCorrelation(w: Where, index: number, ctx: RuleContext): Correlation | null {
|
|
58
|
+
if (w.kind !== "scalar" || w.expr.kind !== "binary" || w.expr.op !== "==") return null;
|
|
59
|
+
const { left, right } = w.expr;
|
|
60
|
+
return pair(left, right, index, ctx) ?? pair(right, left, index, ctx);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pair(local: Expr, outer: Expr, index: number, ctx: RuleContext): Correlation | null {
|
|
64
|
+
const path = localPath(local);
|
|
65
|
+
if (path === null) return null;
|
|
66
|
+
if (exprReadsCurrentScope(outer) || !exprRaiseFree(outer, ctx)) return null;
|
|
67
|
+
return { local, outer, path, index };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- rule 1b: a receiver stable across enclosing rows ----------------------------
|
|
71
|
+
//
|
|
72
|
+
// Precondition. The receiver expression, read in the enclosing scope (depth
|
|
73
|
+
// `plan.depth`), reads no scope at depth 1..depth: only the root (named roots,
|
|
74
|
+
// `^…^name` reaching it, or past it) and bindings.
|
|
75
|
+
//
|
|
76
|
+
// Argument. Such an expression yields the same value for every enclosing row
|
|
77
|
+
// (evaluation is pure; the root and the bindings do not change within a run),
|
|
78
|
+
// so its rows can be materialized once per run and indexed once per path. A
|
|
79
|
+
// receiver that does read an enclosing row is still indexable when it happens
|
|
80
|
+
// to evaluate to the same object twice (the engine keys indexes by collection
|
|
81
|
+
// identity as well); a receiver that yields a fresh value per row is scanned.
|
|
82
|
+
|
|
83
|
+
export const stableReceiver: Rule = (plan) => {
|
|
84
|
+
if (plan.receiverStable) return null;
|
|
85
|
+
if (exprReadsScopeIn(plan.receiver, plan.depth, plan.depth)) return null;
|
|
86
|
+
return { ...plan, receiverStable: true };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ---- rule 2: invariant nested block → evaluate once per run ----------------------
|
|
90
|
+
//
|
|
91
|
+
// Precondition. Nothing in the block — receiver, `from`, `where`, `select`,
|
|
92
|
+
// `order by`, bound, `follow` — reads a scope at depth 1..depth (an enclosing
|
|
93
|
+
// row), and the block lifts nothing (a lift is a side effect on an enclosing
|
|
94
|
+
// scope that must recur per row).
|
|
95
|
+
//
|
|
96
|
+
// Argument. The block's value is a function of the root scope, the bindings
|
|
97
|
+
// and its own rows only, all constant within a run, so every evaluation yields
|
|
98
|
+
// the same value or raises the same error. The engine evaluates it the first
|
|
99
|
+
// time the scan would (so an error surfaces at the same point) and returns the
|
|
100
|
+
// memoized value afterwards; a run that raised never reaches a second
|
|
101
|
+
// evaluation. The memoized value is shared by reference across the enclosing
|
|
102
|
+
// rows that project it.
|
|
103
|
+
|
|
104
|
+
export const invariantBlock: Rule = (plan) => {
|
|
105
|
+
if (plan.invariant) return null;
|
|
106
|
+
if (opHasLifts(plan.node)) return null;
|
|
107
|
+
if (opReadsScopeIn(plan.node, plan.depth, plan.depth)) return null;
|
|
108
|
+
return { ...plan, invariant: true };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ---- rule 3: exists / none / count from cardinality -------------------------------
|
|
112
|
+
//
|
|
113
|
+
// Precondition. A where-position `exists`, `none` or `count` without `distinct`
|
|
114
|
+
// (dedup projects every row), without `order by` (`count` evaluates the keys),
|
|
115
|
+
// without `follow`, and with nothing left to evaluate per row: either the block
|
|
116
|
+
// has no `where`, or every conjunct is correlated (so the probe's bucket is the
|
|
117
|
+
// matched set).
|
|
118
|
+
//
|
|
119
|
+
// Argument. With no residual predicate the matched rows are exactly the
|
|
120
|
+
// accessed rows (§13: the receiver's rows filtered by `where`), entering a row
|
|
121
|
+
// as a scope cannot raise, and the consumers only need the count after the
|
|
122
|
+
// bound (§18): `count` is `boundedCount(n)`, `exists` is `boundedCount(n) > 0`,
|
|
123
|
+
// `none` its negation — what the scan computes after materializing. When the
|
|
124
|
+
// probe is unavailable at run time the residual is the whole `where` and the
|
|
125
|
+
// engine takes the per-row path.
|
|
126
|
+
|
|
127
|
+
export const cardinalityOnly: Rule = (plan) => {
|
|
128
|
+
if (plan.fromCardinality) return null;
|
|
129
|
+
if (plan.consumer !== "exists" && plan.consumer !== "none" && plan.consumer !== "count") return null;
|
|
130
|
+
if (plan.distinct || plan.orderBy || plan.follow) return null;
|
|
131
|
+
if (plan.where !== null && plan.residual !== null) return null;
|
|
132
|
+
return { ...plan, fromCardinality: true };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** The rules the engine applies by default, in order. */
|
|
136
|
+
export const DEFAULT_RULES: readonly Rule[] = [correlatedEqualityProbe, stableReceiver, invariantBlock, cardinalityOnly];
|