@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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,70 @@ All notable changes to `@omgbase/oqx` are recorded here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/) (pre-1.0: a minor bump may break).
|
|
6
6
|
|
|
7
|
+
## [0.14.1] - 2026-10-09
|
|
8
|
+
|
|
9
|
+
### Patch
|
|
10
|
+
- Relational patterns in nested blocks stop being quadratic. The in-memory engine
|
|
11
|
+
now runs every nested block through an optimizer (`optimize/`: a logical
|
|
12
|
+
`BlockPlan` per block, a `Rule` interface, a fixpoint driver) with four rules,
|
|
13
|
+
each proven unobservable — same rows, lifts and errors as the scan:
|
|
14
|
+
|
|
15
|
+
- **Correlated equality → hash probe.** `^customers first { where id ==
|
|
16
|
+
^customer_id }`, `^orders collect { … where customer_id == ^id }`,
|
|
17
|
+
`^customers exists { where id == ^customer_id }`, `${customers} first { … }`:
|
|
18
|
+
a top-level `&&` conjunct `local == outer` (either side) is answered from a
|
|
19
|
+
hash index on the receiver built once per run (per collection and path),
|
|
20
|
+
with the residual conjuncts evaluated over the bucket in receiver order.
|
|
21
|
+
Multiple equalities intersect smallest-first. The index reproduces §5
|
|
22
|
+
equality exactly (absent ≡ null, `-0` ≡ `0`, `NaN` matches nothing, objects
|
|
23
|
+
by reference). The sound rule: every conjunct left of the equality must be
|
|
24
|
+
raise-free (no call, no `single`, no lift, no bound, no out-of-range binding)
|
|
25
|
+
and the outer side must be raise-free and read nothing from the block's row;
|
|
26
|
+
otherwise the scan runs. A receiver is indexed when it is statically stable
|
|
27
|
+
(reads only the root/bindings) or when the same collection object is probed
|
|
28
|
+
twice; a receiver that yields a fresh value per enclosing row is scanned.
|
|
29
|
+
5k × 5k zip: ~9 ms (the scan: seconds).
|
|
30
|
+
- **Invariant block hoisting.** A block that reads nothing from any enclosing
|
|
31
|
+
row and lifts nothing is evaluated once per run and its value reused.
|
|
32
|
+
- **Semi/anti-join short-circuit** is kept over the probe's bucket (`exists`
|
|
33
|
+
stops at the first bucket row that passes).
|
|
34
|
+
- **Cardinality.** `exists`/`none`/`count` with no residual predicate are
|
|
35
|
+
answered from the bucket (or receiver) size without entering a row.
|
|
36
|
+
|
|
37
|
+
`DataContext.indexFor(collection, path)` (TypeScript) / `index_for` (Rust) is
|
|
38
|
+
a new optional seam for pre-built indexes; `IndexedCollection` keys under §5
|
|
39
|
+
equality (it kept `null` and `undefined` apart and matched `NaN` to itself)
|
|
40
|
+
and exposes `context()` (TypeScript) so a residual's nested blocks see the
|
|
41
|
+
root and reuse its indexes. `InMemoryEngine` takes `{ rules, trace }`
|
|
42
|
+
(TypeScript) / `with_rules` (Rust); `rules: []` is the naive scan, and the
|
|
43
|
+
conformance suites prove optimized ≡ naive — result or error, stage and
|
|
44
|
+
message — over every `spec/oqx` fixture. No language change.
|
|
45
|
+
|
|
46
|
+
A `RowIndex` may also implement `lookupRows(value)` (TypeScript) /
|
|
47
|
+
`lookup_rows` (Rust): the matching rows themselves, in receiver order. For a
|
|
48
|
+
statically stable receiver the engine asks the context's index **before**
|
|
49
|
+
reading the collection and, when it answers rows, never materializes the
|
|
50
|
+
receiver (one correlation is probed — the first whose outer side is a `^`
|
|
51
|
+
reference rather than a literal — and the other conjuncts, remaining equalities
|
|
52
|
+
included, stay residual in their original order; a `TraceEvent` of kind
|
|
53
|
+
`lookup` reports it). A statically stable receiver is now also read once per
|
|
54
|
+
run when nothing is correlated, instead of once per enclosing row. Rust:
|
|
55
|
+
`index_for` returns `Option<Rc<dyn RowIndex>>` and `lookup` returns
|
|
56
|
+
`Vec<usize>`, so a context can create indexes on demand.
|
|
57
|
+
|
|
58
|
+
`DataContext::materialize(value)` (Rust) / `materialize?(value)` (TypeScript)
|
|
59
|
+
is a new optional seam, default identity: a context that hands out a stand-in
|
|
60
|
+
for a collection it has not read yet (a lazy table handle) resolves it here.
|
|
61
|
+
The engine calls it on every value it is about to observe as a value — an
|
|
62
|
+
operand, an argument, a projected item, an `order by`/`distinct` key, a
|
|
63
|
+
`where` scalar, a lift — and never in row position (the source, a receiver, a
|
|
64
|
+
`from`, a `follow` destination), where the value reaches `to_rows`/`index_for`
|
|
65
|
+
as handed out. The Rust engine's execution path was folded to mirror the
|
|
66
|
+
TypeScript one: one `access` for every consumer (the `follow` seeds included),
|
|
67
|
+
`scan`/`materialize`, a `memoized` helper for invariant plans, `outer_value`,
|
|
68
|
+
and `lookup_order`/`residual_without` in `optimize`; optimized ≡ naive over
|
|
69
|
+
every fixture, same perf.
|
|
70
|
+
|
|
7
71
|
## [0.14.0] - 2026-10-06
|
|
8
72
|
|
|
9
73
|
### Minor
|
package/README.md
CHANGED
|
@@ -781,6 +781,117 @@ blocks/nodes, the relations table, `$` intrinsics, `WITH RECURSIVE` for `follow`
|
|
|
781
781
|
becomes a `QueryPlanner`, while oqx contributes the parser, IR, semantics
|
|
782
782
|
contract, and residual executor.
|
|
783
783
|
|
|
784
|
+
## Performance: relational patterns
|
|
785
|
+
|
|
786
|
+
A nested block runs once per enclosing row, so the natural way to join two
|
|
787
|
+
collections —
|
|
788
|
+
|
|
789
|
+
```js
|
|
790
|
+
oqx`select id, total, customer: ${customers} first { where id == ^customer_id } from ${orders}`;
|
|
791
|
+
oqx`select name, orders: ${orders} collect { id, total where customer_id == ^id } from ${customers}`;
|
|
792
|
+
oqx`select id from ${orders} where ${customers} exists { where id == ^customer_id }`;
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
— would be quadratic if the block scanned its receiver each time. It does not:
|
|
796
|
+
the in-memory engine runs every nested block through an **optimizer**
|
|
797
|
+
(`src/optimize/`), a small logical plan per block plus rules that rewrite it,
|
|
798
|
+
each with a proof that the rewrite is unobservable — same rows in the same
|
|
799
|
+
order, same lifts, same error (same message) at the same point. The planned
|
|
800
|
+
engine and the naive engine (`new InMemoryEngine(ctx, { rules: [] })`) are run
|
|
801
|
+
over every `spec/oqx` fixture by `test/conformance.test.ts` and must agree on
|
|
802
|
+
results and on errors.
|
|
803
|
+
|
|
804
|
+
**What is optimized**
|
|
805
|
+
|
|
806
|
+
- **Correlated equality → hash probe.** A top-level `&&` conjunct of the block's
|
|
807
|
+
`where` of the form `local == outer` (either side) — `local` a bare identifier
|
|
808
|
+
or member chain on the block's row (`customer_id`, `meta.id`, `$value`),
|
|
809
|
+
`outer` anything that reads nothing from that row (`^customer_id`, `^^x.id`,
|
|
810
|
+
a `${…}` binding, a literal, arithmetic over them) — is answered from a hash
|
|
811
|
+
index on the receiver built **once per run** per collection and path. The
|
|
812
|
+
remaining conjuncts run over the bucket only, in receiver order; several
|
|
813
|
+
equalities intersect their buckets smallest first. The index keys are exactly
|
|
814
|
+
`==` (SEMANTICS §5): absent ≡ `null`, `-0` ≡ `0`, `NaN` matches nothing,
|
|
815
|
+
objects and arrays by reference. 5 000 orders × 5 000 customers zip in a few
|
|
816
|
+
milliseconds; the scan takes seconds.
|
|
817
|
+
- **Invariant blocks run once.** A block that reads nothing from any enclosing
|
|
818
|
+
row (`^customers collect { name values }` from a top-level row) and lifts
|
|
819
|
+
nothing is evaluated the first time it is reached and its value reused for
|
|
820
|
+
every other row (the same array/record by reference).
|
|
821
|
+
- **Semi/anti-join short-circuit.** `exists` and `none` stop at the first bucket
|
|
822
|
+
row that passes the residual, as they stop at the first matching row of a scan.
|
|
823
|
+
- **Cardinality only.** `exists` / `none` / `count` with nothing left to check
|
|
824
|
+
per row (no `where`, or every conjunct correlated; no `distinct`, no `order
|
|
825
|
+
by`) are answered from the bucket's size after the bound without entering a
|
|
826
|
+
row.
|
|
827
|
+
|
|
828
|
+
**The sound rule.** `&&` is strictly left to right and short-circuiting (§13),
|
|
829
|
+
so a probe may skip a row only if everything the scan would have evaluated for
|
|
830
|
+
it could neither raise nor bind: an equality is hoisted only when **every
|
|
831
|
+
conjunct to its left is raise-free** — no function or method call, no `single`,
|
|
832
|
+
no lift, no `limit`/`offset`, no out-of-range `${…}`, only `exists`/`none`/
|
|
833
|
+
`count` blocks that are themselves raise-free — and its outer side is raise-free
|
|
834
|
+
too. A conjunct that could raise *right* of the equality is simply evaluated
|
|
835
|
+
over the bucket, where the scan would have evaluated it on the same rows. A
|
|
836
|
+
block whose `where` lifts anywhere is never probed (a lift could change what
|
|
837
|
+
the outer side reads between rows). Otherwise the block scans as before, so
|
|
838
|
+
`where nope(x) && id == ^id` still raises `unknown function`.
|
|
839
|
+
|
|
840
|
+
Property reads are treated as total (§23: evaluation is otherwise total). A
|
|
841
|
+
`DataContext.get` that throws is caught where the engine relies on reads it
|
|
842
|
+
would not otherwise perform — building an index, evaluating the probe value —
|
|
843
|
+
and the block falls back to the scan, which raises or not in its own order.
|
|
844
|
+
|
|
845
|
+
**Which receivers are indexed.** A receiver that reads only the root scope and
|
|
846
|
+
bindings (`^customers`, `${customers}`, `^^config.items`) is evaluated once per
|
|
847
|
+
run — its rows are read once and reused across the enclosing rows, with or
|
|
848
|
+
without a correlation — and indexed once per path even if the context hands out
|
|
849
|
+
a fresh array per read. Any other receiver is indexed the second time the
|
|
850
|
+
**same object** is probed, so a per-row relation (`lines collect { … where cid
|
|
851
|
+
== ^id }` with a different `lines` array per row) is scanned, not indexed. A
|
|
852
|
+
scalar receiver is one row.
|
|
853
|
+
|
|
854
|
+
**Pre-built indexes.** `DataContext.indexFor(collection, path)` is an optional
|
|
855
|
+
seam: return a `RowIndex` (`lookup(value) → ascending positions into
|
|
856
|
+
toRows(collection)`) for a collection you have already indexed, or `undefined`
|
|
857
|
+
to let the engine build one. A `RowIndex` may also implement
|
|
858
|
+
`lookupRows(value) → the matching rows, in receiver order`: for a stable
|
|
859
|
+
receiver the engine then probes it **before** reading the collection, so a
|
|
860
|
+
context backed by a store (a lazy handle for a table, an index on a column)
|
|
861
|
+
answers the probe with one indexed lookup and the table is never materialized.
|
|
862
|
+
One correlation is probed this way — the first whose outer side is a `^`
|
|
863
|
+
reference rather than a literal — and the other conjuncts, remaining
|
|
864
|
+
equalities included, are evaluated per selected row in their original order.
|
|
865
|
+
|
|
866
|
+
**Lazy handles.** A context that hands out a stand-in for a collection it has
|
|
867
|
+
not read yet may also implement `materialize(value)`: the engine calls it on
|
|
868
|
+
every value it is about to observe *as a value* (an operand of `==`/`in`/
|
|
869
|
+
arithmetic, a function or method argument, a projected item, an `order by` or
|
|
870
|
+
`distinct` key, a `where` scalar, a lift) and never on a value in *row
|
|
871
|
+
position* (the source, a block receiver, a body-level `from`, a `follow`
|
|
872
|
+
destination), which reaches `toRows` / `indexFor` as handed out. So the probe
|
|
873
|
+
above never reads the table, and the language never sees the stand-in. A
|
|
874
|
+
handle that already behaves as the collection (a `Proxy` over an array, as
|
|
875
|
+
omgbase's does) needs no `materialize`; the Rust crate's `DataContext::
|
|
876
|
+
materialize` is the same seam for a `Value` that cannot be lazy on its own.
|
|
877
|
+
`IndexedCollection` does the positional form for its fields:
|
|
878
|
+
|
|
879
|
+
```js
|
|
880
|
+
import { parse, run, IndexedCollection } from "@omgbase/oqx";
|
|
881
|
+
const orders = new IndexedCollection("orders", orderRows, ["customer_id"]);
|
|
882
|
+
run(parse("select name, o: ^orders collect { id values where customer_id == ^id } from customers"),
|
|
883
|
+
{ context: orders.context({ customers }) }); // probes the pre-built index
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
**Still a scan.** A block with a `from` re-projection or a `follow`; a `where`
|
|
887
|
+
whose equality sits right of a conjunct that could raise; inequalities,
|
|
888
|
+
`in`, `contains()` and other non-equality correlations; `order by … limit N`
|
|
889
|
+
(sorted in full, then bounded); `distinct` (hashes projections, not an index);
|
|
890
|
+
top-level queries (a `QueryPlanner` is the seam for those). `trace` in
|
|
891
|
+
`InMemoryEngine`'s options receives one event per index built, probe, direct
|
|
892
|
+
lookup, memo hit, cardinality answer or fallback, if you want to see what a
|
|
893
|
+
query did.
|
|
894
|
+
|
|
784
895
|
## Exports
|
|
785
896
|
|
|
786
897
|
Everything below is exported from `@omgbase/oqx` (`src/index.ts`); the SQLite
|
|
@@ -827,10 +938,18 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
827
938
|
**Engines and contexts**
|
|
828
939
|
|
|
829
940
|
- `Engine` — `{ run(query, bindings): OqxResult }`; what `run({ engine })` accepts.
|
|
830
|
-
- `InMemoryEngine` — `new InMemoryEngine(context?)`; the reference
|
|
831
|
-
`DataContext` (tier 1/2).
|
|
941
|
+
- `InMemoryEngine` — `new InMemoryEngine(context?, options?)`; the reference
|
|
942
|
+
engine over a `DataContext` (tier 1/2). `InMemoryEngineOptions`: `rules`
|
|
943
|
+
(the optimizer rules for nested blocks, default `DEFAULT_RULES`; `[]` is the
|
|
944
|
+
naive scan) and `trace` (a `(event: TraceEvent) => void` sink for index /
|
|
945
|
+
probe / memo / cardinality / fallback events). See [Performance](#performance-relational-patterns).
|
|
832
946
|
- `DataContext` — the tier-2 interface: `root`, `get`, `toRows`, `identity`,
|
|
833
|
-
plus optional `callFunction` / `callMethod` (above)
|
|
947
|
+
plus optional `callFunction` / `callMethod` (above), an optional
|
|
948
|
+
`indexFor(collection, path)` returning a `RowIndex` for a pre-built equality
|
|
949
|
+
index (`lookup` → positions; optionally `lookupRows` → rows, probed without
|
|
950
|
+
materializing the collection), an optional `materialize(value)` resolving a
|
|
951
|
+
lazy handle the engine is about to observe as a value (see
|
|
952
|
+
[Performance](#performance-relational-patterns)), and an optional
|
|
834
953
|
`regexDialect` (`"oqx"` default | `"native"`). The engine dispatches
|
|
835
954
|
`matches` through `callMethod`, so a custom context that wants the native
|
|
836
955
|
dialect answers `matches` itself with `semantics.regexMatches(recv, args,
|
|
@@ -873,17 +992,31 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
873
992
|
its native query using `constValue` for parameters, then returns
|
|
874
993
|
`{ rows, residual: residualQuery(query, residual) }`.
|
|
875
994
|
- `IndexedCollection` — `new IndexedCollection(rootName, rows, indexFields)`; a
|
|
876
|
-
`QueryPlanner` that hash-indexes `rows` on `indexFields`
|
|
877
|
-
`field == value` predicates on them
|
|
995
|
+
`QueryPlanner` that hash-indexes `rows` on `indexFields` (under `==`: absent ≡
|
|
996
|
+
null, `NaN` matches nothing) and answers `field == value` predicates on them
|
|
997
|
+
from the index, leaving the rest residual. `indexFor(collection, path)` serves
|
|
998
|
+
the same indexes to the engine's correlated probes and `context(extraRoots?)`
|
|
999
|
+
is a `DataContext` that resolves the root and wires `indexFor` up; the plans
|
|
1000
|
+
it returns carry that context, so a residual's nested blocks see the root.
|
|
878
1001
|
|
|
879
1002
|
```js
|
|
880
1003
|
import { parse, run, IndexedCollection, PlannedEngine } from "@omgbase/oqx";
|
|
881
1004
|
const planner = new IndexedCollection("people", people, ["city", "title"]);
|
|
882
|
-
const engine = new PlannedEngine(planner);
|
|
1005
|
+
const engine = new PlannedEngine(planner, planner.context());
|
|
883
1006
|
run(parse('name from people where city == "NYC" && age > 30'), { engine });
|
|
884
1007
|
// city probe from the index; `age > 30` finished in-memory over the candidates
|
|
885
1008
|
```
|
|
886
1009
|
|
|
1010
|
+
**Optimizer (nested blocks)**
|
|
1011
|
+
|
|
1012
|
+
- `DEFAULT_RULES`, `correlatedEqualityProbe`, `stableReceiver`, `invariantBlock`,
|
|
1013
|
+
`cardinalityOnly` — the shipped `Rule`s (`(plan: BlockPlan, ctx: RuleContext)
|
|
1014
|
+
=> BlockPlan | null`); `logicalBlock(op, depth)` is the unoptimized plan of a
|
|
1015
|
+
block and `optimizeBlock(op, depth, ctx, rules)` runs rules to a fixpoint.
|
|
1016
|
+
`BlockPlan` / `Correlation` / `RuleContext` are the types; `HashIndex` is the
|
|
1017
|
+
engine's `RowIndex` implementation (`add(value, position)`, `lookup(value)`),
|
|
1018
|
+
reusable by a context implementing `indexFor`.
|
|
1019
|
+
|
|
887
1020
|
- `SqliteTable` (from `@omgbase/oqx/sqlite`) —
|
|
888
1021
|
`new SqliteTable(db, tableName, options)` over a `node:sqlite` `DatabaseSync`.
|
|
889
1022
|
`SqliteTableOptions`:
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import type { Query } from "../ast.js";
|
|
2
|
+
import type { DataContext } from "../context.js";
|
|
2
3
|
import type { Plan, QueryPlanner } from "../planner.js";
|
|
4
|
+
import type { RowIndex } from "../optimize/hash-index.js";
|
|
3
5
|
export declare class IndexedCollection implements QueryPlanner {
|
|
4
6
|
private name;
|
|
5
7
|
private rows;
|
|
6
8
|
private indexes;
|
|
7
|
-
/** Index `rows` (exposed as root `name`) on each field in `indexFields`.
|
|
9
|
+
/** Index `rows` (exposed as root `name`) on each field in `indexFields`. A
|
|
10
|
+
* field is read as the engine reads a bare identifier (own property; array
|
|
11
|
+
* element for an integer-spelled name); a row without it keys as absent. */
|
|
8
12
|
constructor(name: string, rows: readonly unknown[], indexFields: readonly string[]);
|
|
13
|
+
/** The pre-built index for `collection` on `path` when `collection` is this
|
|
14
|
+
* collection's rows and `path` is one indexed field — the `DataContext.indexFor`
|
|
15
|
+
* seam, which `context()` wires up. */
|
|
16
|
+
indexFor(collection: unknown, path: readonly string[]): RowIndex | undefined;
|
|
17
|
+
/** A `DataContext` over plain objects that serves this collection as the
|
|
18
|
+
* root `name` (plus `extraRoots`) and exposes the indexes through
|
|
19
|
+
* `indexFor`, so the engine's correlated probes reuse them. */
|
|
20
|
+
context(extraRoots?: Record<string, unknown>): DataContext;
|
|
9
21
|
plan(query: Query, params: readonly unknown[]): Plan | null;
|
|
10
22
|
}
|
|
11
23
|
//# sourceMappingURL=indexed.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexed.d.ts","sourceRoot":"","sources":["../../src/adapters/indexed.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"indexed.d.ts","sourceRoot":"","sources":["../../src/adapters/indexed.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAG1D,qBAAa,iBAAkB,YAAW,YAAY;IACpD,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,OAAO,CAAgC;IAE/C;;gFAE4E;gBAChE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,OAAO,EAAE,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE;IAWlF;;2CAEuC;IACvC,QAAQ,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,GAAG,SAAS;IAK5E;;mEAE+D;IAC/D,OAAO,CAAC,UAAU,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,WAAW;IAI9D,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,GAAG,IAAI;CAoB5D"}
|
package/dist/adapters/indexed.js
CHANGED
|
@@ -1,33 +1,57 @@
|
|
|
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
|
-
|
|
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).
|
|
21
|
+
import { DefaultContext } from "../context.js";
|
|
22
|
+
import { partitionPushable, residualQuery, asEquality, constValue, ROWS_ROOT } from "../plan.js";
|
|
23
|
+
import { HashIndex, intersectPositions } from "../optimize/hash-index.js";
|
|
11
24
|
export class IndexedCollection {
|
|
12
25
|
name;
|
|
13
26
|
rows;
|
|
14
27
|
indexes = new Map();
|
|
15
|
-
/** Index `rows` (exposed as root `name`) on each field in `indexFields`.
|
|
28
|
+
/** Index `rows` (exposed as root `name`) on each field in `indexFields`. A
|
|
29
|
+
* field is read as the engine reads a bare identifier (own property; array
|
|
30
|
+
* element for an integer-spelled name); a row without it keys as absent. */
|
|
16
31
|
constructor(name, rows, indexFields) {
|
|
17
32
|
this.name = name;
|
|
18
33
|
this.rows = rows;
|
|
34
|
+
const read = new DefaultContext();
|
|
19
35
|
for (const field of indexFields) {
|
|
20
|
-
const idx = new
|
|
21
|
-
|
|
22
|
-
const key = row?.[field];
|
|
23
|
-
let bucket = idx.get(key);
|
|
24
|
-
if (!bucket)
|
|
25
|
-
idx.set(key, (bucket = []));
|
|
26
|
-
bucket.push(row);
|
|
27
|
-
}
|
|
36
|
+
const idx = new HashIndex();
|
|
37
|
+
rows.forEach((row, i) => idx.add(read.get(row, field), i));
|
|
28
38
|
this.indexes.set(field, idx);
|
|
29
39
|
}
|
|
30
40
|
}
|
|
41
|
+
/** The pre-built index for `collection` on `path` when `collection` is this
|
|
42
|
+
* collection's rows and `path` is one indexed field — the `DataContext.indexFor`
|
|
43
|
+
* seam, which `context()` wires up. */
|
|
44
|
+
indexFor(collection, path) {
|
|
45
|
+
if (collection !== this.rows || path.length !== 1)
|
|
46
|
+
return undefined;
|
|
47
|
+
return this.indexes.get(path[0]);
|
|
48
|
+
}
|
|
49
|
+
/** A `DataContext` over plain objects that serves this collection as the
|
|
50
|
+
* root `name` (plus `extraRoots`) and exposes the indexes through
|
|
51
|
+
* `indexFor`, so the engine's correlated probes reuse them. */
|
|
52
|
+
context(extraRoots = {}) {
|
|
53
|
+
return new IndexedContext(this, { ...extraRoots, [this.name]: this.rows });
|
|
54
|
+
}
|
|
31
55
|
plan(query, params) {
|
|
32
56
|
if (query.source.kind !== "ident" || query.source.name !== this.name)
|
|
33
57
|
return null;
|
|
@@ -40,18 +64,25 @@ export class IndexedCollection {
|
|
|
40
64
|
if (pushed.length === 0)
|
|
41
65
|
return null; // no index probe available — let the scan handle it
|
|
42
66
|
// Intersect the candidate sets from each indexed equality (smallest first).
|
|
43
|
-
|
|
44
|
-
for (const e of pushed) {
|
|
67
|
+
const buckets = pushed.map((e) => {
|
|
45
68
|
const eq = asEquality(e);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
return this.indexes.get(eq.field).lookup(constValue(eq.value, params));
|
|
70
|
+
}).sort((a, b) => a.length - b.length);
|
|
71
|
+
let candidate = buckets[0];
|
|
72
|
+
for (let i = 1; i < buckets.length && candidate.length > 0; i++)
|
|
73
|
+
candidate = intersectPositions(candidate, buckets[i]);
|
|
74
|
+
const rows = candidate.map((i) => this.rows[i]);
|
|
75
|
+
return { rows: () => rows, residual: residualQuery(query, residual), context: this.context({ [ROWS_ROOT]: rows }) };
|
|
51
76
|
}
|
|
52
77
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
class IndexedContext extends DefaultContext {
|
|
79
|
+
owner;
|
|
80
|
+
constructor(owner, roots) {
|
|
81
|
+
super(roots);
|
|
82
|
+
this.owner = owner;
|
|
83
|
+
}
|
|
84
|
+
indexFor(collection, path) {
|
|
85
|
+
return this.owner.indexFor(collection, path);
|
|
86
|
+
}
|
|
56
87
|
}
|
|
57
88
|
//# sourceMappingURL=indexed.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexed.js","sourceRoot":"","sources":["../../src/adapters/indexed.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,
|
|
1
|
+
{"version":3,"file":"indexed.js","sourceRoot":"","sources":["../../src/adapters/indexed.ts"],"names":[],"mappings":"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"}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RegexDialect } from "./regex.js";
|
|
2
|
+
import type { RowIndex } from "./optimize/hash-index.js";
|
|
2
3
|
/** Result of a context-provided function/method call: `handled: false` tells the
|
|
3
4
|
* engine to fall back to the builtin table (or error if none). */
|
|
4
5
|
export interface CallResult {
|
|
@@ -17,6 +18,18 @@ export interface DataContext {
|
|
|
17
18
|
get(row: unknown, key: string): unknown;
|
|
18
19
|
/** Coerce a relation/source value into rows (may be lazy). */
|
|
19
20
|
toRows(value: unknown): Iterable<unknown>;
|
|
21
|
+
/** Optional: a value this context handed out as a stand-in for a collection
|
|
22
|
+
* it has not read yet (a lazy table handle), resolved to what it stands for.
|
|
23
|
+
* The engine calls it on every value it is about to observe AS A VALUE — an
|
|
24
|
+
* operand of `==`/`in`/arithmetic, a function or method argument, a projected
|
|
25
|
+
* item, an `order by` or `distinct` key, a `where` scalar, a lift — and never
|
|
26
|
+
* on a value it reads in ROW POSITION (the query source, a block receiver, a
|
|
27
|
+
* body-level `from`, a `follow` destination), which reaches `toRows` and
|
|
28
|
+
* `indexFor` as handed out, so a store-backed context can answer a probe on
|
|
29
|
+
* the handle without reading the table and still never lets the stand-in be
|
|
30
|
+
* seen by the language. Absent = identity; a context whose handle already
|
|
31
|
+
* behaves as the collection (a `Proxy` over an array) needs none. */
|
|
32
|
+
materialize?(value: unknown): unknown;
|
|
20
33
|
/** Identity of a row for `follow` cycle detection / dedup. */
|
|
21
34
|
identity(row: unknown): unknown;
|
|
22
35
|
/** Optional custom free function; return `{ handled: false }` to defer. */
|
|
@@ -31,6 +44,17 @@ export interface DataContext {
|
|
|
31
44
|
* `regexMatches(recv, args, dialect)` is the helper). `BUILTIN_METHODS.matches`
|
|
32
45
|
* is always the baseline. */
|
|
33
46
|
readonly regexDialect?: RegexDialect;
|
|
47
|
+
/** Optional: a pre-built equality index over `collection` (a value this
|
|
48
|
+
* context served as a root or relation) on the property path `path`
|
|
49
|
+
* (`["customer_id"]`, `["meta", "id"]`; `[]` keys by the row itself). The
|
|
50
|
+
* engine asks before building its own hash index for a correlated equality
|
|
51
|
+
* in a nested block (`where id == ^customer_id`); return `undefined` to let
|
|
52
|
+
* it build one. `lookup(value)` must return the ascending positions, into
|
|
53
|
+
* `toRows(collection)` in order, of the rows whose value at `path` equals
|
|
54
|
+
* `value` under OQX equality (SEMANTICS §5: absent ≡ null, `-0` ≡ `0`, `NaN`
|
|
55
|
+
* matches nothing, objects by reference). `IndexedCollection.context()`
|
|
56
|
+
* implements it over its indexes. */
|
|
57
|
+
indexFor?(collection: unknown, path: readonly string[]): RowIndex | undefined;
|
|
34
58
|
}
|
|
35
59
|
export interface DefaultContextOptions {
|
|
36
60
|
/** See `DataContext.regexDialect`. Default `"oqx"`. */
|
package/dist/context.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAQzD;kEACkE;AAClE,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B;;;;;kFAK8E;IAC9E,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,8DAA8D;IAC9D,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC1C;;;;;;;;;;yEAUqE;IACrE,WAAW,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;IAChC,2EAA2E;IAC3E,YAAY,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC;IACzD,oEAAoE;IACpE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC;IACtE;;;;;;iCAM6B;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;IACrC;;;;;;;;;yCASqC;IACrC,QAAQ,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,GAAG,SAAS,CAAC;CAC/E;AAED,MAAM,WAAW,qBAAqB;IACpC,uDAAuD;IACvD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED;;;0EAG0E;AAC1E,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,KAAK,CAA0B;IACvC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;gBAExB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EAAE,OAAO,GAAE,qBAA0B;IAKpF,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI3B,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAMvC,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IAIzC,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO;IAK/B,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU;IAKvD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU;CAKrE"}
|
package/dist/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,iFAAiF;AACjF,gFAAgF;AAChF,4EAA4E;AAC5E,gFAAgF;AAChF,gFAAgF;AAChF,EAAE;AACF,gFAAgF;AAChF,2EAA2E;AAE3E,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Query } from "./ast.js";
|
|
1
|
+
import type { Query, OpNode } from "./ast.js";
|
|
2
2
|
import type { DataContext } from "./context.js";
|
|
3
|
+
import type { Rule } from "./optimize/index.js";
|
|
3
4
|
/** The shaped result of a top-level query, discriminated by consumer. */
|
|
4
5
|
export type OqxResult = {
|
|
5
6
|
consumer: "collect";
|
|
@@ -24,9 +25,47 @@ export type OqxResult = {
|
|
|
24
25
|
export interface Engine {
|
|
25
26
|
run(query: Query, bindings: readonly unknown[]): OqxResult;
|
|
26
27
|
}
|
|
28
|
+
/** What the optimizer did at run time (for tests and diagnostics): an index
|
|
29
|
+
* built (or taken from the context) for a collection and path, a probe that
|
|
30
|
+
* selected a bucket, a context index that answered a probe with rows directly
|
|
31
|
+
* (`lookupRows`: the collection was never materialized), a memoized invariant
|
|
32
|
+
* block reused, a consumer answered from a cardinality, or a probe that fell
|
|
33
|
+
* back to the scan. */
|
|
34
|
+
export type TraceEvent = {
|
|
35
|
+
kind: "index";
|
|
36
|
+
path: readonly string[];
|
|
37
|
+
rows: number;
|
|
38
|
+
source: "engine" | "context";
|
|
39
|
+
} | {
|
|
40
|
+
kind: "probe";
|
|
41
|
+
paths: readonly (readonly string[])[];
|
|
42
|
+
candidates: number;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "lookup";
|
|
45
|
+
path: readonly string[];
|
|
46
|
+
candidates: number;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "memo";
|
|
49
|
+
op: OpNode;
|
|
50
|
+
} | {
|
|
51
|
+
kind: "cardinality";
|
|
52
|
+
count: number;
|
|
53
|
+
} | {
|
|
54
|
+
kind: "fallback";
|
|
55
|
+
reason: "unstable" | "index" | "probe-value" | "not-a-collection";
|
|
56
|
+
};
|
|
57
|
+
export interface InMemoryEngineOptions {
|
|
58
|
+
/** The optimizer rules to apply to nested blocks (default `DEFAULT_RULES`);
|
|
59
|
+
* `[]` runs the naive scan everywhere. */
|
|
60
|
+
rules?: readonly Rule[];
|
|
61
|
+
/** Receives a `TraceEvent` for each optimizer action during `run`. */
|
|
62
|
+
trace?: (event: TraceEvent) => void;
|
|
63
|
+
}
|
|
27
64
|
export declare class InMemoryEngine implements Engine {
|
|
28
65
|
private ctx;
|
|
29
|
-
|
|
66
|
+
private rules;
|
|
67
|
+
private trace;
|
|
68
|
+
constructor(context?: DataContext, options?: InMemoryEngineOptions);
|
|
30
69
|
run(query: Query, bindings?: readonly unknown[]): OqxResult;
|
|
31
70
|
private boundOf;
|
|
32
71
|
private matches;
|
|
@@ -41,13 +80,27 @@ export declare class InMemoryEngine implements Engine {
|
|
|
41
80
|
private itemValue;
|
|
42
81
|
private evalWhere;
|
|
43
82
|
private evalWhereOp;
|
|
83
|
+
private evalWhereOpPlanned;
|
|
44
84
|
private opRows;
|
|
45
85
|
private anyMatch;
|
|
46
86
|
private iterMatchRows;
|
|
47
87
|
private matchRows;
|
|
88
|
+
private planOf;
|
|
89
|
+
private memoized;
|
|
90
|
+
private access;
|
|
91
|
+
private scan;
|
|
92
|
+
private reproject;
|
|
93
|
+
private materialize;
|
|
94
|
+
private lookupRows;
|
|
95
|
+
private probe;
|
|
96
|
+
private outerValue;
|
|
97
|
+
private contextIndex;
|
|
98
|
+
private indexFor;
|
|
48
99
|
private evalCollectValue;
|
|
100
|
+
private evalCollectValuePlanned;
|
|
49
101
|
private sortScopes;
|
|
50
102
|
private evalExpr;
|
|
103
|
+
private evalRaw;
|
|
51
104
|
private resolveIn;
|
|
52
105
|
private evalCall;
|
|
53
106
|
}
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|