@omgbase/oqx 0.15.0 → 0.18.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +170 -17
  3. package/dist/ast.d.ts +76 -22
  4. package/dist/ast.d.ts.map +1 -1
  5. package/dist/ast.js +21 -6
  6. package/dist/ast.js.map +1 -1
  7. package/dist/build.d.ts +37 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +54 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/context.d.ts +13 -3
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +7 -3
  14. package/dist/context.js.map +1 -1
  15. package/dist/engine.d.ts +1 -2
  16. package/dist/engine.d.ts.map +1 -1
  17. package/dist/engine.js +52 -23
  18. package/dist/engine.js.map +1 -1
  19. package/dist/errors.d.ts +1 -1
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +2 -1
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +19 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +34 -12
  26. package/dist/index.js.map +1 -1
  27. package/dist/lexer.d.ts +15 -1
  28. package/dist/lexer.d.ts.map +1 -1
  29. package/dist/lexer.js +104 -51
  30. package/dist/lexer.js.map +1 -1
  31. package/dist/optimize/analysis.d.ts.map +1 -1
  32. package/dist/optimize/analysis.js +5 -3
  33. package/dist/optimize/analysis.js.map +1 -1
  34. package/dist/optimize/ir.d.ts +2 -2
  35. package/dist/optimize/ir.d.ts.map +1 -1
  36. package/dist/optimize/ir.js +3 -3
  37. package/dist/optimize/ir.js.map +1 -1
  38. package/dist/parser.d.ts.map +1 -1
  39. package/dist/parser.js +616 -251
  40. package/dist/parser.js.map +1 -1
  41. package/dist/plan.d.ts.map +1 -1
  42. package/dist/plan.js +4 -2
  43. package/dist/plan.js.map +1 -1
  44. package/dist/print.d.ts +17 -0
  45. package/dist/print.d.ts.map +1 -0
  46. package/dist/print.js +266 -0
  47. package/dist/print.js.map +1 -0
  48. package/dist/resolve.d.ts +23 -0
  49. package/dist/resolve.d.ts.map +1 -0
  50. package/dist/resolve.js +330 -0
  51. package/dist/resolve.js.map +1 -0
  52. package/dist/version.d.ts +5 -0
  53. package/dist/version.d.ts.map +1 -0
  54. package/dist/version.js +5 -0
  55. package/dist/version.js.map +1 -0
  56. package/dist/walk.d.ts +54 -0
  57. package/dist/walk.d.ts.map +1 -0
  58. package/dist/walk.js +139 -0
  59. package/dist/walk.js.map +1 -0
  60. package/package.json +1 -1
  61. package/src/ast.ts +104 -35
  62. package/src/build.ts +72 -0
  63. package/src/context.ts +16 -3
  64. package/src/engine.ts +55 -26
  65. package/src/errors.ts +3 -2
  66. package/src/index.ts +38 -13
  67. package/src/lexer.ts +102 -51
  68. package/src/optimize/analysis.ts +4 -3
  69. package/src/optimize/ir.ts +4 -4
  70. package/src/parser.ts +573 -225
  71. package/src/plan.ts +3 -2
  72. package/src/print.ts +274 -0
  73. package/src/resolve.ts +305 -0
  74. package/src/version.ts +4 -0
  75. package/src/walk.ts +180 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ 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.18.0] - 2026-10-10
