@omgbase/oqx 0.15.0 → 0.20.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 (76) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +308 -70
  3. package/dist/ast.d.ts +97 -27
  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 +43 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +58 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/context.d.ts +30 -6
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +11 -10
  14. package/dist/context.js.map +1 -1
  15. package/dist/engine.d.ts +13 -3
  16. package/dist/engine.d.ts.map +1 -1
  17. package/dist/engine.js +281 -100
  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 +36 -13
  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 +3 -2
  32. package/dist/optimize/analysis.d.ts.map +1 -1
  33. package/dist/optimize/analysis.js +19 -18
  34. package/dist/optimize/analysis.js.map +1 -1
  35. package/dist/optimize/ir.d.ts +2 -2
  36. package/dist/optimize/ir.d.ts.map +1 -1
  37. package/dist/optimize/ir.js +3 -3
  38. package/dist/optimize/ir.js.map +1 -1
  39. package/dist/parser.d.ts.map +1 -1
  40. package/dist/parser.js +782 -335
  41. package/dist/parser.js.map +1 -1
  42. package/dist/plan.d.ts.map +1 -1
  43. package/dist/plan.js +4 -2
  44. package/dist/plan.js.map +1 -1
  45. package/dist/print.d.ts +17 -0
  46. package/dist/print.d.ts.map +1 -0
  47. package/dist/print.js +266 -0
  48. package/dist/print.js.map +1 -0
  49. package/dist/resolve.d.ts +23 -0
  50. package/dist/resolve.d.ts.map +1 -0
  51. package/dist/resolve.js +333 -0
  52. package/dist/resolve.js.map +1 -0
  53. package/dist/version.d.ts +5 -0
  54. package/dist/version.d.ts.map +1 -0
  55. package/dist/version.js +5 -0
  56. package/dist/version.js.map +1 -0
  57. package/dist/walk.d.ts +56 -0
  58. package/dist/walk.d.ts.map +1 -0
  59. package/dist/walk.js +157 -0
  60. package/dist/walk.js.map +1 -0
  61. package/package.json +1 -1
  62. package/src/ast.ts +126 -40
  63. package/src/build.ts +73 -0
  64. package/src/context.ts +36 -12
  65. package/src/engine.ts +285 -110
  66. package/src/errors.ts +3 -2
  67. package/src/index.ts +40 -14
  68. package/src/lexer.ts +102 -51
  69. package/src/optimize/analysis.ts +17 -15
  70. package/src/optimize/ir.ts +4 -4
  71. package/src/parser.ts +721 -289
  72. package/src/plan.ts +3 -2
  73. package/src/print.ts +276 -0
  74. package/src/resolve.ts +309 -0
  75. package/src/version.ts +4 -0
  76. package/src/walk.ts +201 -0
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
 
@@ -34,7 +34,7 @@ const company = "Globocorp";
34
34
  const employees = oqx`
35
35
  name, id, title
36
36
  from ${people}
37
- where jobs exists { where employer == ${company} && !end_date }
37
+ where jobs exists { where employer == ${company} and not end_date }
38
38
  `;
39
39
  // → [{ name: "Bob", id: 124, title: "Engineer" }, …] (current Globocorp employees)
40
40
  ```
@@ -56,8 +56,8 @@ once, and every one is optional except `from` at the top level:
56
56
  ```
57
57
  [ [select] projection ] name, id, title: label (or: select distinct …, … values)
58
58
  from <collection> from ${people}
59
- [ where <predicate> ] where age >= 18 && jobs exists { where !end }
60
- [ follow <dest>, … ] follow children { depth 4 } (or: follow before, after; follow ^people collect { … })
59
+ [ where <predicate> ] where age >= 18 and jobs exists { where not end }
60
+ [ follow <dest>, … ] follow children { depth 4 } (or: follow before, after; follow ^people { where … })
61
61
  [ order by <expr> … ] order by age desc, name
62
62
  [ limit N ] limit 10
63
63
  [ offset N ] offset 20
@@ -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
@@ -194,19 +200,22 @@ const flags = { beta: { on: true }, legacy: { on: false } };
194
200
  oqx`$key values from entries(${flags}) where on`; // ["beta"] (bare `on` reads the value)
195
201
 
196
202
  oqx`name, on: entries(prefs) collect { $key values where $it } from ${users}`; // as a nested receiver
