@omgbase/oqx 0.13.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +184 -17
  3. package/dist/adapters/indexed.d.ts +13 -1
  4. package/dist/adapters/indexed.d.ts.map +1 -1
  5. package/dist/adapters/indexed.js +55 -24
  6. package/dist/adapters/indexed.js.map +1 -1
  7. package/dist/ast.d.ts +5 -1
  8. package/dist/ast.d.ts.map +1 -1
  9. package/dist/context.d.ts +24 -0
  10. package/dist/context.d.ts.map +1 -1
  11. package/dist/context.js.map +1 -1
  12. package/dist/engine.d.ts +55 -2
  13. package/dist/engine.d.ts.map +1 -1
  14. package/dist/engine.js +295 -37
  15. package/dist/engine.js.map +1 -1
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/optimize/analysis.d.ts +45 -0
  21. package/dist/optimize/analysis.d.ts.map +1 -0
  22. package/dist/optimize/analysis.js +194 -0
  23. package/dist/optimize/analysis.js.map +1 -0
  24. package/dist/optimize/driver.d.ts +8 -0
  25. package/dist/optimize/driver.d.ts.map +1 -0
  26. package/dist/optimize/driver.js +35 -0
  27. package/dist/optimize/driver.js.map +1 -0
  28. package/dist/optimize/hash-index.d.ts +34 -0
  29. package/dist/optimize/hash-index.d.ts.map +1 -0
  30. package/dist/optimize/hash-index.js +79 -0
  31. package/dist/optimize/hash-index.js.map +1 -0
  32. package/dist/optimize/index.d.ts +8 -0
  33. package/dist/optimize/index.d.ts.map +1 -0
  34. package/dist/optimize/index.js +11 -0
  35. package/dist/optimize/index.js.map +1 -0
  36. package/dist/optimize/ir.d.ts +84 -0
  37. package/dist/optimize/ir.d.ts.map +1 -0
  38. package/dist/optimize/ir.js +60 -0
  39. package/dist/optimize/ir.js.map +1 -0
  40. package/dist/optimize/rules.d.ts +8 -0
  41. package/dist/optimize/rules.d.ts.map +1 -0
  42. package/dist/optimize/rules.js +136 -0
  43. package/dist/optimize/rules.js.map +1 -0
  44. package/dist/parser.d.ts.map +1 -1
  45. package/dist/parser.js +59 -5
  46. package/dist/parser.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/adapters/indexed.ts +60 -24
  49. package/src/ast.ts +6 -1
  50. package/src/context.ts +24 -0
  51. package/src/engine.ts +329 -35
  52. package/src/index.ts +7 -2
  53. package/src/optimize/analysis.ts +183 -0
  54. package/src/optimize/driver.ts +37 -0
  55. package/src/optimize/hash-index.ts +91 -0
  56. package/src/optimize/index.ts +16 -0
  57. package/src/optimize/ir.ts +128 -0
  58. package/src/optimize/rules.ts +136 -0
  59. package/src/parser.ts +54 -6
