@omgbase/oqx 0.14.0 → 0.15.0
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 +69 -0
- package/README.md +162 -24
- 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/ast.d.ts +1 -1
- package/dist/ast.d.ts.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 +272 -39
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- 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/dist/plan.js +1 -1
- package/dist/plan.js.map +1 -1
- package/dist/semantics.js +1 -1
- package/dist/semantics.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/indexed.ts +60 -24
- package/src/ast.ts +2 -2
- package/src/context.ts +24 -0
- package/src/engine.ts +307 -37
- package/src/index.ts +7 -2
- 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
- package/src/plan.ts +1 -1
- package/src/semantics.ts +1 -1
package/src/adapters/indexed.ts
CHANGED
|
@@ -1,38 +1,66 @@
|
|
|
1
1
|
// An in-memory optimizing planner: it hash-indexes a named root collection on
|
|
2
|
-
// chosen fields and, for a query whose where-clause contains equality
|
|
3
|
-
// on those fields, answers from the index (candidate intersection)
|
|
4
|
-
// full scan. Everything it can't turn into an index probe is left
|
|
5
|
-
// query the in-memory engine finishes over the candidate rows.
|
|
2
|
+
// chosen fields and, for a query whose where-clause contains equality
|
|
3
|
+
// predicates on those fields, answers from the index (candidate intersection)
|
|
4
|
+
// instead of a full scan. Everything it can't turn into an index probe is left
|
|
5
|
+
// as a residual query the in-memory engine finishes over the candidate rows.
|
|
6
6
|
//
|
|
7
7
|
// This is the smallest honest demonstration of plan optimization: same answers
|
|
8
8
|
// as a naive scan, far fewer rows examined, and partial-pushdown correctness via
|
|
9
9
|
// the residual.
|
|
10
|
+
//
|
|
11
|
+
// The same indexes serve the engine's correlated probes: `context()` is a
|
|
12
|
+
// `DataContext` that resolves the root and answers `indexFor(collection, path)`
|
|
13
|
+
// from them, so a nested `^emp first { where id == ^manager_id }` probes the
|
|
14
|
+
// pre-built index instead of building one per run. The planner's own plans
|
|
15
|
+
// carry that context too, so a residual's nested blocks still see every root.
|
|
16
|
+
//
|
|
17
|
+
// Buckets are keyed under OQX equality (`HashIndex`): absent ≡ null, `-0` ≡ `0`,
|
|
18
|
+
// `NaN` matches nothing, objects by reference — exactly what `==` compares, so
|
|
19
|
+
// a probe returns the rows the scan would keep (a plain `Map` would keep `null`
|
|
20
|
+
// and `undefined` apart and match `NaN` to itself).
|
|
10
21
|
|
|
11
22
|
import type { Query } from "../ast.ts";
|
|
23
|
+
import type { DataContext } from "../context.ts";
|
|
24
|
+
import { DefaultContext } from "../context.ts";
|
|
12
25
|
import type { Plan, QueryPlanner } from "../planner.ts";
|
|
13
|
-
import { partitionPushable, residualQuery, asEquality, constValue } from "../plan.ts";
|
|
26
|
+
import { partitionPushable, residualQuery, asEquality, constValue, ROWS_ROOT } from "../plan.ts";
|
|
27
|
+
import type { RowIndex } from "../optimize/hash-index.ts";
|
|
28
|
+
import { HashIndex, intersectPositions } from "../optimize/hash-index.ts";
|
|
14
29
|
|
|
15
30
|
export class IndexedCollection implements QueryPlanner {
|
|
16
31
|
private name: string;
|
|
17
32
|
private rows: readonly unknown[];
|
|
18
|
-
private indexes = new Map<string,
|
|
33
|
+
private indexes = new Map<string, HashIndex>();
|
|
19
34
|
|
|
20
|
-
/** Index `rows` (exposed as root `name`) on each field in `indexFields`.
|
|
35
|
+
/** Index `rows` (exposed as root `name`) on each field in `indexFields`. A
|
|
36
|
+
* field is read as the engine reads a bare identifier (own property; array
|
|
37
|
+
* element for an integer-spelled name); a row without it keys as absent. */
|
|
21
38
|
constructor(name: string, rows: readonly unknown[], indexFields: readonly string[]) {
|
|
22
39
|
this.name = name;
|
|
23
40
|
this.rows = rows;
|
|
41
|
+
const read = new DefaultContext();
|
|
24
42
|
for (const field of indexFields) {
|
|
25
|
-
const idx = new
|
|
26
|
-
|
|
27
|
-
const key = (row as Record<string, unknown>)?.[field];
|
|
28
|
-
let bucket = idx.get(key);
|
|
29
|
-
if (!bucket) idx.set(key, (bucket = []));
|
|
30
|
-
bucket.push(row);
|
|
31
|
-
}
|
|
43
|
+
const idx = new HashIndex();
|
|
44
|
+
rows.forEach((row, i) => idx.add(read.get(row, field), i));
|
|
32
45
|
this.indexes.set(field, idx);
|
|
33
46
|
}
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
/** The pre-built index for `collection` on `path` when `collection` is this
|
|
50
|
+
* collection's rows and `path` is one indexed field — the `DataContext.indexFor`
|
|
51
|
+
* seam, which `context()` wires up. */
|
|
52
|
+
indexFor(collection: unknown, path: readonly string[]): RowIndex | undefined {
|
|
53
|
+
if (collection !== this.rows || path.length !== 1) return undefined;
|
|
54
|
+
return this.indexes.get(path[0]!);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A `DataContext` over plain objects that serves this collection as the
|
|
58
|
+
* root `name` (plus `extraRoots`) and exposes the indexes through
|
|
59
|
+
* `indexFor`, so the engine's correlated probes reuse them. */
|
|
60
|
+
context(extraRoots: Record<string, unknown> = {}): DataContext {
|
|
61
|
+
return new IndexedContext(this, { ...extraRoots, [this.name]: this.rows });
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
plan(query: Query, params: readonly unknown[]): Plan | null {
|
|
37
65
|
if (query.source.kind !== "ident" || query.source.name !== this.name) return null;
|
|
38
66
|
if (query.from.length > 0 || query.follow) return null;
|
|
@@ -44,18 +72,26 @@ export class IndexedCollection implements QueryPlanner {
|
|
|
44
72
|
if (pushed.length === 0) return null; // no index probe available — let the scan handle it
|
|
45
73
|
|
|
46
74
|
// Intersect the candidate sets from each indexed equality (smallest first).
|
|
47
|
-
|
|
48
|
-
for (const e of pushed) {
|
|
75
|
+
const buckets = pushed.map((e) => {
|
|
49
76
|
const eq = asEquality(e)!;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
return this.indexes.get(eq.field)!.lookup(constValue(eq.value, params));
|
|
78
|
+
}).sort((a, b) => a.length - b.length);
|
|
79
|
+
let candidate = buckets[0]!;
|
|
80
|
+
for (let i = 1; i < buckets.length && candidate.length > 0; i++) candidate = intersectPositions(candidate, buckets[i]!);
|
|
81
|
+
const rows = candidate.map((i) => this.rows[i]);
|
|
82
|
+
return { rows: () => rows, residual: residualQuery(query, residual), context: this.context({ [ROWS_ROOT]: rows }) };
|
|
55
83
|
}
|
|
56
84
|
}
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
class IndexedContext extends DefaultContext {
|
|
87
|
+
private owner: IndexedCollection;
|
|
88
|
+
|
|
89
|
+
constructor(owner: IndexedCollection, roots: Record<string, unknown>) {
|
|
90
|
+
super(roots);
|
|
91
|
+
this.owner = owner;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
indexFor(collection: unknown, path: readonly string[]): RowIndex | undefined {
|
|
95
|
+
return this.owner.indexFor(collection, path);
|
|
96
|
+
}
|
|
61
97
|
}
|
package/src/ast.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type RelOp = "==" | "!=" | "<" | "<=" | ">" | ">=";
|
|
|
14
14
|
/** Scalar value/predicate expression, evaluated against a row scope + bindings. */
|
|
15
15
|
export type Expr =
|
|
16
16
|
| { kind: "lit"; value: string | number | boolean | null }
|
|
17
|
-
| { kind: "ident"; name: string } // bare property of the CURRENT row/scope only (never climbs); `$
|
|
17
|
+
| { kind: "ident"; name: string } // bare property of the CURRENT row/scope only (never climbs); `$it` is the row itself
|
|
18
18
|
| { kind: "outer"; levels: number; name: string } // `^name` — read from exactly `levels` scopes out
|
|
19
19
|
| { kind: "binding"; index: number } // a ${…} interpolated host value
|
|
20
20
|
| { kind: "member"; recv: Expr; name: string } // .prop navigation on the value to its left
|
|
@@ -81,7 +81,7 @@ export interface Subquery {
|
|
|
81
81
|
follow: Follow | null;
|
|
82
82
|
/** `values` — scalar projection mode: the (single) projected expression is the
|
|
83
83
|
* row's result itself rather than being wrapped in a `{ name: value }` record,
|
|
84
|
-
* so `name values` yields `["Bob", …]` and `$
|
|
84
|
+
* so `name values` yields `["Bob", …]` and `$it values` yields the rows. */
|
|
85
85
|
values?: boolean;
|
|
86
86
|
/** `limit N` / `offset N` — bound the row set AFTER where/order/distinct and
|
|
87
87
|
* BEFORE the consumer reduces it, so `count { … limit 5 }` is at most 5 and
|
package/src/context.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { coerceCollection, regexMatches, BUILTIN_FUNCTIONS, BUILTIN_METHODS } from "./semantics.ts";
|
|
12
12
|
import type { RegexDialect } from "./regex.ts";
|
|
13
|
+
import type { RowIndex } from "./optimize/hash-index.ts";
|
|
13
14
|
|
|
14
15
|
// An array's only properties are its integer indices, spelled canonically
|
|
15
16
|
// (`"0"`, `"12"`; never `"01"`, `"length"`, or a method name).
|
|
@@ -36,6 +37,18 @@ export interface DataContext {
|
|
|
36
37
|
get(row: unknown, key: string): unknown;
|
|
37
38
|
/** Coerce a relation/source value into rows (may be lazy). */
|
|
38
39
|
toRows(value: unknown): Iterable<unknown>;
|
|
40
|
+
/** Optional: a value this context handed out as a stand-in for a collection
|
|
41
|
+
* it has not read yet (a lazy table handle), resolved to what it stands for.
|
|
42
|
+
* The engine calls it on every value it is about to observe AS A VALUE — an
|
|
43
|
+
* operand of `==`/`in`/arithmetic, a function or method argument, a projected
|
|
44
|
+
* item, an `order by` or `distinct` key, a `where` scalar, a lift — and never
|
|
45
|
+
* on a value it reads in ROW POSITION (the query source, a block receiver, a
|
|
46
|
+
* body-level `from`, a `follow` destination), which reaches `toRows` and
|
|
47
|
+
* `indexFor` as handed out, so a store-backed context can answer a probe on
|
|
48
|
+
* the handle without reading the table and still never lets the stand-in be
|
|
49
|
+
* seen by the language. Absent = identity; a context whose handle already
|
|
50
|
+
* behaves as the collection (a `Proxy` over an array) needs none. */
|
|
51
|
+
materialize?(value: unknown): unknown;
|
|
39
52
|
/** Identity of a row for `follow` cycle detection / dedup. */
|
|
40
53
|
identity(row: unknown): unknown;
|
|
41
54
|
/** Optional custom free function; return `{ handled: false }` to defer. */
|
|
@@ -50,6 +63,17 @@ export interface DataContext {
|
|
|
50
63
|
* `regexMatches(recv, args, dialect)` is the helper). `BUILTIN_METHODS.matches`
|
|
51
64
|
* is always the baseline. */
|
|
52
65
|
readonly regexDialect?: RegexDialect;
|
|
66
|
+
/** Optional: a pre-built equality index over `collection` (a value this
|
|
67
|
+
* context served as a root or relation) on the property path `path`
|
|
68
|
+
* (`["customer_id"]`, `["meta", "id"]`; `[]` keys by the row itself). The
|
|
69
|
+
* engine asks before building its own hash index for a correlated equality
|
|
70
|
+
* in a nested block (`where id == ^customer_id`); return `undefined` to let
|
|
71
|
+
* it build one. `lookup(value)` must return the ascending positions, into
|
|
72
|
+
* `toRows(collection)` in order, of the rows whose value at `path` equals
|
|
73
|
+
* `value` under OQX equality (SEMANTICS §5: absent ≡ null, `-0` ≡ `0`, `NaN`
|
|
74
|
+
* matches nothing, objects by reference). `IndexedCollection.context()`
|
|
75
|
+
* implements it over its indexes. */
|
|
76
|
+
indexFor?(collection: unknown, path: readonly string[]): RowIndex | undefined;
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
export interface DefaultContextOptions {
|