197
- oqx`name from ${users} where entries(prefs) exists { where $key == "dark" && $it }`;
203
+ oqx`name from ${users} where entries(prefs) exists { where $key == "dark" and $it }`;
198
204
  ```
199
205
 
200
206
  `entries(array)` yields numeric index keys, a `Map` yields its entries, and
201
- absence/scalars yield nothing. `$key` exists **only** in an entry scope — an
202
- ordinary row or array element has no implicit index; `entries(arr)` is how you
207
+ absence/scalars yield nothing. An entry's `$key` is its key — and since
208
+ language 0.20 `$key` is the **identity** every row is known by (§9): an
209
+ ordinary row's `$key` is the host's identity, structurally the row itself for
210
+ plain data; an array element has no implicit index — `entries(arr)` is how you
203
211
  ask for one. As a plain value (not a source) `entries(x)` is an array of
204
212
  `{ key, value }` records.
205
213
 
206
214
  ### 3. Predicates (where)
207
215
 
208
216
  `where` filters rows. The predicate language has comparisons (`== != < <= > >=`),
209
- boolean operators (`&& || !`) with grouping `( )`, membership (`in`), arithmetic
217
+ boolean operators — the words `and`, `or`, `not`; the symbols `&& || !` are
218
+ accepted synonyms — with grouping `( )`, membership (`in`), arithmetic
210
219
  (`+ - * / %`), and bare truthiness. The `where` keyword is always written — a
211
220
  leading expression without it is a projection, and a bare comparison in that
212
221
  position is an error that points you at `where`.
@@ -259,9 +268,10 @@ against the value of `x`; a string in `x` can't inject operators or identifiers.
259
268
 
260
269
  **`where` sees the `select` aliases.** A name defined in the same body's
261
270
  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:
271
+ there. The alias's expression is substituted inline before evaluation
272
+ (`resolveAliases`, applied once by the run entry points — the parsed tree keeps
273
+ what you wrote), so it costs nothing at run time and a storage planner still
274
+ sees an ordinary predicate:
265
275
 
266
276
  ```js
267
277
  oqx`select name, adult: age >= 18 from ${people} where adult`; // ≡ where age >= 18
@@ -280,7 +290,11 @@ an alias. `order by` is not rewritten — it reads row fields (§8).
280
290
  against the **current row only**. If the row lacks that property the value is
281
291
  absent — it never falls through to an enclosing row. To correlate with an
282
292
  enclosing scope you say so explicitly with `^name` ("exactly one scope out";
283
- `^^name` for two, and so on):
293
+ `^^name` for two, and so on) — or by **depth** with `N^name` (language 0.18):
294
+ `0^people` is the root scope's `people` from any depth, `1^name` the top-level
295
+ row's `name`, `0^$it` the root object. `N^` is an integer touching a caret
296
+ (`1 ^x` is not one); it is pure sugar for the caret form with `levels = depth −
297
+ N`, and `N ≥ the current depth` is a parse error.
284
298
 
285
299
  ```js
286
300
  const accounts = [
@@ -308,7 +322,7 @@ from a row nested one level deeper. A receiver may start with `^` too, which is
308
322
  how a nested consumer runs over a named root or an enclosing row's relation:
309
323
 
310
324
  ```js
311
- execute("name, peers: ^people collect { name where city == ^city && name != ^name } from people", { people });
325
+ execute("name, peers: ^people collect { name where city == ^city and name != ^name } from people", { people });
312
326
  ```
313
327
 
314
328
  **Correlated subqueries.** Because inner and outer rows often share names, the
@@ -323,7 +337,7 @@ const family = [
323
337
  ];
324
338
  oqx`
325
339
  name,
326
- siblings: ${family} collect { name where parent == ^parent && name != ^name }
340
+ siblings: ${family} collect { name where parent == ^parent and name != ^name }
327
341
  from ${family}
328
342
  `;
329
343
  // [{ name: "Ada", siblings: [{ name: "Ben" }] },
@@ -346,7 +360,7 @@ Methods on a value: `contains`, `startsWith`, `endsWith`, `matches` (regex),
346
360
  ```js
347
361
  oqx`name from ${people} where title.startsWith("Eng")`; // → Bob
348
362
  oqx`name from ${people} where title.lower() == "director"`; // → Alice
349
- oqx`name from ${people} where has(age) && !has(nickname)`; // present vs absent
363
+ oqx`name from ${people} where has(age) and not has(nickname)`; // present vs absent
350
364
  oqx`name from ${people} where tags.contains("admin")`; // array membership
351
365
  ```
352
366
 
@@ -580,8 +594,10 @@ Reached rows expose recursion **intrinsics** in `select` / `order by`:
580
594
  graph beyond — a boundary or the depth cap), `$ordinal` (a deterministic 1..N
581
595
  rank over the walk, ordered by depth then path), and `$stop`
582
596
  (`"interior"` / `"leaf"` / `"frontier"` / `"depth"` / `"cycle"`, precedence
583
- cycle > frontier > depth > leaf > interior — only `interior` rows expand). Options
584
- go in a trailing block:
597
+ cycle > frontier > depth > leaf > interior — only `interior` rows expand). A
598
+ brace after a destination opens that destination's **block** (language 0.20),
599
+ whose body has two clauses of its own after `where`: `depth <n>` and
600
+ `frontier <pred>`:
585
601
 
586
602
  ```js
587
603
  oqx`id, stop: $stop from ${tree} follow children { depth 2 } order by $ordinal`;
@@ -593,10 +609,10 @@ a nested block (`kids: children collect { … }`) a bare `$depth` is the nested
593
609
  row's own `$depth` property (absent unless the data carries one), and the
594
610
  occurrence's depth is `^$depth`. The same holds everywhere the metadata is not
595
611
  present — outside a `follow`, `$depth`/`$ordinal`/`$stop`/`$leaf`/`$frontier`