@@ -0,0 +1,79 @@
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
+ const NONE = Object.freeze([]);
16
+ /** The engine's own `RowIndex`: `add` every row's key in row order, then probe. */
17
+ export class HashIndex {
18
+ prim = new Map();
19
+ refs = new Map();
20
+ count = 0;
21
+ /** Record that the row at `position` has key `value`. Positions must be
22
+ * added in ascending order (row order). A `NaN` key is not indexed. */
23
+ add(value, position) {
24
+ const k = primitiveKey(value);
25
+ if (k === null)
26
+ return; // NaN: equals nothing
27
+ const map = k === undefined ? this.refs : this.prim;
28
+ const key = k === undefined ? value : k;
29
+ let bucket = map.get(key);
30
+ if (!bucket)
31
+ map.set(key, (bucket = []));
32
+ bucket.push(position);
33
+ this.count++;
34
+ }
35
+ lookup(value) {
36
+ const k = primitiveKey(value);
37
+ if (k === null)
38
+ return NONE;
39
+ return (k === undefined ? this.refs.get(value) : this.prim.get(k)) ?? NONE;
40
+ }
41
+ /** How many rows were indexed (rows with a `NaN` key are not). */
42
+ get size() {
43
+ return this.count;
44
+ }
45
+ }
46
+ /** The tagged bucket key of a primitive, `null` for `NaN` (no key: it equals
47
+ * nothing), `undefined` for a reference-keyed value (object, array, function,
48
+ * symbol — compared by identity). Mirrors `semantics.equals`: `===` after
49
+ * normalizing `undefined` to `null`. */
50
+ export function primitiveKey(v) {
51
+ switch (typeof v) {
52
+ case "undefined": return "∅"; // absent ≡ absent
53
+ case "object": return v === null ? "∅" : undefined;
54
+ case "number": return Number.isNaN(v) ? null : `n${v}`; // `${-0}` is "0": -0 meets 0
55
+ case "string": return `s${v}`;
56
+ case "boolean": return v ? "t" : "f";
57
+ case "bigint": return `i${v}`;
58
+ default: return undefined; // function, symbol: reference identity
59
+ }
60
+ }
61
+ /** Intersect two ascending position lists, ascending. */
62
+ export function intersectPositions(a, b) {
63
+ const out = [];
64
+ let i = 0, j = 0;
65
+ while (i < a.length && j < b.length) {
66
+ const x = a[i], y = b[j];
67
+ if (x === y) {
68
+ out.push(x);
69
+ i++;
70
+ j++;
71
+ }
72
+ else if (x < y)
73
+ i++;
74
+ else
75
+ j++;
76
+ }
77
+ return out;
78
+ }
79
+ //# sourceMappingURL=hash-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash-index.js","sourceRoot":"","sources":["../../src/optimize/hash-index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export type { BlockPlan, Correlation, Rule, RuleContext } from "./ir.js";
2
+ export { logicalBlock, conjunction, conjuncts, residualWithout, lookupOrder } from "./ir.js";
3
+ export { optimizeBlock, planFor } from "./driver.js";
4
+ export { correlatedEqualityProbe, stableReceiver, invariantBlock, cardinalityOnly, DEFAULT_RULES } from "./rules.js";
5
+ export type { RowIndex } from "./hash-index.js";
6
+ export { HashIndex, primitiveKey, intersectPositions } from "./hash-index.js";
7
+ export { exprReadsScopeIn, whereReadsScopeIn, opReadsScopeIn, exprReadsCurrentScope, exprRaiseFree, whereRaiseFree, opRaiseFree, opHasLifts, whereHasLifts, localPath, SCOPE_INTRINSICS, } from "./analysis.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/optimize/index.ts"],"names":[],"mappings":"AAMA,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,uBAAuB,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACrH,YAAY,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EACL,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,qBAAqB,EAC1E,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,gBAAgB,GACnG,MAAM,eAAe,CAAC"}
@@ -0,0 +1,11 @@
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
+ export { logicalBlock, conjunction, conjuncts, residualWithout, lookupOrder } from "./ir.js";
7
+ export { optimizeBlock, planFor } from "./driver.js";
8
+ export { correlatedEqualityProbe, stableReceiver, invariantBlock, cardinalityOnly, DEFAULT_RULES } from "./rules.js";
9
+ export { HashIndex, primitiveKey, intersectPositions } from "./hash-index.js";
10
+ export { exprReadsScopeIn, whereReadsScopeIn, opReadsScopeIn, exprReadsCurrentScope, exprRaiseFree, whereRaiseFree, opRaiseFree, opHasLifts, whereHasLifts, localPath, SCOPE_INTRINSICS, } from "./analysis.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/optimize/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,uBAAuB;AAGvB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,uBAAuB,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAErH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EACL,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,qBAAqB,EAC1E,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,gBAAgB,GACnG,MAAM,eAAe,CAAC"}
@@ -0,0 +1,84 @@
1
+ import type { Consumer, Expr, Follow, OpNode, OrderSpec, SelectItem, Where } from "../ast.js";
2
+ /** One hoisted equality `local == outer` from the block's top-level `&&`
3
+ * conjunction: `local` reads only the block's row (an identifier or member
4
+ * chain; `path` is its property path, `[]` for `$value`), `outer` reads nothing
5
+ * from the block's row (outer references, bindings, literals, arithmetic over
6
+ * them) and cannot raise. */
7
+ export interface Correlation {
8
+ readonly local: Expr;
9
+ readonly outer: Expr;
10
+ readonly path: readonly string[];
11
+ /** Position of the equality among `conjuncts(where)`, so a probe that
12
+ * answers ONE correlation (a store-backed `lookupRows`) can put the others
13
+ * back in their place as residual conjuncts. */
14
+ readonly index: number;
15
+ }
16
+ /** The plan for one block. `receiver`/`from`/`projection`/`orderBy`/`bound`/
17
+ * `consumer`/`follow`/`distinct` transcribe the AST; `correlated`/`residual`
18
+ * split its `where`; `receiverStable`, `invariant` and `fromCardinality` are the
19
+ * annotations rules add. */
20
+ export interface BlockPlan {
21
+ readonly node: OpNode;
22
+ /** Scope depth of the ENCLOSING scope (root = 0): the receiver is read there;
23
+ * the block's rows are scopes at `depth + 1`. */
24
+ readonly depth: number;
25
+ readonly consumer: Consumer;
26
+ readonly receiver: Expr;
27
+ readonly from: readonly Expr[];
28
+ readonly where: Where | null;
29
+ readonly projection: {
30
+ readonly select: readonly SelectItem[];
31
+ readonly values: boolean;
32
+ };
33
+ readonly orderBy: readonly OrderSpec[] | null;
34
+ readonly distinct: boolean;
35
+ readonly bound: {
36
+ readonly limit: Expr | undefined;
37
+ readonly offset: Expr | undefined;
38
+ };
39
+ readonly follow: Follow | null;
40
+ /** Equalities answered by an index probe on the receiver (in conjunct order). */
41
+ readonly correlated: readonly Correlation[];
42
+ /** The conjuncts still evaluated per row once the probe has selected its
43
+ * bucket; equals `where` when nothing is correlated. */
44
+ readonly residual: Where | null;
45
+ /** The receiver reads nothing from any enclosing row (only the root scope
46
+ * and bindings), so its rows are the same for every enclosing row and can be
47
+ * materialized and indexed once per run. */
48
+ readonly receiverStable: boolean;
49
+ /** The whole block reads nothing from any enclosing row and has no side
50
+ * effects on one (no lifts): its value is the same for every enclosing row
51
+ * and is computed once per run. */
52
+ readonly invariant: boolean;
53
+ /** `exists`/`none`/`count` may be answered from the cardinality of the
54
+ * accessed rows when no residual predicate remains (nothing per row to
55
+ * evaluate, order, dedup or project). */
56
+ readonly fromCardinality: boolean;
57
+ }
58
+ /** What a rule may consult besides the plan. */
59
+ export interface RuleContext {
60
+ /** How many positional bindings this run has (a `${i}` with `i` at or past
61
+ * it raises when evaluated, so it is not raise-free). */
62
+ readonly bindingCount: number;
63
+ }
64
+ /** A rewrite: return an improved plan, or `null` when the rule does not apply
65
+ * (or has already been applied). Rules must be idempotent on their own output. */
66
+ export type Rule = (plan: BlockPlan, ctx: RuleContext) => BlockPlan | null;
67
+ /** The faithful transcription of a block: a scan with every predicate residual. */
68
+ export declare function logicalBlock(node: OpNode, depth: number): BlockPlan;
69
+ /** Rebuild a conjunction from its remaining parts. */
70
+ export declare function conjunction(parts: readonly Where[]): Where | null;
71
+ /** The top-level `&&` conjuncts of a where tree (a non-`and` tree is one). */
72
+ export declare function conjuncts(where: Where): readonly Where[];
73
+ /** The residual when exactly ONE correlation is answered by a probe (a context
74
+ * index's `lookupRows`): every other conjunct, the remaining equalities
75
+ * included, stays in its original place. A kept equality is true by
76
+ * construction for the rows the probe selects and, being two reads, can neither
77
+ * raise nor bind, so the scan's strict left-to-right order and outcome hold. */
78
+ export declare function residualWithout(plan: BlockPlan, answered: Correlation): Where | null;
79
+ /** The order in which a single-correlation probe tries the plan's correlations:
80
+ * those whose outer side varies with the enclosing row first (a literal selects
81
+ * the same rows for every enclosing row and narrows nothing), each group in
82
+ * conjunct order. */
83
+ export declare function lookupOrder(plan: BlockPlan): readonly Correlation[];
84
+ //# sourceMappingURL=ir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../../src/optimize/ir.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAE9F;;;;6BAI6B;AAC7B,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;oDAEgD;IAChD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;4BAG4B;AAC5B,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;qDACiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,IAAI,EAAE,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IAC1F,QAAQ,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,IAAI,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,GAAG,SAAS,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,SAAS,CAAA;KAAE,CAAC;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC;IAC5C;4DACwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,KAAK,GAAG,IAAI,CAAC;IAChC;;gDAE4C;IAC5C,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;uCAEmC;IACnC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;6CAEyC;IACzC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B;6DACyD;IACzD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;kFACkF;AAClF,MAAM,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,KAAK,SAAS,GAAG,IAAI,CAAC;AAE3E,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAoBnE;AAED,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,KAAK,EAAE,GAAG,KAAK,GAAG,IAAI,CAEjE;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,KAAK,EAAE,CAExD;AAED;;;;gFAIgF;AAChF,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,GAAG,KAAK,GAAG,IAAI,CAEpF;AAED;;;qBAGqB;AACrB,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,WAAW,EAAE,CAInE"}
@@ -0,0 +1,60 @@
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
+ /** The faithful transcription of a block: a scan with every predicate residual. */
13
+ export function logicalBlock(node, depth) {
14
+ const sub = node.sub;
15
+ return {
16
+ node,
17
+ depth,
18
+ consumer: node.op,
19
+ receiver: node.receiver,
20
+ from: sub.from,
21
+ where: sub.where,
22
+ projection: { select: sub.select, values: sub.values ?? false },
23
+ orderBy: sub.orderBy,
24
+ distinct: node.distinct ?? false,
25
+ bound: { limit: sub.limit, offset: sub.offset },
26
+ follow: sub.follow,
27
+ correlated: [],
28
+ residual: sub.where,
29
+ receiverStable: false,
30
+ invariant: false,
31
+ fromCardinality: false,
32
+ };
33
+ }
34
+ /** Rebuild a conjunction from its remaining parts. */
35
+ export function conjunction(parts) {
36
+ return parts.length === 0 ? null : parts.length === 1 ? parts[0] : { kind: "and", parts: [...parts] };
37
+ }
38
+ /** The top-level `&&` conjuncts of a where tree (a non-`and` tree is one). */
39
+ export function conjuncts(where) {
40
+ return where.kind === "and" ? where.parts : [where];
41
+ }
42
+ /** The residual when exactly ONE correlation is answered by a probe (a context
43
+ * index's `lookupRows`): every other conjunct, the remaining equalities
44
+ * included, stays in its original place. A kept equality is true by
45
+ * construction for the rows the probe selects and, being two reads, can neither
46
+ * raise nor bind, so the scan's strict left-to-right order and outcome hold. */
47
+ export function residualWithout(plan, answered) {
48
+ return plan.where ? conjunction(conjuncts(plan.where).filter((_, i) => i !== answered.index)) : null;
49
+ }
50
+ /** The order in which a single-correlation probe tries the plan's correlations:
51
+ * those whose outer side varies with the enclosing row first (a literal selects
52
+ * the same rows for every enclosing row and narrows nothing), each group in
53
+ * conjunct order. */
54
+ export function lookupOrder(plan) {
55
+ const all = plan.correlated;
56
+ if (all.length < 2)
57
+ return all;
58
+ return [...all.filter((c) => c.outer.kind !== "lit"), ...all.filter((c) => c.outer.kind === "lit")];
59
+ }
60
+ //# sourceMappingURL=ir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ir.js","sourceRoot":"","sources":["../../src/optimize/ir.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,+EAA+E;AAC/E,6EAA6E;AAC7E,8EAA8E;AAC9E,EAAE;AACF,+EAA+E;AAC/E,4EAA4E;AAC5E,yEAAyE;AACzE,gFAAgF;AAChF,8EAA8E;AAC9E,8BAA8B;AAmE9B,mFAAmF;AACnF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,KAAa;IACtD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;IACrB,OAAO;QACL,IAAI;QACJ,KAAK;QACL,QAAQ,EAAE,IAAI,CAAC,EAAE;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,UAAU,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,KAAK,EAAE;QAC/D,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,KAAK;QAChC,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;QAC/C,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,UAAU,EAAE,EAAE;QACd,QAAQ,EAAE,GAAG,CAAC,KAAK;QACnB,cAAc,EAAE,KAAK;QACrB,SAAS,EAAE,KAAK;QAChB,eAAe,EAAE,KAAK;KACvB,CAAC;AACJ,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC;AACzG,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,OAAO,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACtD,CAAC;AAED;;;;gFAIgF;AAChF,MAAM,UAAU,eAAe,CAAC,IAAe,EAAE,QAAqB;IACpE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvG,CAAC;AAED;;;qBAGqB;AACrB,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC;IAC5B,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAC/B,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC;AACtG,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Rule } from "./ir.js";
2
+ export declare const correlatedEqualityProbe: Rule;
3
+ export declare const stableReceiver: Rule;
4
+ export declare const invariantBlock: Rule;
5
+ export declare const cardinalityOnly: Rule;
6
+ /** The rules the engine applies by default, in order. */
7
+ export declare const DEFAULT_RULES: readonly Rule[];
8
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/optimize/rules.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAe,IAAI,EAAe,MAAM,SAAS,CAAC;AA2B9D,eAAO,MAAM,uBAAuB,EAAE,IAgBrC,CAAC;AA6BF,eAAO,MAAM,cAAc,EAAE,IAI5B,CAAC;AAiBF,eAAO,MAAM,cAAc,EAAE,IAK5B,CAAC;AAkBF,eAAO,MAAM,eAAe,EAAE,IAM7B,CAAC;AAEF,yDAAyD;AACzD,eAAO,MAAM,aAAa,EAAE,SAAS,IAAI,EAA+E,CAAC"}
@@ -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
+ import { exprRaiseFree, exprReadsCurrentScope, exprReadsScopeIn, localPath, opHasLifts, opReadsScopeIn, whereHasLifts, whereRaiseFree, } from "./analysis.js";
6
+ import { conjunction, conjuncts } from "./ir.js";
7
+ // ---- rule 1: correlated equality → hash probe ------------------------------------
8
+ //
9
+ // Precondition. The block has no `from` re-projection (its rows ARE the
10
+ // receiver's rows) and no `follow`; its `where` has no lift anywhere (a lift
11
+ // into an enclosing scope by one row could change what the outer side reads
12
+ // for the next row); some top-level conjunct k is `local == outer` (either
13
+ // side) with `local` an identifier/member chain on the block's row and `outer`
14
+ // a raise-free expression reading nothing from the block's row; and every
15
+ // conjunct left of k is raise-free (no call, no `single`, no lift, no bound, no
16
+ // binding past the run's count).
17
+ //
18
+ // Argument. The scan evaluates, per receiver row r in order, c1(r) && … &&
19
+ // ck(r) && …, short-circuiting. The probe selects exactly the rows with
20
+ // local(r) == outer (the index reproduces §5 equality, `outer` is evaluated
21
+ // once because nothing in the block can change what it reads) and evaluates the
22
+ // residual conjunction over them, in receiver order. For a selected row every
23
+ // conjunct is evaluated as the scan would (ck is dropped: it is true by
24
+ // construction and, being two reads, cannot raise or bind). For a row the probe
25
+ // rejects, the scan would have evaluated c1..c(k-1) — which cannot raise or
26
+ // bind — then ck, false, and stopped: no result, no error, no lift. So the two
27
+ // paths keep the same rows, raise the same first error, and bind the same
28
+ // lifts. A probe that cannot be built at run time (a receiver that is not
29
+ // stable, an index whose construction throws) falls back to the scan itself.
30
+ export const correlatedEqualityProbe = (plan, ctx) => {
31
+ if (plan.correlated.length > 0 || plan.from.length > 0 || plan.follow || !plan.where)
32
+ return null;
33
+ if (whereHasLifts(plan.where))
34
+ return null;
35
+ const correlated = [];
36
+ const residual = [];
37
+ let prefixRaiseFree = true;
38
+ conjuncts(plan.where).forEach((part, index) => {
39
+ const eq = prefixRaiseFree ? asCorrelation(part, index, ctx) : null;
40
+ if (eq)
41
+ correlated.push(eq);
42
+ else {
43
+ residual.push(part);
44
+ prefixRaiseFree &&= whereRaiseFree(part, ctx);
45
+ }
46
+ });
47
+ if (correlated.length === 0)
48
+ return null;
49
+ return { ...plan, correlated, residual: conjunction(residual) };
50
+ };
51
+ /** `local == outer` / `outer == local` as a correlation, else null. */
52
+ function asCorrelation(w, index, ctx) {
53
+ if (w.kind !== "scalar" || w.expr.kind !== "binary" || w.expr.op !== "==")
54
+ return null;
55
+ const { left, right } = w.expr;
56
+ return pair(left, right, index, ctx) ?? pair(right, left, index, ctx);
57
+ }
58
+ function pair(local, outer, index, ctx) {
59
+ const path = localPath(local);
60
+ if (path === null)
61
+ return null;
62
+ if (exprReadsCurrentScope(outer) || !exprRaiseFree(outer, ctx))
63
+ return null;
64
+ return { local, outer, path, index };
65
+ }
66
+ // ---- rule 1b: a receiver stable across enclosing rows ----------------------------
67
+ //
68
+ // Precondition. The receiver expression, read in the enclosing scope (depth
69
+ // `plan.depth`), reads no scope at depth 1..depth: only the root (named roots,
70
+ // `^…^name` reaching it, or past it) and bindings.
71
+ //
72
+ // Argument. Such an expression yields the same value for every enclosing row
73
+ // (evaluation is pure; the root and the bindings do not change within a run),
74
+ // so its rows can be materialized once per run and indexed once per path. A
75
+ // receiver that does read an enclosing row is still indexable when it happens
76
+ // to evaluate to the same object twice (the engine keys indexes by collection
77
+ // identity as well); a receiver that yields a fresh value per row is scanned.
78
+ export const stableReceiver = (plan) => {
79
+ if (plan.receiverStable)
80
+ return null;
81
+ if (exprReadsScopeIn(plan.receiver, plan.depth, plan.depth))
82
+ return null;
83
+ return { ...plan, receiverStable: true };
84
+ };
85
+ // ---- rule 2: invariant nested block → evaluate once per run ----------------------
86
+ //
87
+ // Precondition. Nothing in the block — receiver, `from`, `where`, `select`,
88
+ // `order by`, bound, `follow` — reads a scope at depth 1..depth (an enclosing
89
+ // row), and the block lifts nothing (a lift is a side effect on an enclosing
90
+ // scope that must recur per row).
91
+ //
92
+ // Argument. The block's value is a function of the root scope, the bindings
93
+ // and its own rows only, all constant within a run, so every evaluation yields
94
+ // the same value or raises the same error. The engine evaluates it the first
95
+ // time the scan would (so an error surfaces at the same point) and returns the
96
+ // memoized value afterwards; a run that raised never reaches a second
97
+ // evaluation. The memoized value is shared by reference across the enclosing
98
+ // rows that project it.
99
+ export const invariantBlock = (plan) => {
100
+ if (plan.invariant)
101
+ return null;
102
+ if (opHasLifts(plan.node))
103
+ return null;
104
+ if (opReadsScopeIn(plan.node, plan.depth, plan.depth))
105
+ return null;
106
+ return { ...plan, invariant: true };
107
+ };
108
+ // ---- rule 3: exists / none / count from cardinality -------------------------------
109
+ //
110
+ // Precondition. A where-position `exists`, `none` or `count` without `distinct`
111
+ // (dedup projects every row), without `order by` (`count` evaluates the keys),
112
+ // without `follow`, and with nothing left to evaluate per row: either the block
113
+ // has no `where`, or every conjunct is correlated (so the probe's bucket is the
114
+ // matched set).
115
+ //
116
+ // Argument. With no residual predicate the matched rows are exactly the
117
+ // accessed rows (§13: the receiver's rows filtered by `where`), entering a row
118
+ // as a scope cannot raise, and the consumers only need the count after the
119
+ // bound (§18): `count` is `boundedCount(n)`, `exists` is `boundedCount(n) > 0`,
120
+ // `none` its negation — what the scan computes after materializing. When the
121
+ // probe is unavailable at run time the residual is the whole `where` and the
122
+ // engine takes the per-row path.
123
+ export const cardinalityOnly = (plan) => {
124
+ if (plan.fromCardinality)
125
+ return null;
126
+ if (plan.consumer !== "exists" && plan.consumer !== "none" && plan.consumer !== "count")
127
+ return null;
128
+ if (plan.distinct || plan.orderBy || plan.follow)
129
+ return null;
130
+ if (plan.where !== null && plan.residual !== null)
131
+ return null;
132
+ return { ...plan, fromCardinality: true };
133
+ };
134
+ /** The rules the engine applies by default, in order. */
135
+ export const DEFAULT_RULES = [correlatedEqualityProbe, stableReceiver, invariantBlock, cardinalityOnly];
136
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/optimize/rules.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EACV,KAAK,EACN,MAAM,UAAU,CAAC;AAgClB,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAEjF;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAE9C"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EACV,KAAK,EACN,MAAM,UAAU,CAAC;AAgClB,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAEjF;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAE9C"}
package/dist/parser.js CHANGED
@@ -12,9 +12,15 @@
12
12
  // Clause order is FIXED (ADR-020). Within one clause body — the top level or a
