@omgbase/oqx 0.10.0 → 0.10.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 (4) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/LICENSE +21 -0
  3. package/README.md +185 -4
  4. package/package.json +27 -6
package/CHANGELOG.md ADDED
@@ -0,0 +1,214 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@omgbase/oqx` are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
+ [Semantic Versioning](https://semver.org/) (pre-1.0: a minor bump may break).
6
+
7
+ ## [0.10.1] - 2026-09-23
8
+
9
+ Metadata and documentation only; no code changes.
10
+
11
+ ### Added
12
+ - `LICENSE` file (MIT) — `package.json` previously said ISC with no license text.
13
+ - Package metadata: `author`, `repository`, `homepage`, `bugs`, `keywords`,
14
+ `sideEffects: false`, and a single consistent `description`.
15
+ - This changelog, shipped in the package.
16
+ - GitHub Actions CI (typecheck, test, build on Node 22 and 24).
17
+ - README: an "Exports" reference for every public export, the
18
+ `DataContext.callFunction` / `callMethod` extension seam (and the
19
+ "unknown function" pitfall of a context that does not delegate to the
20
+ builtins), `OqxError` and its `stage` discriminator, an `IndexedCollection`
21
+ example, `SqliteTableOptions` fields, string-literal escapes, `has(x)` and
22
+ array `contains`, and a License section.
23
+
24
+ ### Changed
25
+ - `engines.node` is now `>=22.13.0` — the floor at which `node:sqlite` (used
26
+ only by the `./sqlite` subpath) is available without a flag. The main entry is
27
+ plain ES2022 ESM and runs on older Node in practice.
28
+ - README "Requirements" states the real floors (see above) and that the package
29
+ is ESM-only.
30
+
31
+ ### Fixed
32
+ - README referred to `oqx.semantics`; the scalar contract is the named export
33
+ `semantics`.
34
+
35
+ ## [0.10.0] - 2026-09-22
36
+
37
+ ### Added
38
+ - `entries(x)` builtin: the explicit bridge from a record to a collection of
39
+ `{ key, value }` entries. Objects yield own enumerable pairs in insertion
40
+ order, arrays yield `(index, element)`, `Map`s yield their entries; absence,
41
+ scalars, and ranges yield nothing. A plain object is still **not** iterable —
42
+ `from ${obj}` is one row.
43
+ - `$key` intrinsic: inside an entry scope, the entry's key. The scope's row is
44
+ the property's *value*, so `$value` and bare names read the value.
45
+ `$key` exists only in entry scopes — an array element has no implicit index.
46
+ - A free-function call may be a consumer receiver
47
+ (`entries(prefs) exists { … }`) and may be navigated further.
48
+ - `semantics`: `makeEntry`, `isEntry`, `entriesOf` (the entry tag is a
49
+ non-enumerable symbol, so `entries(x)` as a plain value reads as an array of
50
+ `{ key, value }` records).
51
+
52
+ ### Changed
53
+ - Engine: one `enter(row, parent, extra)` now creates every scope (top level,
54
+ nested blocks, `from` re-projections, follow seeds/occurrences, fast paths).
55
+
56
+ ## [0.9.0] - 2026-09-21
57
+
58
+ ### Added
59
+ - `none { … }` consumer — true iff the block yields zero rows; the explicit
60
+ complement of `exists`, and how universal quantification is spelled
61
+ (`members none { where !active }`). Works in `where` position and as a
62
+ whole-query directive (`{ consumer: "none", none }`). Not comparable and not a
63
+ projection.
64
+ - `limit N` / `offset N` — bound the row set **after** `where` / `order by` /
65
+ `distinct` and **before** the consumer reduces it, at the top level and in any
66
+ block. `N` is a literal, a `${…}` binding, or an outer reference (`limit ^n`);
67
+ it must be a non-negative integer (eval error otherwise). `limit`/`offset` are
68
+ clause words only when followed by a number, binding, or `^`, so a field with
69
+ that name still reads as a field.
70
+
71
+ ### Changed
72
+ - Engine: `exists` / `none` / `count` fast paths stop at the (offset+1)th match;
73
+ `first` / `single` caps grow by the offset; consumer ops share one `opRows`
74
+ pipeline (matched → ordered → distinct → bounded), which also makes
75
+ `collect distinct { ^x: … }` lifts dedup as documented.
76
+ - SQLite adapter: no SQL `LIMIT` under `first` / `single` when the query carries
77
+ its own `limit` / `offset` (the residual applies them).
78
+
79
+ ## [0.8.0] - 2026-09-21
80
+
81
+ ### Added
82
+ - `$value` intrinsic — the current scope's own row, whatever its type. Absent at
83
+ the root scope. Inside a nested block `$value` is the inner item and `^$value`
84
+ the enclosing row. Never resolved through the `DataContext`.
85
+ - `values` projection mode — after a projection of exactly one item, the row's
86
+ result is that value rather than a `{ name: value }` record. Works at the top
87
+ level and inside any `collect` / `first` / `single` block; composes with
88
+ `distinct` (dedup by the bare value). Cannot be combined with a lift.
89
+ - An unaliased projection item may be any value expression: a bare or dotted
90
+ navigation keys by its last segment; a call / arithmetic / comparison is legal
91
+ only under `values`, otherwise a clear "needs an alias" parse error.
92
+
93
+ ### Fixed
94
+ - `{ has(x) }` used to misparse as select `has` + where `(x)`; it is now the
95
+ alias error above (write `where has(x)`).
96
+
97
+ ## [0.7.0] - 2026-09-19
98
+
99
+ ### Changed
100
+ - **BREAKING:** bare names resolve **locally only**. A bare identifier reads the
101
+ current row/scope; an absent local name stays absent and never falls through
102
+ to an enclosing scope. Outer correlation is always the explicit `^name`
103
+ (exactly one scope out per caret; past the root is absent).
104
+ - **BREAKING:** `DataContext.has` removed — it existed only to stop the scope
105
+ climb; a computed relation now needs only `get`.
106
+ - A consumer receiver may start with `^` (`^people collect { … }`), since a
107
+ named root is reachable from inside a row only as `^root`.
108
+ - Planner helpers / SQLite adapter: a bare identifier in a top-level `where` is
109
+ now unambiguously a column of the scanned rows and is safe to push.
110
+
111
+ ## [0.6.0] - 2026-09-17
112
+
113
+ ### Added
114
+ - `range(s)` builtin: parse a string (`"1..5"`, `"1...5"`, `"..5"`,
115
+ `"2026-01-01..2026-01-31"`, …) into the range value a `lo..hi` literal
116
+ produces; a range passes through unchanged; anything else yields an absent
117
+ range, so `x in range(bad)` is simply false. Bounds must share a scalar domain
118
+ (numeric or ISO-8601).
119
+ - `semantics.parseRangeString`.
120
+
121
+ ## [0.5.0] - 2026-09-16
122
+
123
+ ### Added
124
+ - Ruby-style range literals: `lo..hi` (inclusive), `lo...hi` (exclusive high
125
+ end), and open-ended `..hi` / `lo..`. Used mainly as the right side of `in`
126
+ for interval membership; a range is a general value expression. Bounds
127
+ compare via the ordering rules, so ranges work over numbers and ISO-8601
128
+ date/time strings; an absent value is never covered.
129
+ - `semantics`: `makeRange`, `isRange`, `rangeCovers`; a `membership` branch for
130
+ ranges.
131
+
132
+ ### Fixed
133
+ - Lexer: a decimal point now binds only when a digit follows, so `1..5` no
134
+ longer mis-lexes as `1.` + `.5`.
135
+
136
+ ## [0.4.0] - 2026-09-16
137
+
138
+ ### Added
139
+ - `distinct` modifier on consumers (top-level and nested): dedup the rows a
140
+ consumer reduces by their **projected** value, keeping the first per distinct
141
+ projection in order; an empty projection dedups by row identity. Spelled
142
+ `<op> distinct { … }` or `select distinct …` inside the block.
143
+
144
+ ### Fixed
145
+ - Parser: `looksLikePredicate` skips nested `{ … }` blocks when scanning for a
146
+ top-level operator and recognizes a consumer directive
147
+ (`… exists { }`, `… count distinct { } == N`) as a where-position predicate.
148
+
149
+ ## [0.3.0] - 2026-09-16
150
+
151
+ ### Changed
152
+ - `follow` is now a **per-path** walk: a node reached by N distinct paths yields
153
+ N occurrences (previously collapsed by global identity dedup).
154
+ - Cycles are admitted **once** as `$stop == "cycle"` and never re-expanded
155
+ (previously a revisit produced no occurrence).
156
+ - `$stop` categories are `interior | leaf | frontier | depth | cycle`
157
+ (precedence cycle > frontier > depth > leaf > interior); only `interior` rows
158
+ expand. The old `"continue"` is renamed `"interior"`.
159
+ - `$frontier` covers both frontier and depth cutoffs; `$leaf` stays "no
160
+ successors".
161
+
162
+ ### Added
163
+ - `$ordinal` intrinsic: a deterministic 1..N rank over `(depth, path)`.
164
+
165
+ ### Fixed
166
+ - `follow distinct` now works: collapse occurrences to the minimal
167
+ `(depth, path)` per identity (`by` key, else `ctx.identity`).
168
+
169
+ ## [0.2.0] - 2026-09-16
170
+
171
+ ### Changed
172
+ - Ship a compiled `dist/` (JS + `.d.ts` + source maps) instead of raw `.ts`.
173
+ 0.1.0 pointed `main` / `types` / `exports` at `src/*.ts`, which `tsc` rejects
174
+ under `module: NodeNext` and Node refuses to type-strip under `node_modules`.
175
+ - Added `tsconfig.build.json` (`rewriteRelativeImportExtensions`) and
176
+ `scripts/fix-dts-extensions.mjs` to rewrite `.ts` specifiers left in emitted
177
+ declarations; `build`, `clean`, and `prepublishOnly` scripts.
178
+
179
+ ### Fixed
180
+ - `order by <expr> desc` hoisted rows missing the sort key to the top. Absent
181
+ now sorts last regardless of direction.
182
+
183
+ ## [0.1.0] - 2026-09-14
184
+
185
+ First public release as `@omgbase/oqx`.
186
+
187
+ ### Added
188
+ - The OQX language kernel: lexer, parser, AST; the `oqx` tagged template with
189
+ typed value bindings (prepared-statement semantics); `parse`, `execute`, `run`.
190
+ - Projection (optional `select`, aliases, dotted navigation), `where` predicates,
191
+ `order by`, consumers `collect` / `exists` / `count` / `first` / `single`,
192
+ nested consumer directives over relations, lifts (`^name:`) including
193
+ multi-level `^^` / `^^^` with flatten-append accumulation, `^name` outer
194
+ references, and bounded recursion with `follow` and its intrinsics.
195
+ - `semantics.ts` — the normative scalar contract every backend must obey
196
+ (typed/strict equality, absent-operand comparisons false, CEL-style `in`,
197
+ absent-last sort), backed by a conformance suite.
198
+ - `DataContext` / `DefaultContext` (tier 2), `Engine` / `InMemoryEngine`,
199
+ `QueryPlanner` / `Plan` / `PlannedEngine` (tier 3) with the pushdown helpers
200
+ in `plan.ts`, and two adapters: `IndexedCollection` (hash-index probe) and
201
+ `@omgbase/oqx/sqlite` (`SqliteTable`, real OQX→SQL pushdown over
202
+ `node:sqlite`).
203
+
204
+ [0.10.1]: https://github.com/omgbase/oqx/compare/v0.10.0...HEAD
205
+ [0.10.0]: https://github.com/omgbase/oqx/compare/v0.9.0...v0.10.0
206
+ [0.9.0]: https://github.com/omgbase/oqx/compare/v0.8.0...v0.9.0
207
+ [0.8.0]: https://github.com/omgbase/oqx/compare/v0.7.0...v0.8.0
208
+ [0.7.0]: https://github.com/omgbase/oqx/compare/v0.6.0...v0.7.0
209
+ [0.6.0]: https://github.com/omgbase/oqx/compare/v0.5.0...v0.6.0
210
+ [0.5.0]: https://github.com/omgbase/oqx/compare/v0.4.0...v0.5.0
211
+ [0.4.0]: https://github.com/omgbase/oqx/commits/v0.4.0
212
+ [0.3.0]: https://github.com/omgbase/oqx/commit/aca739b
213
+ [0.2.0]: https://github.com/omgbase/oqx/commit/82fb99e
214
+ [0.1.0]: https://github.com/omgbase/oqx/commit/4552bb5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brendan Baldwin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -187,6 +187,14 @@ oqx`name from ${people} where !active`; // → Carol
187
187
  Equality is **typed and strict** (`5 == "5"` is false); a comparison against an
188
188
  absent (`null`/`undefined`) field is simply false rather than an error.
189
189
 
190
+ **String literals** may be double- or single-quoted (`"NYC"` / `'NYC'`) and
191
+ support backslash escapes: `\n`, `\t`, `\r`, `\0`, and `\<any other char>`
192
+ for that character itself (`\"`, `\'`, `\\`). An unterminated string is a lex
193
+ error. Note that inside the `oqx` tagged template JavaScript resolves its own
194
+ escapes first (the template's cooked strings are what OQX lexes), so `\\n` in
195
+ your source reaches OQX as `\n`. Usually you don't need any of this —
196
+ interpolate the host value instead (`where name == ${name}`).
197
+
190
198
  **Ranges.** A Ruby-style range `lo..hi` (inclusive) or `lo...hi` (exclusive high
191
199
  end) is a value, used most often as the right side of `in`. Either bound may be
192
200
  omitted for an open-ended range (`..hi`, `lo..`). Bounds compare with the same
@@ -285,8 +293,23 @@ Methods on a value: `contains`, `startsWith`, `endsWith`, `matches` (regex),
285
293
  ```js
286
294
  oqx`name from ${people} where title.startsWith("Eng")`; // → Bob
287
295
  oqx`name from ${people} where title.lower() == "director"`; // → Alice
296
+ oqx`name from ${people} where has(age) && !has(nickname)`; // present vs absent
297
+ oqx`name from ${people} where tags.contains("admin")`; // array membership
288
298
  ```
289
299
 
300
+ - `has(x)` is true when `x` is **present** — anything other than `null` /
301
+ `undefined`. `has(0)`, `has("")`, and `has(false)` are all true; use bare
302
+ truthiness (`where active`) when you mean truthy.
303
+ - `.contains(v)` works on strings (substring) **and arrays** (an element equal
304
+ to `v` under OQX's strict equality); on anything else it is false.
305
+ - `.size()` / `size(x)` is the length of a string or array, the key count of
306
+ an object, and 0 for absent; `.matches(re)` compiles its argument as a
307
+ JavaScript `RegExp`.
308
+
309
+ Anything not in these tables is an **eval error** (`unknown function 'f(…)'` /
310
+ `unknown method '.m(…)'`) — see [custom functions](#custom-functions-and-methods)
311
+ for adding your own.
312
+
290
313
  ### 5. Consumers
291
314
 
292
315
  A consumer shapes a result set. There are six:
@@ -528,7 +551,7 @@ Query AST ─┬─ InMemoryEngine(DataContext) tier 1/2 — drive any data
528
551
  └─ finishes the residual on the in-memory engine
529
552
  ```
530
553
 
531
- **Everything obeys one scalar-semantics contract** (`oqx.semantics`): typed/strict
554
+ **Everything obeys one scalar-semantics contract** (the `semantics` export): typed/strict
532
555
  equality (`5 == "5"` is false), absent operands make ordering comparisons false,
533
556
  CEL-style `in`, absent-last sort order. Any backend that can't reproduce a rule
534
557
  in its native language must leave that fragment as an in-memory *residual* rather
@@ -556,6 +579,41 @@ const graph = {
556
579
  run(parse("id, depth: $depth from tree follow children"), { context: graph });
557
580
  ```
558
581
 
582
+ #### Custom functions and methods
583
+
584
+ `DataContext` has two optional hooks, `callFunction(name, args)` for free
585
+ functions (`f(x)`) and `callMethod(name, recv, args)` for methods (`x.m()`).
586
+ Each returns a `CallResult`: `{ handled: true, value }` to answer, or
587
+ `{ handled: false }` to decline. **The engine does not consult the builtin
588
+ table itself** — `DefaultContext` is what does that. So a context that omits
589
+ these hooks, or handles only its own names without deferring, loses
590
+ `entries()`, `size()`, `has()`, `range()`, `.contains()`, and the rest: the
591
+ engine throws `OqxError("unknown function 'size(…)'", "eval")`. (The `graph`
592
+ context above has exactly this limitation.) Delegate whatever you don't
593
+ recognize:
594
+
595
+ ```js
596
+ import { DefaultContext, parse, run } from "@omgbase/oqx";
597
+
598
+ const builtins = new DefaultContext(); // or: semantics.BUILTIN_FUNCTIONS[name]
599
+ const ctx = {
600
+ ...graph,
601
+ callFunction(name, args) {
602
+ if (name === "age") return { handled: true, value: yearsSince(args[0]) };
603
+ return builtins.callFunction(name, args); // entries/size/has/range keep working
604
+ },
605
+ callMethod(name, recv, args) {
606
+ if (name === "slug") return { handled: true, value: slugify(recv) };
607
+ return builtins.callMethod(name, recv, args);
608
+ },
609
+ };
610
+ run(parse("id from tree where age(born) > 18 && title.slug() == 'x'"), { context: ctx });
611
+ ```
612
+
613
+ The simplest route is to `extends DefaultContext` and `super.callFunction(...)`
614
+ in the fallthrough. Custom calls are never pushed down by the shipped planners;
615
+ they always run in the residual.
616
+
559
617
  ### Tier 3 — a `QueryPlanner` (pushdown + planning)
560
618
 
561
619
  A planner translates as much of a query as it can into its store's native query
@@ -584,11 +642,130 @@ blocks/nodes, the relations table, `$` intrinsics, `WITH RECURSIVE` for `follow`
584
642
  becomes a `QueryPlanner`, while oqx contributes the parser, IR, semantics
585
643
  contract, and residual executor.
586
644
 
645
+ ## Exports
646
+
647
+ Everything below is exported from `@omgbase/oqx` (`src/index.ts`); the SQLite
648
+ adapter lives on the `@omgbase/oqx/sqlite` subpath.
649
+
650
+ **Running queries**
651
+
652
+ - `oqx` (default export **and** named) — the tagged template. Returns the
653
+ consumer-shaped value: an array for `collect`, a boolean for `exists` / `none`,
654
+ a number for `count`, a record or `null` for `first` / `single`.
655
+ - `parse(source)` — a query string → reusable `Query` AST (an `OqxError` with
656
+ `stage: "lex" | "parse"` on bad input).
657
+ - `execute(source, roots?)` — parse and run a string against named roots
658
+ (`{ people }`), returning the consumer-shaped value.
659
+ - `run(query, opts?)` — run a parsed `Query`, returning the full `OqxResult`.
660
+ `opts.values` are the positional `${…}` bindings; then exactly one of
661
+ `opts.engine` (any `Engine`, e.g. a `PlannedEngine`), `opts.context` (a
662
+ `DataContext`, run on the in-memory engine), or `opts.roots` (plain-object
663
+ named roots → `DefaultContext`). Default: an empty `DefaultContext`.
664
+ - `runQuery(query, values, roots)` — the lower-level call `oqx` / `execute` use:
665
+ in-memory over a `DefaultContext(roots)` (`roots` may be `undefined`).
666
+ - `OqxResult` — the discriminated result `run` returns:
667
+
668
+ ```ts
669
+ type OqxResult =
670
+ | { consumer: "collect"; rows: unknown[] }
671
+ | { consumer: "exists"; exists: boolean }
672
+ | { consumer: "none"; none: boolean }
673
+ | { consumer: "count"; count: number }
674
+ | { consumer: "first"; row: unknown | null }
675
+ | { consumer: "single"; row: unknown | null };
676
+ ```
677
+
678
+ **Errors**
679
+
680
+ - `OqxError` — the one error class for every failure. `error.stage` is
681
+ `"lex" | "parse" | "eval"` so you can branch without matching messages;
682
+ `error.message` carries the detail (position for lex/parse errors, the
683
+ offending name or count for eval errors). Eval errors include: unknown
684
+ function/method, a non-integer or negative `limit` / `offset`, and
685
+ `single { … }` matching **more than one** row (zero rows is `null`, not an
686
+ error — use `first` when zero-or-one is expected and you don't care to assert).
687
+
688
+ **Engines and contexts**
689
+
690
+ - `Engine` — `{ run(query, bindings): OqxResult }`; what `run({ engine })` accepts.
691
+ - `InMemoryEngine` — `new InMemoryEngine(context?)`; the reference engine over a
692
+ `DataContext` (tier 1/2).
693
+ - `DataContext` — the tier-2 interface: `root`, `get`, `toRows`, `identity`,
694
+ plus optional `callFunction` / `callMethod` (above).
695
+ - `CallResult` — `{ handled: boolean; value?: unknown }`, returned by those hooks.
696
+ - `DefaultContext` — `new DefaultContext(roots?)`; plain-object access, `.id`
697
+ identity, and the builtin function/method tables.
698
+ - `semantics` — the scalar-contract module (`equals`, `relate`, `arith`,
699
+ `membership`, `truthy`, `compareForSort`, `sizeOf`, `toList`,
700
+ `coerceCollection`, ranges: `makeRange` / `isRange` / `rangeCovers` /
701
+ `parseRangeString`, entries: `makeEntry` / `isEntry` / `entriesOf`, and the
702
+ `BUILTIN_FUNCTIONS` / `BUILTIN_METHODS` tables). A backend reproducing a rule
703
+ natively must match these.
704
+
705
+ **Planning (tier 3)**
706
+
707
+ - `QueryPlanner` — `{ plan(query, params): Plan | null }`. Return `null` to
708
+ decline a query entirely (full in-memory fallback).
709
+ - `Plan` — `{ rows(): Iterable; residual: Query; context?: DataContext }`: the
710
+ rows the store produced, the query to finish over them, and optionally a
711
+ context for navigating those rows' relations.
712
+ - `PlannedEngine` — `new PlannedEngine(planner, fallbackContext?)`; an `Engine`
713
+ that runs the planner and finishes `plan.residual` on the in-memory engine
714
+ over `plan.rows()` (exposed as the `ROWS_ROOT` root).
715
+ - Planner helpers, used together inside `plan()`: `partitionPushable(where,
716
+ canPush)` splits the top-level `where` conjunction into `pushed` expressions
717
+ (those your `canPush` accepts) and a `residual` where-tree; `asEquality(expr)`
718
+ recognizes `field == const` (either order) as `{ field, value }`;
719
+ `isConst(expr)` / `constValue(expr, params)` tell a literal-or-binding from a
720
+ row-dependent expression and evaluate it against the query bindings; and
721
+ `residualQuery(query, residualWhere)` rebuilds the query to scan `ROWS_ROOT`
722
+ with the pushed predicates dropped and projection / order / consumer / bounds
723
+ intact. A planner typically: checks `query.source` names its table (and
724
+ declines `from` / `follow`), partitions the `where`, translates `pushed` into
725
+ its native query using `constValue` for parameters, then returns
726
+ `{ rows, residual: residualQuery(query, residual) }`.
727
+ - `IndexedCollection` — `new IndexedCollection(rootName, rows, indexFields)`; a
728
+ `QueryPlanner` that hash-indexes `rows` on `indexFields` and answers
729
+ `field == value` predicates on them from the index, leaving the rest residual:
730
+
731
+ ```js
732
+ import { parse, run, IndexedCollection, PlannedEngine } from "@omgbase/oqx";
733
+ const planner = new IndexedCollection("people", people, ["city", "title"]);
734
+ const engine = new PlannedEngine(planner);
735
+ run(parse('name from people where city == "NYC" && age > 30'), { engine });
736
+ // city probe from the index; `age > 30` finished in-memory over the candidates
737
+ ```
738
+
739
+ - `SqliteTable` (from `@omgbase/oqx/sqlite`) —
740
+ `new SqliteTable(db, tableName, options)` over a `node:sqlite` `DatabaseSync`.
741
+ `SqliteTableOptions`:
742
+ - `columns: string[]` — the columns that map to bare OQX fields; only these
743
+ are pushable (any other identifier stays residual).
744
+ - `jsonColumns?: string[]` — columns whose stored text is `JSON.parse`d back
745
+ into the row (for nested relations kept as JSON).
746
+ - `map?: (raw) => row` — a custom raw-SQL-row → query-row mapper (overrides
747
+ `jsonColumns`).
748
+
749
+ **Types**
750
+
751
+ - Every AST node type from `src/ast.ts` is re-exported (`Query`, `Subquery`,
752
+ `Where`, `Expr`, `OpNode`, `SelectItem`, `OrderSpec`, `Follow`, …) for
753
+ planners that walk the IR.
754
+
587
755
  ## Requirements
588
756
 
589
- Node 22.6+ (the sources are TypeScript, run natively via type-stripping — the
590
- package has **no runtime dependencies**). `npm test` runs the suite; `npm run
591
- typecheck` typechecks.
757
+ No runtime dependencies. The package is **ESM-only** (there is no `require`
758
+ condition in `exports`; Node 22.12+ can `require()` an ES module natively).
759
+
760
+ - **Main entry** (`@omgbase/oqx`): compiled ES2022 ESM. `engines.node` says
761
+ `>=22.13.0`, but nothing in the main entry needs it — Node 18+ works in
762
+ practice.
763
+ - **`@omgbase/oqx/sqlite`**: imports `node:sqlite`, which is available without
764
+ a flag from Node 22.13 (behind `--experimental-sqlite` in 22.5–22.12). This is
765
+ the reason for the `engines` floor.
766
+ - **Developing the repo**: `npm test` runs the `.ts` suite directly through
767
+ Node's type stripping, unflagged from Node 22.18 (and all of 24). `npm run
768
+ typecheck` typechecks; `npm run build` emits `dist/`.
592
769
 
593
770
  ## Relationship to omgbase
594
771
 
@@ -596,3 +773,7 @@ This is tier 1 (the in-memory object/collection interpreter) of the OQX
596
773
  implementation tiers. The language kernel here is host-agnostic; richer hosts
597
774
  (e.g. omgbase's docs/blocks/nodes with index pushdown) layer data-model
598
775
  vocabulary and execution capabilities on top of the same surface syntax.
776
+
777
+ ## License
778
+
779
+ [MIT](./LICENSE). Release history is in the [CHANGELOG](./CHANGELOG.md).
package/package.json CHANGED
@@ -1,10 +1,30 @@
1
1
  {
2
2
  "name": "@omgbase/oqx",
3
- "version": "0.10.0",
4
- "description": "Generic Object Query eXpression engine for JavaScript",
5
- "license": "ISC",
6
- "author": "",
3
+ "version": "0.10.1",
4
+ "description": "OQX (omgbase Query eXpressions): a composable query language and engine for JavaScript collections",
5
+ "keywords": [
6
+ "query",
7
+ "query-language",
8
+ "collections",
9
+ "graph",
10
+ "traversal",
11
+ "json",
12
+ "sqlite",
13
+ "omgbase",
14
+ "oqx"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Brendan Baldwin",
18
+ "homepage": "https://github.com/omgbase/oqx#readme",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/omgbase/oqx.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/omgbase/oqx/issues"
25
+ },
7
26
  "type": "module",
27
+ "sideEffects": false,
8
28
  "main": "./dist/index.js",
9
29
  "types": "./dist/index.d.ts",
10
30
  "publishConfig": {
@@ -22,10 +42,11 @@
22
42
  },
23
43
  "files": [
24
44
  "dist",
25
- "src"
45
+ "src",
46
+ "CHANGELOG.md"
26
47
  ],
27
48
  "engines": {
28
- "node": ">=22.6.0"
49
+ "node": ">=22.13.0"
29
50
  },
30
51
  "scripts": {
31
52
  "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/fix-dts-extensions.mjs",