596
- are ordinary property reads (a row `{ "$depth": 3 }` projects `3`), and `$key`
597
- is likewise an ordinary property read outside an `entries()` scope. Where the
598
- metadata exists it wins over a same-named property (language 0.13; before, the
599
- six names read absent everywhere they were not metadata).
612
+ are ordinary property reads (a row `{ "$depth": 3 }` projects `3`). `$key` is
613
+ the exception: it is the row's identity everywhere (below). Where the metadata
614
+ exists it wins over a same-named property (language 0.13; before, the six names
615
+ read absent everywhere they were not metadata).
600
616
 
601
617
  The walk is **per-path**: a node reached by N distinct paths yields N
602
618
  occurrences, and revisiting an identity already on the current path is admitted
@@ -605,42 +621,159 @@ runaway. `follow distinct` collapses occurrences to reached nodes (the minimal
605
621
  `(depth, path)` per identity).
606
622
 
607
623
  **Destinations** (language 0.14) are comma-separated; the walk is their union.
608
- A **plain destination** is a relation of the current row (`children`, `doc.out`
609
- — an outer reference `^rel` or a literal is a parse error). A **destination
610
- block** is a receiver followed by `collect`, `first` or `single` (optionally
611
- `distinct`) and a block — the same directive you would write in `select`,
612
- re-evaluated per frontier row with `^` bound to that row. Its receiver *may* be
613
- an outer reference, so a walk can compute successors from a named root:
624
+ A **plain destination** is a relation of the current row (`children`, `doc.out`,
625
+ `entries(kids)`, a tuple `(before, after)` — one destination over the union of
626
+ its elements; an outer reference `^rel` on its own or a literal is a parse
627
+ error). A **destination block** is a receiver followed by a block — `children {
628
+ depth 2 }`, `^people { where manager == ^id }`, `(before, after) { depth 2 }`,
629
+ or with its consumer, `^chain first distinct { … }` — the same directive you
630
+ would write in `select`, re-evaluated per frontier row with `^` bound to that
631
+ row. Its receiver *may* be an outer reference, so a walk can compute successors
632
+ from a named root. Since language 0.20 **a brace after any destination opens
633
+ that destination's block** (there is no options block any more):
614
634
 
615
635
  ```js
616
636
  const people = [{ id: 1, manager: null }, { id: 2, manager: 1 }, { id: 3, manager: 1 }, { id: 4, manager: 2 }];
617
- oqx`id values from ${people} where id == 1 follow ^people collect { where manager == ^id } order by $ordinal`;
637
+ oqx`id values from ${people} where id == 1 follow ^people { where manager == ^id } order by $ordinal`;
618
638
  // [1, 2, 3, 4] — the reporting tree under person 1 (`^people` names the root from a top-level walk)
639
+ oqx`id values from ${people} where id == 1 follow ^people { where manager == ^id depth 2 } order by $ordinal`;
640
+ // [1, 2, 3] — `depth` is a clause of the destination block, after its `where`
619
641
 
620
642
  const chain = [{ id: "a" }, { id: "b", prev: "a" }, { id: "c", prev: "b" }];
621
- oqx`id values from ${chain} where id == "b" follow distinct ^chain collect { where id == ^prev }, ^chain collect { where prev == ^id } order by $ordinal`;
643
+ oqx`id values from ${chain} where id == "b" follow distinct ^chain { where id == ^prev }, ^chain { where prev == ^id } order by $ordinal`;
622
644
  // ["b", "a", "c"] — both directions of a chain; a block may order, bound and `distinct` its rows
623
645
  ```
624
646
 