13
13
  // consumer block — each clause appears at most once, in exactly this order:
14
14
  //
15
- // select <projection> from <source> where <predicate> follow <relation> {…}
15
+ // select <projection> from <source> where <predicate> follow <dest>, … {…}
16
16
  // order by … limit N offset N
17
17
  //
18
+ // A `follow` destination is a relation of the current row (a receiver, never
19
+ // an outer reference) or a destination block — a receiver (which MAY be an
20
+ // outer reference, `^people`) immediately followed by `collect`/`first`/
21
+ // `single` [`distinct`] `{ body }`, exactly the select-position directive
22
+ // `tryOp(false)` recognizes, re-evaluated per frontier row (language 0.14).
23
+ //
18
24
  // Every clause is optional except that a top-level body needs `from` (the
19
25
  // receiver-plus-consumer form `<receiver> <consumer> { block }` supplies the
20
26
  // source itself, so its block's `from` is an optional re-projection). Only
@@ -372,11 +378,15 @@ class Parser {
372
378
  this.fail("expected a relation after `follow distinct` (`follow distinct <relation>`)");
373
379
  }
374
380
  }
375
- if (this.at("caret")) {
376
- this.fail("`follow` takes a relation of the current row (`follow <relation>`); an outer reference `^name` is not allowed there");
381
+ const destinations = [this.parseFollowDestination()];
382
+ while (this.at("comma")) {
383
+ this.next();
384
+ if (!this.at("ident") && !this.at("binding") && !this.at("caret")) {
385
+ this.fail("expected a relation after ',' (`follow <relation>, <relation>`)");
386
+ }
387
+ destinations.push(this.parseFollowDestination());
377
388
  }
378
- const receiver = this.parseReceiver();
379
- const follow = { receiver, distinct, where: null, frontier: null, depth: null, by: null };
389
+ const follow = { destinations, distinct, where: null, frontier: null, depth: null, by: null };
380
390
  if (!this.at("lbrace"))
381
391
  return follow;
382
392
  this.next(); // '{'
@@ -419,6 +429,50 @@ class Parser {
419
429
  this.next();
420
430
  return follow;
421
431
  }
432
+ // One `follow` destination. The lookahead mirrors `tryOp`: a receiver (binding,
433
+ // or carets + a dotted name) followed by a consumer word and `{` / `distinct {`
434
+ // is a destination block, parsed by `tryOp(false)` itself — but only the
435
+ // select-position consumers (collect/first/single) yield rows to walk; the
436
+ // where-position tests are rejected by name before their body is read. With
437
+ // no directive the tokens are re-read as a plain relation of the current row,
438
+ // where an outer reference is still an error (a `^name` head only makes sense
439
+ // as the receiver of a block, since the block is what varies per row).
440
+ parseFollowDestination() {
441
+ const start = this.pos;
442
+ let isReceiver = false;
443
+ if (this.at("binding")) {
444
+ this.next();
445
+ isReceiver = true;
446
+ }
447
+ else if (this.at("ident") || this.at("caret")) {
448
+ this.parseCarets();
449
+ if (this.at("ident")) {
450
+ this.parseNavFrom(this.next());
451
+ isReceiver = true;
452
+ }
453
+ }
454
+ if (isReceiver && this.at("ident") && CONSUMERS.has(this.peek().value)) {
455
+ const op = this.peek().value;
456
+ const after = this.peekAt(1);
457
+ const opThenBrace = after?.type === "lbrace";
458
+ const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
459
+ if (opThenBrace || opDistinctBrace) {
460
+ if (op !== "collect" && op !== "first" && op !== "single") {
461
+ this.fail(`a follow destination must use collect/first/single, not \`${op}\` (exists/none/count are where-position tests)`);
462
+ }
463
+ this.pos = start;
464
+ const block = this.tryOp(false);
465
+ if (!block)
466
+ this.fail("expected a destination block (`<receiver> collect { … }`) after `follow`");
467
+ return block;
468
+ }
469
+ }
470
+ this.pos = start;
471
+ if (this.at("caret")) {
472
+ this.fail("`follow` takes a relation of the current row (`follow <relation>`); an outer reference `^name` is not allowed there — it may only head a destination block (`follow ^name collect { … }`)");
473
+ }
474
+ return this.parseReceiver();
475
+ }
422
476
  // A receiver/source: a `${…}` binding, or a dotted identifier navigation chain
423
477
  // whose head may be an outer reference (`^rel`, `^^root.rel`) — since a bare
424
478
  // name is the current row's own property, an enclosing row's relation or a