8
+
9
+ ### Minor
10
+ - **OQX 0.17 — join syntax sugar** (`spec/oqx` 0.16 → 0.17; `cases/sugar.json`, 134 new cases, 1131 in all; GRAMMAR §2/§4, SEMANTICS §5b/§14, AST.md). Everything that is pure sugar desugars **in the parser** to existing nodes, so the optimizer, the pushdown planners and `print` see the explicit form; two shapes are new.
11
+
12
+ - **A receiver block without a consumer is `collect`**: `x { … }` ≡ `x collect { … }` in select position, where position and as a whole query (`docs { age > 15 }`). Carve-out: after the last `follow` destination a brace is still the options block (`follow children { depth 2 }`); a destination block keeps its consumer.
13
+ - **Where-first bodies**: a block whose leading expression is *syntactically* a predicate (a comparison, `in`, `&&`/`||`, a prefix `!`/`is`/`not`, an infix `is`, a call, a literal, a binding, a parenthesized expression, a postfix `!` or bracket chain, a consumer test — anything that is not a bare name, a dotted navigation or a lift) drops `where`: `people exists { age > 50 }`, `jobs collect { pay > 2 }`. Bare names stay projections (`docs { is active }` is the bare-field filter); `{ name where age >= 18 }` keeps the keyword; `{ age >= 18, name }` is an error suggesting that form. The top level is unchanged: `age > 15 from people` is still the parse error (where would precede from).
14
+ - **Brackets**: `x[p]` → `x first { where p }`; `x[p]!` → `x single { where p }` under a `required` (zero matches is the required error, many the `single` error); `x[n]` / `x[${i}]` → `x first { offset n }` (out of range is absent; a non-integer binding is the `offset` eval error); `x[n]!` → required positional. Chains compose (`refs(company)[0].name`, `jobs[pay > 2][0]`); a negative or fractional literal index is a parse error; `[`/`]` are tokens.
15
+ - **Postfix `!` = required** — new node `{ kind: "required", expr }` / `Expr::Required`: the value, or the eval error `` `<expr>!` is absent `` (`on <id>` when the row has a scalar identity). Never a filter or a coercion (`0!`, `""!` are values); tightest precedence (`refs(c)[0]!.name` vs `refs(c)[0].name!`); a `!` right after a complete operand is postfix, a `!` at operand start is the negation (`!=` stays one token: write `a! == b` with the space).
16
+ - **`is` / `not` / `and` / `or`** become keywords (reserved: `from where select is not and or` + the literal words). Prefix `is x` ≡ `!!x`, `not x` ≡ `!x`. Infix `x is y` / `x is not y` — new `binary` ops `"is"` / `"is not"` (`BinaryOp::Is` / `IsNot`) — compare **identity** per SEMANTICS §16 (a row's `id` when present, else structural), so `$it is ^$it` compares rows and for scalars `is` ≡ `==`; comparison precedence, non-chainable. `and` / `or` are exact synonyms of `&&` / `||` (same nodes, precedence, short-circuit and value semantics; `print` still writes the symbols). `in` over rows is unchanged (deferred).
17
+ - **Alias reuse in `select`**: an item may reference the items to its left (`boss: ^people[id == ^manager], bossName: boss.name`), inlined by `resolveAliases` / `resolve_aliases` like a `where` alias; a reference to an item to the right is a parse error (a cycle when it refers back). **Behavior change:** `a: b, b: a` is now the cycle error (it was a field swap), and `senior: age > 50, s: senior` projects the alias (it was the row field `senior`).
18
+ - **A directive may stand in value position** (`jobs first { }.pay`): this is how `print` writes a bracket chain back; `op` joins the `Expr` family by position (`isExpr` stays false for it; `transform` rebuilds it without handing it to the mapping). AST.md §8 records the additions (minor; nothing renamed or removed).
19
+ - Existing fixtures that turned from error into meaning: `people exists { age > 50 }`-style blocks (`where` ×4, `errors-parse` consumer-after-projection re-queried as `{ name, jobs exists { } }`, `projection` call-in-block re-queried as `{ name, has(budget) }`), the `[` lex-error case (now a token).
20
+ - **omgbase binding**: the pushdown planners decline `is` / `is not` (identity is the engine's structural notion, not SQL's `IS`), `required` and value-position directives (they may raise), so they stay residual; `query_syntax` gains a "Sugar" block; `docs/query-language.md` §3.6. The surface fixture `implicit-where-in-block` re-queried as `{ kind == "md:task", name }` (still the loud error pointing at `where`).
21
+ - `@omgbase/oqx-syntax`: `[`/`]` are bracket punctuation (no longer lex errors), `is`/`not`/`and`/`or` are keywords highlighted as word operators, `LANGUAGE_VERSION` 0.17; assets regenerated.
22
+ - **OQX 0.18 — the root scope is a row, absolute scope references, canonical `and`/`or`** (`spec/oqx` 0.17 → 0.18; `cases/absolute-refs.json` new (24), `outer-refs` +6 (the `$it is absent at the root scope` case re-queried as the root-object cases), `limit-offset` +2: 1163 cases in all; GRAMMAR §1/§3/§4/§5/§6, SEMANTICS §2/§8/§18/§19, AST.md §1/§3/§6/§8, README).
23
+
24
+ - **The root scope is a row: the host's root object.** SEMANTICS §2 step 1 no longer reads "absent at the root": `$it` at the root scope is the host's root object, so from a top-level row `^$it` is that object, `entries(^$it)` enumerates the named roots in insertion order, `^$it.people` ≡ `^people` (and works as a receiver), `^^$it` from depth two is the same object, and `$it values from $it` is the object as one row. The seam gained **`DataContext.rootObject?(): unknown`** (TypeScript, optional — absent keeps the root row absent) / **`fn root_object(&self) -> Value`** (Rust, provided method defaulting to `Value::Undefined`); `DefaultContext` returns the roots record it was constructed with. Step 5 at the root still reads `ctx.root(name)` — a bare name is never a property of the root object — so hosts with lazy roots are unaffected, and a host-provided root-level name (omgbase will expose the repository id as `$id`, cashing in `$repo.$id` → `^$id` from depth one, `^^$id` from depth two) is readable through `root(name)`. A top-level `limit`/`offset` is evaluated in the root scope *without* its row (SEMANTICS §18): `^$it` reached through a bracket inside the bound is absent, pinned.
25
+ - **`N^name` — absolute scope references.** An unsigned integer literal immediately followed by `^` (no whitespace) names a scope by its syntactic depth (root 0, top-level rows 1, +1 per block body, a destination block's rows one below the frontier row, a follow `where` one deeper than the frontier row — the `visit` depth): `0^docs`, `0^$it`, `1^$path`, `0^$id`, and as a lift target `1^tasks: text`. It **desugars in the parser** to the existing `outer` node (or lift) with `levels = currentDepth − N`; no new AST shape, `print` writes the carets. `N ≥ currentDepth` is the parse error `scope N does not enclose this block (the current scope is depth D)`; `1 ^x` (a space) and `1.0^x` stay the stray-`^` error. Composes with brackets, calls and receivers (`0^docs[0]`, `x[p == 0^flag]`, `entries(0^$it)`, `0^$it.docs collect { … }`, `follow 0^people collect { where manager == 1^id }`, `follow next { where prev == 1^id && 0^k == 2 }`, `limit 1^n`). Both parsers now track the scope depth of every position.
26
+ - **Canonical `print` writes `and` / `or`** for the `where` tree's `and`/`or` nodes and for `logical` expressions (both engines); `!` stays `!`, `is`/`is not` were already words, the JSON `logical.op` stays `"&&"`/`"||"`. Consequence: every query text omgbase's `graph` macro prints flips from `||` to `or` — the pinned `graph` query strings in `spec/surface` (`reads.json`, and the `omgbase-surface` unit test for the multi-root seed) are regenerated; nothing else in the surface fixtures changes.
27
+ - **omgbase binding** (`@omgbase/core` `oqx-js/context.ts`, `omgbase-surface` `context.rs`): the root object is the repository root (`$repo`), so `^$it` from a top-level row is `$repo` and `^$it.docs` the docs scan. The `^docs`/`$repo` redesign and the `$id` root name are a separate change.
28
+ - `@omgbase/oqx-syntax`: the `N^` lexeme highlights as one outer-reference token (`lift`), `LANGUAGE_VERSION` 0.18; assets regenerated.
29
+
30
+ ## [0.16.0] - 2026-10-09
31
+
32
+ ### Minor
33
+ - **OQX 0.16 — the AST is a first-class contract** (`spec/oqx/AST.md`, fixtures `cases/ast.json` + `cases/ast-spans.json`, 70 new cases; 959 in all). Both engines now produce one normalized tree a tool can reflect on without re-parsing:
34
+
35
+ - **Spans on every node**: `span: [start, end)` in Unicode code points over the raw source (a template's `rawSource`, where a binding occupies its `${n}` marker). The TypeScript lexer counted UTF-16 units; it now counts code points like the Rust one, so the `(at offset N)` of a lex/parse error is the same number in both engines. `toUtf16(span, source)` converts for editors.
36
+ - **Normalized shape**: `kind` on every node (`query`, `subquery`, `follow`, `order` added), optionals materialized (`where`/`limit`/`offset`/`countCmp: null`, `distinct`/`values: false`), operators and consumers serialized as their source words, the never-produced `Expr.index` variant removed. `toJSON(query)` / `ast_to_json(&query)` emit `{ "oqx": "0.16", "kind": "query", … }`; the Rust crate derives serde under the `json` feature (`query_from_json` reads it back).
37
+ - **`where` keeps its surface form**: the parser no longer inlines `select` aliases into `where` (it only validates them — every `errors-parse` fixture is unchanged); the substitution is the exported pure `resolveAliases` / `resolve_aliases`, applied exactly once by the run entry points (`oqx`, `run`, `execute`, `runQuery`; `run_query`, `execute`). **An `Engine.run` now evaluates the query it is given** — code that drives `InMemoryEngine`/`PlannedEngine` directly with a query whose `where` uses aliases must resolve first (resolution is not idempotent: `name: name.upper() … where name`).
38
+ - **Traversal**: `visit(root, { enter, leave })` with `{ path, clause, depth }` (scope depth, 0 = root) and `transform(root, f)`, both driven by one child-key table (`CHILDREN`; Rust `walk::visit`/`transform` with a `Visitor` trait, `Node`, `Clause`); `stripSpans` / `strip_spans`; builders `build.*` / `oqx::build::*` for hand-made nodes (empty span `[0, 0]`).
39
+ - **Canonical printer**: `print(node)` / `print_query` — single spaces, `select` written at the top level, every item aliased, double-quoted strings, minimal parentheses from the precedence table; `printTemplate` → `{ strings, count, indices }` for a tree with bindings (`print` throws, new error stage `print`). Law, enforced by both spec runners over every fixture query: `strip(parse(print(parse(q)))) ≡ strip(parse(q))`.
40
+ - **Migration** for anyone constructing AST literals (after 0.14's `receiver` → `destinations`): add `kind` to `Query`/`Subquery`/`Follow`/`OrderSpec`, `span` to every node, write the materialized fields (`limit: null`, `countCmp: null`, `distinct: false`, `values: false`), drop `index`; or use `build.*`. Rust: struct variants gained `span` (match with `..`), `Expr::Lit(v)` is `Expr::Lit { value, span }`, `Token` gained `end`.
41
+ - **omgbase** (`@omgbase/core`, `omgbase-surface`): the runner's `$self` row-function rewrite is a `transform`, the `semantic("…")` scan a `visit`, aliases are resolved once before the runner's own rewrites; the `graph` macro builds its `follow` walk with the builders and `print`s it instead of splicing strings — the generated query text and every `spec/surface` result are unchanged. `@omgbase/oqx-syntax`'s `LANGUAGE_VERSION` moves to `0.16` (assets regenerated).
42
+
7
43
  ## [0.15.0] - 2026-10-09
8
44
 
9
45
  ### Minor
package/README.md CHANGED
@@ -16,7 +16,7 @@ JavaScript tagged template.
16
16
  > OQX is a language with more than one implementation; the specification is
17
17
  > [`spec/oqx`](https://github.com/omgbase/omgbase/tree/main/spec/oqx). The
18
18
  > language version is the package version's `major.minor` (`LANGUAGE_VERSION`,
19
- > `"0.15"`); the patch digit is this implementation's own.
19
+ > `"0.17"`); the patch digit is this implementation's own.
20
20
  > Requirements: Node ≥ 22.13 for `@omgbase/oqx/sqlite`; Node ≥ 22.18 to run the
21
21
  > test suite (see [Requirements](#requirements)).
22
22
 
@@ -65,9 +65,12 @@ from <collection> from ${people}
65
65
 
66
66
  **`select` is the only keyword you may drop, and only when the projection comes
67
67
  first** (`name, id from ${people}`). Every other clause always carries its
68
- keyword — in particular a predicate is never implicit, so a block filters with
69
- `where` (`jobs exists { where !end }`), and `from ${people} count` is an error
70
- rather than a projection of a field called `count`. Writing a clause out of
68
+ keyword — with one piece of sugar (0.17): a **block** whose leading expression
69
+ is syntactically a predicate is a `where`-first body (`jobs exists { !end }` ≡
70
+ `jobs exists { where !end }`; a bare name still projects, so `jobs { employer }`
71
+ projects and `jobs { is current }` filters — see §10). At the top level a
72
+ predicate always needs `where`, and `from ${people} count` is an error rather
73
+ than a projection of a field called `count`. Writing a clause out of
71
74
  order is a parse error that names the order (`` `select` must come before
72
75
  `from` — OQX clause order is select, from, where, follow, order by, limit,
73
76
  offset ``). The same body grammar applies inside every consumer block
@@ -171,7 +174,10 @@ oqx`employee: $it from ${people} where name == "Bob"`; // [{ employee: <
171
174
  ```
172
175
 
173
176
  Inside a nested block `$it` is the inner item; the enclosing row is `^$it`
174
- (§3). At the root scope (before any row) it is absent. `$it` is the only name
177
+ (§3). At the root scope `$it` is the context's **root object** (language 0.18;
178
+ `DataContext.rootObject()` — `DefaultContext` returns the roots record, so
179
+ `^$it` from a top-level row is `{ people, … }` and `entries(^$it)` lists the
180
+ named roots). `$it` is the only name
175
181
  for the current item (language 0.15; through 0.14 it was spelled `$value`, and
176
182
  there is no synonym): `$value` is now an ordinary property read, like any other
177
183
  non-metadata `$name` — a row `{ "$value": 3 }` projects `3`, a row without one
@@ -259,9 +265,10 @@ against the value of `x`; a string in `x` can't inject operators or identifiers.
259
265
 
260
266
  **`where` sees the `select` aliases.** A name defined in the same body's
261
267
  projection may be used in its `where`; an alias shadows a same-named field
262
- there. It is a compile-time rewrite — the alias's expression is substituted
263
- inline — so it costs nothing at run time and a storage planner still sees an
264
- ordinary predicate:
268
+ there. The alias's expression is substituted inline before evaluation
269
+ (`resolveAliases`, applied once by the run entry points — the parsed tree keeps
270
+ what you wrote), so it costs nothing at run time and a storage planner still
271
+ sees an ordinary predicate:
265
272
 
266
273
  ```js
267
274
  oqx`select name, adult: age >= 18 from ${people} where adult`; // ≡ where age >= 18
@@ -280,7 +287,11 @@ an alias. `order by` is not rewritten — it reads row fields (§8).
280
287
  against the **current row only**. If the row lacks that property the value is
281
288
  absent — it never falls through to an enclosing row. To correlate with an
282
289
  enclosing scope you say so explicitly with `^name` ("exactly one scope out";
283
- `^^name` for two, and so on):
290
+ `^^name` for two, and so on) — or by **depth** with `N^name` (language 0.18):
291
+ `0^people` is the root scope's `people` from any depth, `1^name` the top-level
292
+ row's `name`, `0^$it` the root object. `N^` is an integer touching a caret
293
+ (`1 ^x` is not one); it is pure sugar for the caret form with `levels = depth −
294
+ N`, and `N ≥ the current depth` is a parse error.
284
295
 
285
296
  ```js
286
297
  const accounts = [
@@ -642,14 +653,61 @@ with `^` the enclosing scope, as the body does. Give `follow` a stable identity
642
653
  (`.id` or `by`) when your relation returns fresh objects rather than shared
643
654
  references.
644
655
 
656
+ ### 10. Sugar (0.17)
657
+
658
+ Every form below is shorthand for an explicit directive — it **desugars in the
659
+ parser** to the nodes you already know (`print` writes the explicit form, the
660
+ optimizer and any pushdown planner see it), so nothing here has semantics of its
661
+ own except the postfix `!` and the infix `is`:
662
+
663
+ ```js
664
+ // A receiver block without a consumer is `collect`; a block whose LEADING
665
+ // expression is syntactically a predicate drops `where`.
666
+ oqx`name, current: jobs { !end_date } from ${people}`
667
+ // ≡ name, current: jobs collect { where !end_date } from ${people}
668
+ oqx`${people} { age > 40 }` // ≡ ${people} collect { where age > 40 }
669
+ oqx`${people} count { jobs exists { } }` // a consumer test is predicate-shaped too
670
+ oqx`${people} first { name }` // a bare name still projects — unchanged
671
+ oqx`${people} first { is active }` // the bare-field filter: `is x` ≡ `!!x`, `not x` ≡ `!x`
672
+
673
+ // Brackets: `[p]` is `first { where p }`, `[n]` is `first { offset n }`;
674
+ // a trailing `!` requires the result (and makes the predicate form `single`).
675
+ oqx`name, boss: ^people[id == ^manager].name from ${people}` // the first match, navigated
676
+ oqx`name, first_job: jobs[0], primary: jobs[pay > 2]! from ${people}`
677
+ // jobs[0] ≡ jobs first { offset 0 } (out of range ⇒ absent)
678
+ // jobs[pay > 2]! ≡ jobs single { where pay > 2 }! (zero or many matches ⇒ an eval error)
679
+
680
+ // Postfix `!` — required: the value, or an eval error naming the expression
681
+ // (and the row's id). Never a filter, never a coercion: 0! and ""! are values.
682
+ oqx`select id!, title from ${docs}` // insist on an identity before a write
683
+ // Tightest precedence: `refs(c)[0]!.name` requires the lookup, `refs(c)[0].name!` the name.
684
+
685
+ // `is` / `is not` compare IDENTITY (a row's id when present, else structurally):
686
+ oqx`name from ${people} where manager is null` // absent (same as == null)
687
+ oqx`${people} count { ^people exists { $it is ^$it } }` // rows compare; `==` leaves that unspecified
688
+ // `and` / `or` are exact synonyms of `&&` / `||`: `title or path` coalesces.
689
+ // `print` writes the words (language 0.18); `!` stays `!`.
690
+
691
+ // A select item may use the items to its left (inlined like a `where` alias):
692
+ oqx`boss: ^people[id == ^manager], bossName: boss.name from ${people}`
693
+ ```
694
+
695
+ Reserved words, never a bare field name: `from where select is not and or` and
696
+ the literal words `true false null`. The top level is unchanged — `age > 15
697
+ from ${people}` stays an error, because `where` would precede `from`.
698
+
645
699
  ### Cheat-sheet
646
700
 
647
701
  ```
648
702
  select … from … where … follow … order by … limit N offset N the fixed clause order (each at most once)
649
703
  name, alias: expr, nested: rel collect { … } projection (`select` may be dropped only here, in first position)
650
704
  from ${source} source collection (required at the top level)
651
- where a == b && rel exists { where … } || !c predicate tree + nested ops (`where` is never implicit)
652
- where alias `where` may use this body's select aliases (inlined; an alias shadows a field)
705
+ where a == b && rel exists { where … } || !c predicate tree + nested ops (`and`/`or` ≡ `&&`/`||`)
706
+ rel { pay > 2 } / rel { employer } a block without a consumer is collect; a leading predicate is where-first, a bare name projects
707
+ rel[p] / rel[p]! / rel[0] / rel[0]! first match / exactly one (required) / positional / required positional
708
+ x! required: the value, or an error when absent (tightest; `a!.b` vs `a.b!`)
709
+ is x / not x / x is y / x is not y truthiness (≡ !!x / !x) / identity (a row's id, else structural; absent ≡ null)
710
+ where alias / a: expr, b: a `where` and later items may use this body's select aliases (resolved before evaluation; an alias shadows a field)
653
711
  where rel none { … } zero rows (≡ !rel exists { … }; "all" = none over the complement)
654
712
  where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl. / open-ended)
655
713
  where x in range(field) coerce a string field to a range, then test coverage
@@ -669,6 +727,78 @@ follow ^people collect { where manager == ^id } a destination block: successors
669
727
  ${source} <collect|exists|none|count|first|single> { … } whole-query consumer
670
728
  ```
671
729
 
730
+ ## The AST: reflecting on a query
731
+
732
+ `parse(source)` returns the query's abstract syntax tree, and since language
733
+ 0.16 that tree is a **public contract** shared by both implementations
734
+ ([`spec/oqx/AST.md`](https://github.com/omgbase/omgbase/tree/main/spec/oqx/AST.md)):
735
+ a tool reflects on a query — which relations it names, which clauses it has,
736
+ where in the text each piece sits — without re-parsing. The objects are plain
737
+ data (no parent pointers, no methods) and *are* the JSON shape.
738
+
739
+ - **Every node has `kind` and `span`.** `span` is `[start, end)` in Unicode
740
+ **code points** over the raw source (for a template, `rawSource(strings)`,
741
+ where a binding occupies its `${n}` marker); `toUtf16(span, source)` converts
742
+ for an editor that counts UTF-16 units. A parenthesized operand spans its
743
+ parentheses; a block (`subquery`) spans its braces.
744
+ - **Optionals are materialized** — `where: null`, `limit: null`, `countCmp: null`,
745
+ `distinct: false`, `values: false` — so nothing has to be defaulted.
746
+ - **`where` keeps its surface form.** A `select` alias used in `where` stays an
747
+ `ident` in the tree; `resolveAliases(query)` is the pure substitution the run
748
+ entry points (`oqx`, `run`, `execute`, `runQuery`) apply exactly once before
749
+ evaluation. An `Engine.run` evaluates the query it is given — if you drive an
750
+ engine yourself, resolve first.
751
+
752
+ ```js
753
+ import { parse, visit, transform, print, printTemplate, toJSON, stripSpans, build } from "@omgbase/oqx";
754
+
755
+ const q = parse('select name, n: jobs collect { employer } from people where jobs exists { where !end }');
756
+
757
+ // visit: every node, with its ancestors, the clause it sits in, and the scope depth
758
+ const relations = [];
759
+ visit(q, {
760
+ enter(node, { clause, depth }) {
761
+ if (node.kind === "op") relations.push({ relation: print(node.receiver), clause, depth });
762
+ },
763
+ });
764
+ // → [{ relation: "jobs", clause: "select", depth: 1 }, { relation: "jobs", clause: "where", depth: 1 }]
765
+
766
+ // transform: rebuild with every expression mapped (children first; untouched nodes keep identity + spans)
767
+ const rewritten = transform(q, (e) => (e.kind === "ident" && e.name === "name" ? build.member(build.ident("person"), "name") : e));
768
+
769
+ // print: the canonical source — single spaces, `select` written, minimal parentheses
770
+ print(rewritten);
771
+ // 'select name: person.name, n: jobs collect { employer: employer } from people where jobs exists { where !end }'
772
+
773
+ toJSON(q); // { oqx: "0.17", kind: "query", span: [0, 93], … } — the shared JSON document
774
+ stripSpans(q); // the same tree without spans, for shape comparisons
775
+ ```
776
+
777
+ `visit(root, { enter?, leave? })` walks depth-first in canonical source order;
778
+ `enter` may return `false` to skip a subtree. Its context is `{ path, clause,
779
+ depth }` — the ancestors, the clause (`source` · `from` · `where` · `select` ·
780
+ `orderBy` · `limit` · `offset` · `follow` · `follow.destination` · `follow.where`
781
+ · `follow.frontier` · `follow.by`; `null` at the root) and the **scope depth**
782
+ the node is evaluated at (0 = the root scope: a top-level source or bound; 1 =
783
+ a top-level row; a block's body is one deeper than its receiver). Both walks
784
+ are driven by one child-key table, `CHILDREN`, so a future field is added in one
785
+ place.
786
+
787
+ `print(node)` is canonical, and the law `stripSpans(parse(print(parse(q))))`
788
+ ≡ `stripSpans(parse(q))` holds for every spec fixture (both runners check it);
789
+ `print(parse(q)) === q` is *not* a law — spelling is normalized. A binding is a
790
+ value, never source text: `print` throws (`OqxError`, stage `"print"`) on one,
791
+ and `printTemplate(node)` returns `{ strings, count, indices }` — the fragments
792
+ around each binding and the binding each gap stands for (the canonical clause
793
+ order can move one past another), to re-run with `parseTemplate(strings,
794
+ count)` and the values permuted by `indices`.
795
+
796
+ `build.*` constructs nodes with the empty span `[0, 0]` and the materialized
797
+ defaults (`build.query(source, { select, where, … })`, `build.op(receiver,
798
+ "collect", build.subquery({ … }))`, `build.field`, `build.collect`,
799
+ `build.follow`, `build.ident`, `build.lit`, `build.binary`, `build.and`, …), so
800
+ a tree you assemble prints with `print` and has the shape of a parsed one.
801
+
672
802
  ## Data context: string queries and named roots
673
803
 
674
804
  When you don't need interpolation, `execute` runs a plain string query against a
@@ -687,7 +817,8 @@ the full discriminated result (`{ consumer, … }`).
687
817
  ## Architecture: adapting to other storage & query systems
688
818
 
689
819
  OQX is layered so it can be the front-end for query systems far beyond in-memory
690
- objects. The parsed `Query` AST is the host-agnostic IR; execution is pluggable.
820
+ objects. The parsed `Query` AST is the host-agnostic contract (above); execution
821
+ is pluggable.
691
822
 
692
823
  ```
693
824
  Query AST ─┬─ InMemoryEngine(DataContext) tier 1/2 — drive any data model
@@ -705,8 +836,10 @@ than approximate it. The conformance suite verifies this.
705
836
 
706
837
  The engine never touches host objects directly; it asks a `DataContext` to
707
838
  resolve named roots, read properties/relations, coerce results to rows, and
708
- compute identity. Implement it to query an ORM graph, a remote API, or lazily
709
- loaded relations — the query *semantics* stay in OQX. Name resolution is simple
839
+ compute identity (and, optionally, to supply a `rootObject()` — the root
840
+ scope's row, what `$it` is at the root and `^$it` from a top-level row; without
841
+ one the root row is absent). Implement it to query an ORM graph, a remote API,
842
+ or lazily loaded relations — the query *semantics* stay in OQX. Name resolution is simple
710
843
  for a context: a bare `field`, a `.field` segment, and a `^field` outer
711
844
  reference each become one `get(row, key)` against exactly the row of the scope
712
845
  they name, so a computed relation only needs `get` to know about it:
@@ -908,7 +1041,8 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
908
1041
  consumer-shaped value: an array for `collect`, a boolean for `exists` / `none`,
909
1042
  a number for `count`, a record or `null` for `first` / `single`.
910
1043
  - `parse(source)` — a query string → reusable `Query` AST (an `OqxError` with
911
- `stage: "lex" | "parse"` on bad input).
1044
+ `stage: "lex" | "parse"` on bad input); `parseTemplate(strings, count)` the
1045
+ tagged-template form, with `binding` nodes for the `${…}` slots.
912
1046
  - `execute(source, roots?)` — parse and run a string against named roots
913
1047
  (`{ people }`), returning the consumer-shaped value.
914
1048
  - `run(query, opts?)` — run a parsed `Query`, returning the full `OqxResult`.
@@ -1032,11 +1166,30 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1032
1166
  - `map?: (raw) => row` — a custom raw-SQL-row → query-row mapper (overrides
1033
1167
  `jsonColumns`).
1034
1168
 
1169
+ **The AST** (see [above](#the-ast-reflecting-on-a-query); `spec/oqx/AST.md` is normative)
1170
+
1171
+ - `visit(root, { enter?, leave? })` / `transform(root, f)` — the generic walks
1172
+ (`Visitor`, `VisitContext`, `Clause` are the types); `CHILDREN` — the
1173
+ child-key table per node kind that drives them.
1174
+ - `print(node)` / `printTemplate(node)` — the canonical printer (`Template` is
1175
+ `{ strings, count, indices }`).
1176
+ - `resolveAliases(query)` — the `select`-alias substitution the entry points
1177
+ apply before evaluation (SEMANTICS §14).
1178
+ - `toJSON(query)` — the tree stamped with `oqx: LANGUAGE_VERSION`; `stripSpans(node)`
1179
+ — the tree without spans (`Stripped<T>`); `toUtf16(span, source)` and
1180
+ `codePointLength(s)` — code-point ↔ UTF-16 offsets; `rawSource(strings)` —
1181
+ the display source of a template.
1182
+ - `build` — the node builders (`build.query`, `build.op`, `build.subquery`,
1183
+ `build.field`, `build.ident`, …).
1184
+ - `LANGUAGE_VERSION` — the `spec/oqx/VERSION` this implementation conforms to (`"0.17"`).
1185
+
1035
1186
  **Types**
1036
1187
 
1037
1188
  - Every AST node type from `src/ast.ts` is re-exported (`Query`, `Subquery`,
1038
- `Where`, `Expr`, `OpNode`, `SelectItem`, `OrderSpec`, `Follow`, …) for
1039
- planners that walk the IR.
1189
+ `Where`, `Expr`, `OpNode`, `SelectItem`, `OrderSpec`, `Follow`,
1190
+ `AstNode`, `AstKind`, `Span`, …) for tools that reflect on queries and
1191
+ planners that walk the tree; `isExpr(node)` tells an expression from the
1192
+ structural nodes.
1040
1193
 
1041
1194
  ## Requirements
1042
1195
 
package/dist/ast.d.ts CHANGED
@@ -1,91 +1,123 @@
1
+ /** A half-open source range `[start, end)` in Unicode code points over the raw
2
+ * source (`rawSource` for a template, where a binding occupies its `${n}`
3
+ * marker). The empty span `[0, 0]` marks a node that did not come from source. */
4
+ export type Span = [start: number, end: number];
1
5
  /** Query consumers — how a (sub)query's row set is shaped. `none` is the
2
6
  * zero-cardinality complement of `exists` (true iff the block yields no rows). */
3
7
  export type Consumer = "collect" | "exists" | "none" | "count" | "first" | "single";
4
8
  /** Comparison operators usable in a `count { … } <op> <int>` test. */
5
9
  export type RelOp = "==" | "!=" | "<" | "<=" | ">" | ">=";
10
+ /** The identity operators (SEMANTICS §5b, since 0.17). */
11
+ export type IdentityOp = "is" | "is not";
12
+ /** The operator of a `binary` expression: a comparison, an identity test, or arithmetic. */
13
+ export type BinaryOp = RelOp | IdentityOp | "+" | "-" | "*" | "/" | "%";
14
+ /** The operator of a `unary` expression. */
15
+ export type UnaryOp = "!" | "-";
16
+ /** The operator of a `logical` expression. */
17
+ export type LogicalOp = "&&" | "||";
6
18
  /** Scalar value/predicate expression, evaluated against a row scope + bindings. */
7
19
  export type Expr = {
8
20
  kind: "lit";
21
+ span: Span;
9
22
  value: string | number | boolean | null;
10
23
  } | {
11
24
  kind: "ident";
25
+ span: Span;
12
26
  name: string;
13
27
  } | {
14
28
  kind: "outer";
29
+ span: Span;
15
30
  levels: number;
16
31
  name: string;
17
32
  } | {
18
33
  kind: "binding";
34
+ span: Span;
19
35
  index: number;
20
36
  } | {
21
37
  kind: "member";
38
+ span: Span;
22
39
  recv: Expr;
23
40
  name: string;
24
- } | {
25
- kind: "index";
26
- recv: Expr;
27
- index: Expr;
28
41
  } | {
29
42
  kind: "call";
43
+ span: Span;
30
44
  recv: Expr | null;
31
45
  name: string;
32
46
  args: Expr[];
33
47
  } | {
34
48
  kind: "unary";
35
- op: "!" | "-";
49
+ span: Span;
50
+ op: UnaryOp;
51
+ expr: Expr;
52
+ } | {
53
+ kind: "required";
54
+ span: Span;
36
55
  expr: Expr;
37
56
  } | {
38
57
  kind: "binary";
39
- op: string;
58
+ span: Span;
59
+ op: BinaryOp;
40
60
  left: Expr;
41
61
  right: Expr;
42
62
  } | {
43
63
  kind: "logical";
44
- op: "&&" | "||";
64
+ span: Span;
65
+ op: LogicalOp;
45
66
  left: Expr;
46
67
  right: Expr;
47
68
  } | {
48
69
  kind: "in";
70
+ span: Span;
49
71
  left: Expr;
50
72
  right: Expr;
51
73
  } | {
52
74
  kind: "range";
75
+ span: Span;
53
76
  lo: Expr | null;
54
77
  hi: Expr | null;
55
78
  exclusiveEnd: boolean;
56
- };
79
+ } | OpNode;
80
+ /** One `order by` term. */
57
81
  export interface OrderSpec {
82
+ kind: "order";
83
+ span: Span;
58
84
  expr: Expr;
59
85
  desc: boolean;
60
86
  }
61
87
  /** One projection item: a named scalar/navigation value, or a named nested
62
- * collection consumer. `lift` marks a `^name` one-scope lift. */
88
+ * collection consumer. `lift` marks a `^name` lift. */
63
89
  export type SelectItem = {
64
90
  kind: "field";
91
+ span: Span;
65
92
  name: string;
66
93
  expr: Expr;
67
94
  lift: number;
68
95
  } | {
69
96
  kind: "collect";
97
+ span: Span;
70
98
  name: string;
71
99
  op: OpNode;
72
100
  };
101
+ /** The `count { … } <op> <int>` test attached to an `OpNode` — an attribute of
102
+ * the directive, not a node of its own. */
103
+ export interface CountCmp {
104
+ op: RelOp;
105
+ value: number;
106
+ }
73
107
  /** A postfix consumer directive over a receiver collection:
74
108
  * `<receiver> <op> { <sub> }`, optionally `count { … } <relop> <int>`. */
75
109
  export interface OpNode {
76
110
  kind: "op";
111
+ span: Span;
77
112
  receiver: Expr;
78
113
  op: Consumer;
79
114
  sub: Subquery;
80
- countCmp?: {
81
- op: RelOp;
82
- value: number;
83
- };
115
+ countCmp: CountCmp | null;
84
116
  /** `distinct` — dedup the rows this directive consumes by their projected
85
117
  * value (the `select`), so `count distinct { … }` counts distinct projections
86
118
  * and `collect distinct { … }` yields distinct rows. Empty select ⇒ dedup by
87
119
  * row identity. Spellable as `<op> distinct { … }` or `{ select distinct … }`. */
88
- distinct?: boolean;
120
+ distinct: boolean;
89
121
  }
90
122
  /** One `follow` destination: a relation of the current row (an `Expr` receiver), or a
91
123
  * destination block — a select-position directive (`collect`/`first`/`single`) re-evaluated
@@ -93,6 +125,8 @@ export interface OpNode {
93
125
  export type FollowDestination = Expr | OpNode;
94
126
  /** The recursive `follow` clause. */
95
127
  export interface Follow {
128
+ kind: "follow";
129
+ span: Span;
96
130
  destinations: FollowDestination[];
97
131
  distinct: boolean;
98
132
  where: Expr | null;
@@ -100,8 +134,10 @@ export interface Follow {
100
134
  depth: number | null;
101
135
  by: Expr | null;
102
136
  }
103
- /** A nested (sub)query body. */
137
+ /** A nested (sub)query body — the `{ … }` of a directive. */
104
138
  export interface Subquery {
139
+ kind: "subquery";
140
+ span: Span;
105
141
  from: Expr[];
106
142
  where: Where | null;
107
143
  select: SelectItem[];
@@ -110,33 +146,39 @@ export interface Subquery {
110
146
  /** `values` — scalar projection mode: the (single) projected expression is the
111
147
  * row's result itself rather than being wrapped in a `{ name: value }` record,
112
148
  * so `name values` yields `["Bob", …]` and `$it values` yields the rows. */
113
- values?: boolean;
149
+ values: boolean;
114
150
  /** `limit N` / `offset N` — bound the row set AFTER where/order/distinct and
115
151
  * BEFORE the consumer reduces it, so `count { … limit 5 }` is at most 5 and
116
152
  * `first { … offset 1 }` is the second row. Each is a value expression
117
153
  * (a literal, a `${…}` binding, or an outer reference) read as part of the
118
154
  * block — `^n` is the enclosing row's field, as everywhere inside `{ … }` —
119
155
  * and must yield a non-negative integer. */
120
- limit?: Expr;
121
- offset?: Expr;
156
+ limit: Expr | null;
157
+ offset: Expr | null;
122
158
  }
123
159
  /** The where-clause boolean tree: OQX owns &&/||/!/grouping so consumer ops
124
160
  * (invisible to the scalar evaluator) compose with scalar predicates. */
125
161
  export type Where = {
126
162
  kind: "and";
163
+ span: Span;
127
164
  parts: Where[];
128
165
  } | {
129
166
  kind: "or";
167
+ span: Span;
130
168
  parts: Where[];
131
169
  } | {
132
170
  kind: "not";
171
+ span: Span;
133
172
  expr: Where;
134
173
  } | {
135
174
  kind: "scalar";
175
+ span: Span;
136
176
  expr: Expr;
137
177
  } | OpNode;
138
178
  /** A top-level OQX query. */
139
179
  export interface Query {
180
+ kind: "query";
181
+ span: Span;
140
182
  source: Expr;
141
183
  from: Expr[];
142
184
  where: Where | null;
@@ -145,11 +187,23 @@ export interface Query {
145
187
  consumer: Consumer;
146
188
  follow: Follow | null;
147
189
  /** `distinct` — dedup the result rows by their projected value (see OpNode). */
148
- distinct?: boolean;
190
+ distinct: boolean;
149
191
  /** `values` — scalar projection mode (see Subquery). */
150
- values?: boolean;
192
+ values: boolean;
151
193
  /** `limit N` / `offset N` (see Subquery); evaluated at the root scope. */
152
- limit?: Expr;
153
- offset?: Expr;
194
+ limit: Expr | null;
195
+ offset: Expr | null;
154
196
  }
197
+ /** Every node of the tree. `AstNode["kind"]` is the closed set of node kinds. */
198
+ export type AstNode = Query | Subquery | OpNode | Follow | OrderSpec | SelectItem | Where | Expr;
199
+ /** The closed set of node kinds. */
200
+ export type AstKind = AstNode["kind"];
201
+ /** Whether a node is a scalar `Expr` (an `op` in expression position is not: it
202
+ * is an expression only by position, see AST.md §1). */
203
+ export declare function isExpr(node: AstNode): node is Expr;
204
+ /** The parsed query's JSON form (`toJSON`): the tree with the language version
205
+ * it was produced under. */
206
+ export type QueryJson = Query & {
207
+ oqx: string;
208
+ };
155
209
  //# sourceMappingURL=ast.d.ts.map
package/dist/ast.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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"}
package/dist/ast.js CHANGED
@@ -1,7 +1,22 @@
1
- // OQX query AST for the generic kernel. The parser produces this directly and
2
- // the interpreter walks it — there is no separate lowering/IR pass, because a
3
- // "relation" in the generic kernel is just an expression evaluated against the
4
- // current row (property navigation on the host object model), resolved at run
5
- // time rather than against a fixed schema.
6
- export {};
1
+ // The OQX AST — a first-class, language-level contract (spec/oqx/AST.md, since
2
+ // language 0.16). The parser produces it, the interpreter walks it, and tools
3
+ // reflect on it without re-parsing: every node carries `kind` and `span`, every
4
+ // optional field is materialized (`null` / `false`), and the in-memory objects
5
+ // ARE the JSON shape both implementations serialize (plain data, no parent
6
+ // pointers, no methods). There is no separate lowering/IR pass: a "relation" is
7
+ // just an expression evaluated against the current row.
8
+ //
9
+ // `where` keeps its surface form: a `select` alias referenced in `where` stays a
10
+ // bare identifier here; `resolveAliases` (resolve.ts) performs the substitution
11
+ // before evaluation. Nodes built by a tool rather than the parser carry the
12
+ // empty span `[0, 0]` (see `build.ts`).
13
+ /** The expression kinds (`Expr["kind"]`), for `isExpr`. */
14
+ const EXPR_KINDS = new Set([
15
+ "lit", "ident", "outer", "binding", "member", "call", "unary", "required", "binary", "logical", "in", "range",
16
+ ]);
17
+ /** Whether a node is a scalar `Expr` (an `op` in expression position is not: it
18
+ * is an expression only by position, see AST.md §1). */
19
+ export function isExpr(node) {
20
+ return EXPR_KINDS.has(node.kind);
21
+ }
7
22
  //# sourceMappingURL=ast.js.map
package/dist/ast.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,2CAA2C"}
1
+ {"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,2EAA2E;AAC3E,gFAAgF;AAChF,wDAAwD;AACxD,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,4EAA4E;AAC5E,wCAAwC;AAqKxC,2DAA2D;AAC3D,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC;IAC9C,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO;CAC9G,CAAC,CAAC;AAEH;wDACwD;AACxD,MAAM,UAAU,MAAM,CAAC,IAAa;IAClC,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC"}