625
- A row's successors are, per destination in order, that destination's rows,
626
- filtered by the follow `where`, then **unioned by identity within the step**: a
627
- node reached by two destinations (or twice by one relation) in the same step is
628
- a single successor. `exists`/`none`/`count` are not destinations (they are
647
+ A row's successors are, per destination in order, that destination's rows
648
+ (shaped by the block's projection, if any), then **unioned by identity within
649
+ the step**: a node reached by two destinations (or twice by one relation) in the
650
+ same step is a single successor, and the destination that reached it first
651
+ governs it. `exists`/`none`/`count` are not destinations (they are
629
652
  where-position tests).
630
653
 
631
- The block accepts: `where <succ>` (which successors keep participating),
632
- `frontier <pred>` (cut a relation that could continue, whichever destination
633
- reached it), `depth <n>` (1–8), and `by <expr>` (the identity used for cycle
634
- detection + `distinct` — default `.id` or the object reference). The follow
635
- `where` is **correlated**: it reads the candidate successor's scope whose parent
636
- is the frontier row's — a bare name is the candidate's own property, `^name` is
637
- the row being expanded, `^^name` the walk's enclosing scope (language 0.14;
638
- before, `^` skipped the frontier row). So `follow doc.in { where
639
- before.contains(^$path) }` steps only into rows that name the row they were
640
- reached from. `frontier` and `by` are not correlated: they read the occurrence
641
- with `^` the enclosing scope, as the body does. Give `follow` a stable identity
642
- (`.id` or `by`) when your relation returns fresh objects rather than shared
643
- references.
654
+ A destination block's body is an ordinary block — `where <succ>` (which
655
+ successors keep participating), a projection, `order by`, `limit`/`offset`,
656
+ `distinct` — plus two clauses after `where`: `frontier <pred>` (an occurrence
657
+ this destination reached for which it holds is a frontier — cut, not expanded)
658
+ and `depth <n>` (1–8: this destination expands a frontier row only while its
659
+ `$depth < n`; a destination block's clause alone). The block's `where` is **correlated**: it reads
660
+ the candidate successor's scope whose parent is the frontier row's — a bare name
661
+ is the candidate's own property, `^name` is the row being expanded, `^^name` the
662
+ walk's enclosing scope (language 0.14; before, `^` skipped the frontier row). So
663
+ `follow doc.in { where before.contains(^$path) }` steps only into rows that
664
+ name the row they were reached from. `frontier` is not correlated: it reads the
665
+ occurrence with `^` the enclosing scope, as the body does; seeds are reached
666
+ through no destination and are never frontiers by a destination's rule. **The
667
+ body that carries the `follow` may write `frontier <pred>` too**, after its
668
+ `where` and before `follow`: it applies to the seeds — a matching seed stays in
669
+ the result with `$stop == "frontier"` and is not expanded (`id from tree frontier
670
+ !active follow children { depth 2 }`); read in the seed's scope like the body's
671
+ `where`. Each destination's `frontier` is its own; without a `follow`, a
672
+ body-level `frontier` is a parse error.
673
+
674
+ **Identity is `$key`** (language 0.20). Cycle detection, the per-step union,
675
+ `follow distinct`, `distinct` over unprojected rows and `is` all compare one
676
+ thing — the identity a row is known by — and reading `$key` on any row returns
677
+ it. The default is the host's (`DataContext.identityOf`), else the row's
678
+ **structural value**; `DefaultContext` names none, so over plain objects two
679
+ objects with the same `id` are two nodes. A block may project `$key: <expr>` to
680
+ give the rows it yields that identity — the body's `select` names the seeds, a
681
+ destination block its successors (where `$key:` is not a column; the rows keep
682
+ their shape):
683
+
684
+ ```js
685
+ const n1 = { id: 1 }, n2 = { id: 2, next: [n1] }; n1.next = [n2]; // a cyclic graph of fresh references
686
+ oqx`id, stop: $stop, $key: id from ${[n1]} follow next { $key: id } order by $ordinal`;
687
+ // [{ id: 1, stop: "interior", $key: 1 }, { id: 2, stop: "interior", $key: 2 }, { id: 1, stop: "cycle", $key: 1 }]
688
+ oqx`name, $key: email from ${people} follow next { $key: email }`; // known by email at every step
689
+ ```
690
+
691
+ Identities are compared as values across the whole walk, so destinations that
692
+ can reach the same rows should project the same `$key`, or a cycle through both
693
+ may run to the depth cap. Give a cyclic graph of fresh objects a `$key` (or a
694
+ context with `identityOf`) rather than rely on the structural default.
695
+
696
+ ### 9b. Cardinality and tuples (0.20)
697
+
698
+ In row position an **array** is a collection and a non-array **object** is one
699
+ row: a block over it yields **one record** (or absent, when the row is filtered
700
+ out) rather than a one-element list, `values` the one value, `first`/`single`
701
+ that object — `doc { title }` is `{ title }`, `doc { status == "active" }` is the
702
+ object or absent (a conditional reach-through), `title from doc` the same record.
703
+ A scalar in row position is an eval error (`list(s)` says "one element"
704
+ explicitly); a `from` re-projection or a `follow` is plural. A host says which
705
+ of its objects are collections through `DataContext.isSingle` (a lazy handle);
706
+ by default a non-iterable object is one row.
707
+
708
+ ```js
709
+ oqx`${settings} { theme }`; // { theme: "dark" } (one object → one record)
710
+ oqx`${[settings]} { theme }`; // [{ theme: "dark" }] (a collection → a list)
711
+ oqx`from list(${settings})`; // [settings] (the explicit one-element collection)
712
+ ```
713
+
714
+ A **tuple** `(e1, e2, …)` — two or more expressions in parentheses — is a value
715
+ and a receiver: its elements concatenated with one level of flattening (a
716
+ collection contributes its items, a row itself; an absent element nothing, a
717
+ `null` is kept; duplicates kept). One parenthesized expression is grouping.
718
+
719
+ ```js
720
+ oqx`n: size((tags, aliases)) from ${people}`; // one count over both lists
721
+ oqx`name from ${people} where "x" in (tags, aliases)`; // membership in the union
722
+ oqx`n: (mom, dad, siblings) { name values } from ${people}`; // one block over the union
723
+ oqx`id from ${g} follow (before, after) { depth 2 }`; // one destination over the union
724
+ ```
725
+
726
+ ### 10. Sugar (0.17)
727
+
728
+ Every form below is shorthand for an explicit directive — it **desugars in the
729
+ parser** to the nodes you already know (`print` writes the explicit form, the
730
+ optimizer and any pushdown planner see it), so nothing here has semantics of its
731
+ own except the postfix `!` and the infix `is`:
732
+
733
+ ```js
734
+ // A receiver block without a consumer is `collect` — wherever a value stands
735
+ // (language 0.20): a select item, a where leaf, a follow destination, a call
736
+ // argument; `x distinct { … }` is `x collect distinct { … }`. A block whose
737
+ // LEADING expression is syntactically a predicate drops `where`.
738
+ oqx`name, current: jobs { !end_date } from ${people}`
739
+ // ≡ name, current: jobs collect { where !end_date } from ${people}
740
+ oqx`${people} { age > 40 }` // ≡ ${people} collect { where age > 40 }
741
+ oqx`n: size(jobs { !end_date }) from ${people}` // a block as a call argument
742
+ oqx`${people} distinct { city }` // ≡ ${people} collect distinct { city }
743
+ oqx`${people} count { jobs exists { } }` // a consumer test is predicate-shaped too
744
+ oqx`${people} first { name }` // a bare name still projects — unchanged
745
+ oqx`${people} first { is active }` // the bare-field filter: `is x` ≡ `!!x`, `not x` ≡ `!x`
746
+
747
+ // Brackets: `[p]` is `first { where p }`, `[n]` is `first { offset n }`;
748
+ // a trailing `!` requires the result (and makes the predicate form `single`).
749
+ oqx`name, boss: ^people[id == ^manager].name from ${people}` // the first match, navigated
750
+ oqx`name, first_job: jobs[0], primary: jobs[pay > 2]! from ${people}`
751
+ // jobs[0] ≡ jobs first { offset 0 } (out of range ⇒ absent)
752
+ // jobs[pay > 2]! ≡ jobs single { where pay > 2 }! (zero or many matches ⇒ an eval error)
753
+
754
+ // Postfix `!` — required: the value, or an eval error naming the expression
755
+ // (and the row's `$key` when scalar). Never a filter, never a coercion: 0! and ""! are values.
756
+ oqx`select id!, title from ${docs}` // insist on an identity before a write
757
+ // Tightest precedence: `refs(c)[0]!.name` requires the lookup, `refs(c)[0].name!` the name.
758
+
759
+ // `is` / `is not` compare IDENTITY (`$key`: the host's, else structural — §9):
760
+ oqx`name from ${people} where manager is null` // absent (same as == null)
761
+ oqx`${people} count { ^people exists { $it is ^$it } }` // rows compare; `==` leaves that unspecified
762
+ // `and` / `or` / `not` are the canonical connectives; `&&` / `||` / `!` are exact
763
+ // synonyms (`title or path` coalesces). `print` writes the words (language 0.18); `!` stays `!`.
764
+
765
+ // A select item may use the items to its left (inlined like a `where` alias):
766
+ oqx`boss: ^people[id == ^manager], bossName: boss.name from ${people}`
767
+ ```
768
+
769
+ Reserved words, never a bare field name: `from where select is not and or` and
770
+ the literal words `true false null`. The top level is unchanged — `age > 15
771
+ from ${people}` stays an error, because `where` would precede `from`. One
772
+ carve-out: after a `follow` destination that is a relation of the current row
773
+ (a bare name, a call, a binding) a brace is still the **options** block (`follow
774
+ children { depth 2 }`), never a collect; only over an outer-reference receiver,
775
+ which cannot be a plain destination, is the brace a destination block with the
776
+ implicit `collect` (`follow ^people { where … }`, language 0.19).
644
777
 
645
778
  ### Cheat-sheet
646
779
 
@@ -648,8 +781,12 @@ references.
648
781
  select … from … where … follow … order by … limit N offset N the fixed clause order (each at most once)
649
782
  name, alias: expr, nested: rel collect { … } projection (`select` may be dropped only here, in first position)
650
783
  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)
784
+ where a == b and rel exists { where … } or not c predicate tree + nested ops (`&&`/`||`/`!` are accepted synonyms)
785
+ rel { pay > 2 } / rel { employer } a block without a consumer is collect; a leading predicate is where-first, a bare name projects
786
+ rel[p] / rel[p]! / rel[0] / rel[0]! first match / exactly one (required) / positional / required positional
787
+ x! required: the value, or an error when absent (tightest; `a!.b` vs `a.b!`)
788
+ is x / not x / x is y / x is not y truthiness (≡ !!x / !x) / identity ($key: the host's, else structural; absent ≡ null)
789
+ 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
790
  where rel none { … } zero rows (≡ !rel exists { … }; "all" = none over the complement)
654
791
  where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl. / open-ended)
655
792
  where x in range(field) coerce a string field to a range, then test coverage
@@ -663,12 +800,87 @@ from entries(obj) … $key / $it a record's properties as a co
663
800
  entries(rel) exists { … } a free-function call may be a receiver
664
801
  order by expr desc, expr2 ordering
665
802
  limit n / offset n bound the row set (after where/order/distinct, before the consumer)
666
- follow rel { where … frontier … depth n by … } recursion ($depth/$stop/$leaf/$frontier); `^name` in the where is the frontier row
667
- follow before, after several destinations: the union of their rows (per step, by identity)
668
- follow ^people collect { where manager == ^id } a destination block: successors computed per frontier row (`^` = that row)
803
+ follow rel { where … frontier … depth n } recursion ($depth/$stop/$leaf/$frontier/$key); the brace is the destination's block, `^name` in its where the frontier row
804
+ follow before, after / follow (before, after) several destinations: the union of their rows (per step, by identity) / one tuple destination
805
+ follow ^people { where manager == ^id } a destination block: successors computed per frontier row (`^` = that row; `collect` implicit, 0.20)
806
+ $key / $key: expr the identity a row is known by (the host's, else structural) / give the rows a block yields that identity
807
+ (a, b) / rel distinct { … } a tuple: the elements, one level flat (a value and a receiver) / collect distinct (0.20)
808
+ obj { title } one object → one record (or absent); a collection → a list; a scalar → an error (0.20)
669
809
  ${source} <collect|exists|none|count|first|single> { … } whole-query consumer
670
810
  ```
671
811
 
812
+ ## The AST: reflecting on a query
813
+
814
+ `parse(source)` returns the query's abstract syntax tree, and since language
815
+ 0.16 that tree is a **public contract** shared by both implementations
816
+ ([`spec/oqx/AST.md`](https://github.com/omgbase/omgbase/tree/main/spec/oqx/AST.md)):
817
+ a tool reflects on a query — which relations it names, which clauses it has,
818
+ where in the text each piece sits — without re-parsing. The objects are plain
819
+ data (no parent pointers, no methods) and *are* the JSON shape.
820
+
821
+ - **Every node has `kind` and `span`.** `span` is `[start, end)` in Unicode
822
+ **code points** over the raw source (for a template, `rawSource(strings)`,
823
+ where a binding occupies its `${n}` marker); `toUtf16(span, source)` converts
824
+ for an editor that counts UTF-16 units. A parenthesized operand spans its
825
+ parentheses; a block (`subquery`) spans its braces.
826
+ - **Optionals are materialized** — `where: null`, `limit: null`, `countCmp: null`,
827
+ `distinct: false`, `values: false` — so nothing has to be defaulted.
828
+ - **`where` keeps its surface form.** A `select` alias used in `where` stays an
829
+ `ident` in the tree; `resolveAliases(query)` is the pure substitution the run
830
+ entry points (`oqx`, `run`, `execute`, `runQuery`) apply exactly once before
831
+ evaluation. An `Engine.run` evaluates the query it is given — if you drive an
832
+ engine yourself, resolve first.
833
+
834
+ ```js
835
+ import { parse, visit, transform, print, printTemplate, toJSON, stripSpans, build } from "@omgbase/oqx";
836
+
837
+ const q = parse('select name, n: jobs collect { employer } from people where jobs exists { where !end }');
838
+
839
+ // visit: every node, with its ancestors, the clause it sits in, and the scope depth
840
+ const relations = [];
841
+ visit(q, {
842
+ enter(node, { clause, depth }) {
843
+ if (node.kind === "op") relations.push({ relation: print(node.receiver), clause, depth });
844
+ },
845
+ });
846
+ // → [{ relation: "jobs", clause: "select", depth: 1 }, { relation: "jobs", clause: "where", depth: 1 }]
847
+
848
+ // transform: rebuild with every expression mapped (children first; untouched nodes keep identity + spans)
849
+ const rewritten = transform(q, (e) => (e.kind === "ident" && e.name === "name" ? build.member(build.ident("person"), "name") : e));
850
+
851
+ // print: the canonical source — single spaces, `select` written, minimal parentheses
852
+ print(rewritten);
853
+ // 'select name: person.name, n: jobs collect { employer: employer } from people where jobs exists { where !end }'
854
+
855
+ toJSON(q); // { oqx: "0.17", kind: "query", span: [0, 93], … } — the shared JSON document
856
+ stripSpans(q); // the same tree without spans, for shape comparisons
857
+ ```
858
+
859
+ `visit(root, { enter?, leave? })` walks depth-first in canonical source order;
860
+ `enter` may return `false` to skip a subtree. Its context is `{ path, clause,
861
+ depth }` — the ancestors, the clause (`source` · `from` · `where` · `select` ·
862
+ `orderBy` · `limit` · `offset` · `follow` · `follow.destination` · `follow.where`
863
+ · `follow.frontier` · `follow.by`; `null` at the root) and the **scope depth**
864
+ the node is evaluated at (0 = the root scope: a top-level source or bound; 1 =
865
+ a top-level row; a block's body is one deeper than its receiver). Both walks
866
+ are driven by one child-key table, `CHILDREN`, so a future field is added in one
867
+ place.
868
+
869
+ `print(node)` is canonical, and the law `stripSpans(parse(print(parse(q))))`
870
+ ≡ `stripSpans(parse(q))` holds for every spec fixture (both runners check it);
871
+ `print(parse(q)) === q` is *not* a law — spelling is normalized. A binding is a
872
+ value, never source text: `print` throws (`OqxError`, stage `"print"`) on one,
873
+ and `printTemplate(node)` returns `{ strings, count, indices }` — the fragments
874
+ around each binding and the binding each gap stands for (the canonical clause
875
+ order can move one past another), to re-run with `parseTemplate(strings,
876
+ count)` and the values permuted by `indices`.
877
+
878
+ `build.*` constructs nodes with the empty span `[0, 0]` and the materialized
879
+ defaults (`build.query(source, { select, where, … })`, `build.op(receiver,
880
+ "collect", build.subquery({ … }))`, `build.field`, `build.collect`,
881
+ `build.follow`, `build.ident`, `build.lit`, `build.binary`, `build.and`, …), so
882
+ a tree you assemble prints with `print` and has the shape of a parsed one.
883
+
672
884
  ## Data context: string queries and named roots
673
885
 
674
886
  When you don't need interpolation, `execute` runs a plain string query against a
@@ -687,7 +899,8 @@ the full discriminated result (`{ consumer, … }`).
687
899
  ## Architecture: adapting to other storage & query systems
688
900
 
689
901
  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.
902
+ objects. The parsed `Query` AST is the host-agnostic contract (above); execution
903
+ is pluggable.
691
904
 
692
905
  ```
693
906
  Query AST ─┬─ InMemoryEngine(DataContext) tier 1/2 — drive any data model
@@ -705,8 +918,13 @@ than approximate it. The conformance suite verifies this.
705
918
 
706
919
  The engine never touches host objects directly; it asks a `DataContext` to
707
920
  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
921
+ optionally to name a row's identity (`identityOf` — `$key`; the default is the
922
+ row's structural value), to say which of its objects are collections rather
923
+ than one row (`isSingle`; by default a non-iterable object is one row, so a
924
+ block over it is one record), and to supply a `rootObject()` — the root
925
+ scope's row, what `$it` is at the root and `^$it` from a top-level row; without
926
+ one the root row is absent. Implement it to query an ORM graph, a remote API,
927
+ or lazily loaded relations — the query *semantics* stay in OQX. Name resolution is simple
710
928
  for a context: a bare `field`, a `.field` segment, and a `^field` outer
711
929
  reference each become one `get(row, key)` against exactly the row of the scope
712
930
  they name, so a computed relation only needs `get` to know about it:
@@ -718,7 +936,7 @@ const graph = {
718
936
  root: (name) => name === "tree" ? [nodes.get(1)] : undefined,
719
937
  get: (row, key) => key === "children" ? row.childIds.map(id => nodes.get(id)) : row[key],
720
938
  toRows: (v) => v == null ? [] : Array.isArray(v) ? v : [v],
721
- identity: (row) => row.id, // for follow dedup
939
+ identityOf: (row) => row.id, // `$key`: for follow dedup, distinct, `is` (optional; default: structural)
722
940
  };
723
941
  run(parse("id, depth: $depth from tree follow children"), { context: graph });
724
942
  ```
@@ -751,7 +969,7 @@ const ctx = {
751
969
  return builtins.callMethod(name, recv, args);
752
970
  },
753
971
  };
754
- run(parse("id from tree where age(born) > 18 && title.slug() == 'x'"), { context: ctx });
972
+ run(parse("id from tree where age(born) > 18 and title.slug() == 'x'"), { context: ctx });
755
973
  ```
756
974
 
757
975
  The simplest route is to `extends DefaultContext` and `super.callFunction(...)`
@@ -777,7 +995,7 @@ import { parse, PlannedEngine } from "@omgbase/oqx";
777
995
  import { SqliteTable } from "@omgbase/oqx/sqlite";
778
996
 
779
997
  const planner = new SqliteTable(db, "emp", { columns: ["id", "name", "dept", "level"] });
780
- new PlannedEngine(planner).run(parse('name from emp where dept == "eng" && level >= 5'), []);
998
+ new PlannedEngine(planner).run(parse('name from emp where dept == "eng" and level >= 5'), []);
781
999
  // → `dept`/`level` pushed to SQL; anything untranslatable finishes in-memory
782
1000
  ```
783
1001
 
@@ -808,7 +1026,7 @@ results and on errors.
808
1026
 
809
1027
  **What is optimized**
810
1028
 
811
- - **Correlated equality → hash probe.** A top-level `&&` conjunct of the block's
1029
+ - **Correlated equality → hash probe.** A top-level `and` conjunct of the block's
812
1030
  `where` of the form `local == outer` (either side) — `local` a bare identifier
813
1031
  or member chain on the block's row (`customer_id`, `meta.id`, `$it`),
814
1032
  `outer` anything that reads nothing from that row (`^customer_id`, `^^x.id`,
@@ -830,7 +1048,7 @@ results and on errors.
830
1048
  by`) are answered from the bucket's size after the bound without entering a
831
1049
  row.
832
1050
 
833
- **The sound rule.** `&&` is strictly left to right and short-circuiting (§13),
1051
+ **The sound rule.** `and` is strictly left to right and short-circuiting (§13),
834
1052
  so a probe may skip a row only if everything the scan would have evaluated for
835
1053
  it could neither raise nor bind: an equality is hoisted only when **every
836
1054
  conjunct to its left is raise-free** — no function or method call, no `single`,
@@ -840,7 +1058,7 @@ too. A conjunct that could raise *right* of the equality is simply evaluated
840
1058
  over the bucket, where the scan would have evaluated it on the same rows. A
841
1059
  block whose `where` lifts anywhere is never probed (a lift could change what
842
1060
  the outer side reads between rows). Otherwise the block scans as before, so
843
- `where nope(x) && id == ^id` still raises `unknown function`.
1061
+ `where nope(x) and id == ^id` still raises `unknown function`.
844
1062
 
845
1063
  Property reads are treated as total (§23: evaluation is otherwise total). A
846
1064
  `DataContext.get` that throws is caught where the engine relies on reads it
@@ -908,7 +1126,8 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
908
1126
  consumer-shaped value: an array for `collect`, a boolean for `exists` / `none`,
909
1127
  a number for `count`, a record or `null` for `first` / `single`.
910
1128
  - `parse(source)` — a query string → reusable `Query` AST (an `OqxError` with
911
- `stage: "lex" | "parse"` on bad input).
1129
+ `stage: "lex" | "parse"` on bad input); `parseTemplate(strings, count)` the
1130
+ tagged-template form, with `binding` nodes for the `${…}` slots.
912
1131
  - `execute(source, roots?)` — parse and run a string against named roots
913
1132
  (`{ people }`), returning the consumer-shaped value.
914
1133
  - `run(query, opts?)` — run a parsed `Query`, returning the full `OqxResult`.
@@ -948,7 +1167,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
948
1167
  (the optimizer rules for nested blocks, default `DEFAULT_RULES`; `[]` is the
949
1168
  naive scan) and `trace` (a `(event: TraceEvent) => void` sink for index /
950
1169
  probe / memo / cardinality / fallback events). See [Performance](#performance-relational-patterns).
951
- - `DataContext` — the tier-2 interface: `root`, `get`, `toRows`, `identity`,
1170
+ - `DataContext` — the tier-2 interface: `root`, `get`, `toRows`, `identityOf`, `isSingle`,
952
1171
  plus optional `callFunction` / `callMethod` (above), an optional
953
1172
  `indexFor(collection, path)` returning a `RowIndex` for a pre-built equality
954
1173
  index (`lookup` → positions; optionally `lookupRows` → rows, probed without
@@ -961,7 +1180,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
961
1180
  "native")`; `BUILTIN_METHODS.matches` is always the baseline.
962
1181
  - `CallResult` — `{ handled: boolean; value?: unknown }`, returned by those hooks.
963
1182
  - `DefaultContext` — `new DefaultContext(roots?, options?)`; plain-object
964
- access, `.id` identity, and the builtin function/method tables. `options`:
1183
+ access, structural identity (`$key` is the row itself unless projected), and the builtin function/method tables. `options`:
965
1184
  `{ regexDialect?: RegexDialect }` (see the regex baseline, §4).
966
1185
  - `compileRegex(pattern, flags?, dialect?)` — the `matches()` compiler: a
967
1186
  `RegExp`, or an `OqxError` for a bad pattern/flag. `RegexDialect` /
@@ -1008,7 +1227,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1008
1227
  import { parse, run, IndexedCollection, PlannedEngine } from "@omgbase/oqx";
1009
1228
  const planner = new IndexedCollection("people", people, ["city", "title"]);
1010
1229
  const engine = new PlannedEngine(planner, planner.context());
1011
- run(parse('name from people where city == "NYC" && age > 30'), { engine });
1230
+ run(parse('name from people where city == "NYC" and age > 30'), { engine });
1012
1231
  // city probe from the index; `age > 30` finished in-memory over the candidates
1013
1232
  ```
1014
1233
 
@@ -1032,11 +1251,30 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1032
1251
  - `map?: (raw) => row` — a custom raw-SQL-row → query-row mapper (overrides
1033
1252
  `jsonColumns`).
1034
1253
 
1254
+ **The AST** (see [above](#the-ast-reflecting-on-a-query); `spec/oqx/AST.md` is normative)
1255
+
1256
+ - `visit(root, { enter?, leave? })` / `transform(root, f)` — the generic walks
1257
+ (`Visitor`, `VisitContext`, `Clause` are the types); `CHILDREN` — the
1258
+ child-key table per node kind that drives them.
1259
+ - `print(node)` / `printTemplate(node)` — the canonical printer (`Template` is
1260
+ `{ strings, count, indices }`).
1261
+ - `resolveAliases(query)` — the `select`-alias substitution the entry points
1262
+ apply before evaluation (SEMANTICS §14).
1263
+ - `toJSON(query)` — the tree stamped with `oqx: LANGUAGE_VERSION`; `stripSpans(node)`
1264
+ — the tree without spans (`Stripped<T>`); `toUtf16(span, source)` and
1265
+ `codePointLength(s)` — code-point ↔ UTF-16 offsets; `rawSource(strings)` —
1266
+ the display source of a template.
1267
+ - `build` — the node builders (`build.query`, `build.op`, `build.subquery`,
1268
+ `build.field`, `build.ident`, …).
1269
+ - `LANGUAGE_VERSION` — the `spec/oqx/VERSION` this implementation conforms to (`"0.17"`).
1270
+
1035
1271
  **Types**
1036
1272
 
1037
1273
  - 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.
1274
+ `Where`, `Expr`, `OpNode`, `SelectItem`, `OrderSpec`, `Follow`,
1275
+ `AstNode`, `AstKind`, `Span`, …) for tools that reflect on queries and
1276
+ planners that walk the tree; `isExpr(node)` tells an expression from the
1277
+ structural nodes.
1040
1278
 
1041
1279
  ## Requirements
1042
1280