@omgbase/oqx 0.24.0 → 0.26.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 (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +267 -202
  3. package/dist/ast.d.ts +19 -1
  4. package/dist/ast.d.ts.map +1 -1
  5. package/dist/ast.js.map +1 -1
  6. package/dist/build.d.ts +3 -1
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +4 -2
  9. package/dist/build.js.map +1 -1
  10. package/dist/engine.d.ts +2 -0
  11. package/dist/engine.d.ts.map +1 -1
  12. package/dist/engine.js +95 -44
  13. package/dist/engine.js.map +1 -1
  14. package/dist/optimize/analysis.d.ts +3 -2
  15. package/dist/optimize/analysis.d.ts.map +1 -1
  16. package/dist/optimize/analysis.js +12 -2
  17. package/dist/optimize/analysis.js.map +1 -1
  18. package/dist/optimize/ir.d.ts +4 -1
  19. package/dist/optimize/ir.d.ts.map +1 -1
  20. package/dist/optimize/ir.js +1 -0
  21. package/dist/optimize/ir.js.map +1 -1
  22. package/dist/optimize/rules.d.ts.map +1 -1
  23. package/dist/optimize/rules.js +3 -2
  24. package/dist/optimize/rules.js.map +1 -1
  25. package/dist/parser.d.ts.map +1 -1
  26. package/dist/parser.js +206 -42
  27. package/dist/parser.js.map +1 -1
  28. package/dist/print.d.ts +2 -2
  29. package/dist/print.d.ts.map +1 -1
  30. package/dist/print.js +88 -25
  31. package/dist/print.js.map +1 -1
  32. package/dist/references.d.ts.map +1 -1
  33. package/dist/references.js +39 -1
  34. package/dist/references.js.map +1 -1
  35. package/dist/resolve.d.ts.map +1 -1
  36. package/dist/resolve.js +9 -2
  37. package/dist/resolve.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/walk.d.ts +1 -1
  41. package/dist/walk.d.ts.map +1 -1
  42. package/dist/walk.js +5 -0
  43. package/dist/walk.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/ast.ts +20 -1
  46. package/src/build.ts +5 -3
  47. package/src/engine.ts +89 -39
  48. package/src/optimize/analysis.ts +9 -2
  49. package/src/optimize/ir.ts +5 -1
  50. package/src/optimize/rules.ts +3 -2
  51. package/src/parser.ts +193 -41
  52. package/src/print.ts +92 -30
  53. package/src/references.ts +34 -2
  54. package/src/resolve.ts +10 -3
  55. package/src/version.ts +1 -1
  56. package/src/walk.ts +6 -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.17"`); the patch digit is this implementation's own.
19
+ > `"0.26"`); 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
 
@@ -32,9 +32,10 @@ const people = [
32
32
  const company = "Globocorp";
33
33
 
34
34
  const employees = oqx`
35
- name, id, title
36
- from ${people}
37
- where jobs exists { where employer == ${company} and not end_date }
35
+ ${people} {
36
+ select name, id, title
37
+ where jobs exists { employer == ${company} and not end_date }
38
+ }
38
39
  `;
39
40
  // → [{ name: "Bob", id: 124, title: "Engineer" }, …] (current Globocorp employees)
40
41
  ```
@@ -42,42 +43,63 @@ const employees = oqx`
42
43
  ## Why a tagged template
43
44
 
44
45
  Interpolations cross the host/OQX boundary as **typed value bindings, never as
45
- source text** — prepared-statement semantics. A `${…}` in `from` position is the
46
- collection being queried; a `${…}` in a predicate is an ordinary host value.
46
+ source text** — prepared-statement semantics. A `${…}` heading the body
47
+ (`${people} { … }`) is the collection being queried; a `${…}` in a predicate is an ordinary host value.
47
48
  Because values are never spliced into the query text, they cannot alter the
48
49
  grammar and there is no injection surface. The compiled query is cached by the
49
50
  template's identity and re-runs with fresh bindings each call.
50
51
 
51
52
  ## Language tutorial
52
53
 
53
- A query is a list of clauses in a **fixed order**. Each clause appears at most
54
- once, and every one is optional except `from` at the top level:
54
+ A query names its **source collection** and then, in braces, its **body** — a
55
+ list of clauses in a **fixed order**. Each clause appears at most once, and
56
+ every one is optional:
55
57
 
56
58
  ```
57
- [ [select] projection ] name, id, title: label (or: select distinct …, … values)
58
- from <collection> from ${people}
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
- [ group by <key>, … ] group by city, decade: (age - age % 10) / 10 (0.24; then select reads keys, aggregates, rows)
62
- [ having <predicate> ] having count() > 1
63
- [ order by <expr> … ] order by age desc, name
64
- [ limit N ] limit 10
65
- [ offset N ] offset 20
59
+ <collection> { … } ${people} { … } (a binding, a named root, a navigation or a call may head the body)
60
+ <collection> count { … } a consumer word before the brace reduces the whole query (count, exists, none, first, single)
61
+
62
+ [ [select] projection ] name, id, title: label (or: select distinct …, … values)
63
+ [ from <expr> ] from jobs (re-projection: each row replaced by the rows `jobs` yields — never the source)
64
+ [ where <predicate> ] where age >= 18 and jobs exists { not end }
65
+ [ follow <dest>, … ] follow children { depth 4 } (or: follow before, after; follow ^people { where … })
66
+ [ flatten <expr> … ] flatten tags as t (0.25; the one clause that may repeat)
67
+ [ group by <key>, … ] group by city, decade: (age - age % 10) / 10 (0.24; then select reads keys, aggregates, rows)
68
+ [ having <predicate> ] having count() > 1
69
+ [ order by <expr> … ] order by age desc, name
70
+ [ limit N ] limit 10
71
+ [ offset N ] offset 20
66
72
  ```
67
73
 
74
+ **The collection heads the body, and nothing follows the closing brace.** The
75
+ collection before the brace supplies the rows, so a body never names its
76
+ source; the one clause that still spells `from` is the **re-projection** —
77
+ `${people} { from jobs where !end }` is every current job across all people.
78
+ The same body grammar applies inside every nested block (`jobs { … }`, `jobs
79
+ exists { … }`, …): a block's receiver is its source exactly as the whole
80
+ query's is.
81
+
68
82
  **`select` is the only keyword you may drop, and only when the projection comes
69
- first** (`name, id from ${people}`). Every other clause always carries its
70
- keyword — with one piece of sugar (0.17): a **block** whose leading expression
71
- is syntactically a predicate is a `where`-first body (`jobs exists { !end }` ≡
72
- `jobs exists { where !end }`; a bare name still projects, so `jobs { employer }`
73
- projects and `jobs { is current }` filters — see §10). At the top level a
74
- predicate always needs `where`, and `from ${people} count` is an error rather
75
- than a projection of a field called `count`. Writing a clause out of
76
- order is a parse error that names the order (`` `select` must come before
77
- `from` — OQX clause order is select, from, where, frontier, follow, group by,
78
- having, order by, limit, offset ``). The same body grammar applies inside every consumer block
79
- (`collect { … }`, `exists { … }`, …), where `from` is optional because the
80
- receiver supplies the rows.
83
+ first** (`${people} { name, id }`). Every other clause always carries its
84
+ keyword — with one piece of sugar (0.17): a body whose leading expression
85
+ is syntactically a predicate is a `where`-first body (`${people} { age > 40 }` ≡
86
+ `${people} { where age > 40 }`, `jobs exists { !end }` ≡ `jobs exists { where
87
+ !end }`; a bare name still projects, so `${people} { active }` selects the field
88
+ `active` and `${people} { is active }` — or `{ where active }` — filters; see
89
+ §10). A consumer is a word before the brace, never a projection: `${people}
90
+ count { … }` counts, `${people} { count }` projects a field called `count`.
91
+ Writing a clause out of order is a parse error that names the order (`` `select`
92
+ must come before `from` — OQX clause order is select, from, where, frontier,
93
+ follow, flatten, group by, having, order by, limit, offset ``).
94
+
95
+ **The clause form** — `select name, id from ${people} where …`, the source
96
+ written as a `from` clause — is the pre-0.26 spelling. It is still accepted and
97
+ parses to the identical tree, so existing queries keep working; `print` no
98
+ longer writes it (it renders every query collection-scoped), and this README
99
+ teaches the collection-scoped form throughout. In that form the top-level
100
+ `from` is required, a predicate there always needs `where` (`age > 15 from
101
+ ${people}` is an error, because `where` would precede `from`), and `from
102
+ ${people} count` is an error that points at `${people} count { … }`.
81
103
 
82
104
  The examples below all use this dataset:
83
105
 
@@ -94,14 +116,15 @@ const people = [
94
116
  ];
95
117
  ```
96
118
 
97
- ### 1. Source: `from`
119
+ ### 1. Source: the collection before the brace
98
120
 
99
- Every query reads from a source collection. In the tagged template the source is
100
- normally an interpolated value; it may also be a named root or a navigation (see
121
+ Every query reads from a source collection, written before its body. In the
122
+ tagged template the source is normally an interpolated value; it may also be a
123
+ named root, a navigation or a free-function call (see
101
124
  [data context](#data-context-string-queries-and-named-roots)).
102
125
 
103
126
  ```js
104
- oqx`name from ${people}`;
127
+ oqx`${people} { select name }`;
105
128
  // [{ name: "Bob" }, { name: "Alice" }, { name: "Carol" }]
106
129
  ```
107
130
 
@@ -110,10 +133,10 @@ oqx`name from ${people}`;
110
133
  List the fields to keep. With **no** projection you get the raw rows unchanged.
111
134
 
112
135
  ```js
113
- oqx`name, id from ${people}`;
136
+ oqx`${people} { select name, id }`;
114
137
  // [{ name: "Bob", id: 124 }, { name: "Alice", id: 7 }, { name: "Carol", id: 55 }]
115
138
 
116
- oqx`from ${people} where active`; // no projection → whole objects
139
+ oqx`${people} { where active }`; // no projection → whole objects
117
140
  // [ <Bob>, <Alice> ]
118
141
  ```
119
142
 
@@ -123,10 +146,10 @@ A projection item can be:
123
146
  - a **dotted navigation**, keyed by its last segment — `meta.slug` produces
124
147
  `{ slug: … }`;
125
148
  - an **alias / value expression** — `label: name`, `decade: age / 10`;
126
- - a **nested collection** — `current: jobs collect { … }` (see §6).
149
+ - a **nested collection** — `current: jobs { … }` (see §6).
127
150
 
128
151
  ```js
129
- oqx`label: name, decade: age / 10 from ${people} where name == "Bob"`;
152
+ oqx`${people} { select label: name, decade: age / 10 where name == "Bob" }`;
130
153
  // [{ label: "Bob", decade: 4.1 }]
131
154
  ```
132
155
 
@@ -135,24 +158,24 @@ no natural key, so it must be aliased (`n: size(jobs)`), unless the projection i
135
158
  in `values` mode (next).
136
159
 
137
160
  The `select` keyword may be dropped when the projection is the first clause —
138
- `select name from …` is identical to `name from …`. The keyword form hosts
139
- `select distinct` (§6), and is the only way to write a projection whose first
140
- item would otherwise be misread (there is no other keyword-less clause). The
141
- projection always comes *before* `from`: `from ${people} select name` is an
142
- ordering error.
161
+ `${people} { select name }` is identical to `${people} { name }`. The keyword
162
+ form hosts `select distinct` (§6), and is the only way to write a projection
163
+ whose first item would otherwise be read as a predicate (§10; there is no other
164
+ keyword-less clause). The projection always comes *first* in a body:
165
+ `${people} { where active select name }` is an ordering error.
143
166
 
144
167
  **`values` — scalar projection.** Ordinarily every row projects to a record. Add
145
168
  `values` after a projection of exactly **one** item to get the value itself:
146
169
 
147
170
  ```js
148
- oqx`name values from ${people}`; // ["Bob", "Alice", "Carol"]
149
- oqx`name.upper() values from ${people} where age < 30`; // ["CAROL"] (no alias needed)
150
- oqx`${people} first { name values where age > 50 }`; // "Alice"
171
+ oqx`${people} { select name values }`; // ["Bob", "Alice", "Carol"]
172
+ oqx`${people} { select name.upper() values where age < 30 }`; // ["CAROL"] (no alias needed)
173
+ oqx`${people} first { name values where age > 50 }`; // "Alice"
151
174
  ```
152
175
 
153
176
  `values` is a result-shape mode, not a consumer: it works in the top-level
154
177
  projection and inside any `collect` / `first` / `single` block, and composes with
155
- `distinct` (`select distinct employer values from ${jobs}` is the distinct set of
178
+ `distinct` (`${jobs} { select distinct employer values }` is the distinct set of
156
179
  employers as strings, not `{ employer }` records). An alias, if present, is
157
180
  ignored; a lift (`^name:`) cannot be combined with it.
158
181
 
@@ -165,14 +188,14 @@ first-class:
165
188
 
166
189
  ```js
167
190
  const scores = [10, 60, 70, 45];
168
- oqx`$it values from ${scores} where $it > 50`; // [60, 70]
169
- oqx`$it values from ${scores} order by $it desc`; // [70, 60, 45, 10]
191
+ oqx`${scores} { select $it values where $it > 50 }`; // [60, 70]
192
+ oqx`${scores} { select $it values order by $it desc }`; // [70, 60, 45, 10]
170
193
 
171
194
  const players = [{ name: "Ann", scores: [10, 60, 70] }, { name: "Ben", scores: [45] }];
172
- oqx`name, big: scores collect { $it values where $it > 50 } from ${players}`;
195
+ oqx`${players} { select name, big: scores { $it values where $it > 50 } }`;
173
196
  // [{ name: "Ann", big: [60, 70] }, { name: "Ben", big: [] }]
174
197
 
175
- oqx`employee: $it from ${people} where name == "Bob"`; // [{ employee: <Bob> }]
198
+ oqx`${people} { select employee: $it where name == "Bob" }`; // [{ employee: <Bob> }]
176
199
  ```
177
200
 
178
201
  Inside a nested block `$it` is the inner item; the enclosing row is `^$it`
@@ -187,22 +210,22 @@ reads absent. A row that itself owns a `$it` property is shadowed by the
187
210
  intrinsic; reach it with `^` from a nested scope or rename it upstream.
188
211
 
189
212
  **`entries(x)` and `$key` — records to collections, explicitly.** A plain object
190
- is **not** iterable: `from ${obj}` is one row (the object). `entries(obj)`
213
+ is **not** iterable: `${obj} { … }` is one row (the object). `entries(obj)`
191
214
  converts it into a collection of entries, and inside such a scope the current
192
215
  item is the property's **value** — `$it` and bare names read it — while
193
216
  **`$key`** is the property's key:
194
217
 
195
218
  ```js
196
219
  const settings = { theme: "dark", fontSize: 14, autosave: true };
197
- oqx`key: $key, value: $it from entries(${settings})`;
220
+ oqx`entries(${settings}) { select key: $key, value: $it }`;
198
221
  // [{ key: "theme", value: "dark" }, { key: "fontSize", value: 14 }, { key: "autosave", value: true }]
199
- oqx`$key values from entries(${settings}) where $it != "dark"`; // ["fontSize", "autosave"]
222
+ oqx`entries(${settings}) { select $key values where $it != "dark" }`; // ["fontSize", "autosave"]
200
223
 
201
224
  const flags = { beta: { on: true }, legacy: { on: false } };
202
- oqx`$key values from entries(${flags}) where on`; // ["beta"] (bare `on` reads the value)
225
+ oqx`entries(${flags}) { select $key values where on }`; // ["beta"] (bare `on` reads the value)
203
226
 
204
- oqx`name, on: entries(prefs) collect { $key values where $it } from ${users}`; // as a nested receiver
205
- oqx`name from ${users} where entries(prefs) exists { where $key == "dark" and $it }`;
227
+ oqx`${users} { select name, on: entries(prefs) { $key values where $it } }`; // as a nested receiver
228
+ oqx`${users} { select name where entries(prefs) exists { $key == "dark" and $it } }`;
206
229
  ```
207
230
 
208
231
  `entries(array)` yields numeric index keys, a `Map` yields its entries, and
@@ -218,15 +241,16 @@ ask for one. As a plain value (not a source) `entries(x)` is an array of
218
241
  `where` filters rows. The predicate language has comparisons (`== != < <= > >=`),
219
242
  boolean operators — the words `and`, `or`, `not`; the symbols `&& || !` are
220
243
  accepted synonyms — with grouping `( )`, membership (`in`), arithmetic
221
- (`+ - * / %`), and bare truthiness. The `where` keyword is always written — a
222
- leading expression without it is a projection, and a bare comparison in that
223
- position is an error that points you at `where`.
244
+ (`+ - * / %`), and bare truthiness. With a projection the `where` keyword is
245
+ written (`{ name where age >= 18 }`); a body that is *only* a predicate may lead
246
+ with it (`${people} { age >= 18 }`, §10) — but a leading bare name is a
247
+ projection, so the bare-field filter is `{ is active }` or `{ where active }`.
224
248
 
225
249
  ```js
226
250
  const min = 40;
227
- oqx`name from ${people} where age >= ${min}`; // → Bob, Alice
228
- oqx`name from ${people} where city in ${["SF", "LA"]}`; // → Alice
229
- oqx`name from ${people} where !active`; // → Carol
251
+ oqx`${people} { select name where age >= ${min} }`; // → Bob, Alice
252
+ oqx`${people} { select name where city in ${["SF", "LA"]} }`; // → Alice
253
+ oqx`${people} { select name where !active }`; // → Carol
230
254
  ```
231
255
 
232
256
  Equality is **typed and strict** (`5 == "5"` is false); a comparison against an
@@ -247,11 +271,11 @@ ordering rules as `<`/`<=`, so ranges work over numbers and over ISO-8601
247
271
  date/time strings alike:
248
272
 
249
273
  ```js
250
- oqx`name from ${people} where age in 40..50`; // 40 ≤ age ≤ 50
251
- oqx`name from ${people} where age in 40...50`; // 40 ≤ age < 50 (excludes 50)
252
- oqx`name from ${people} where age in 40..`; // 40 and up
253
- oqx`name from ${people} where age in ..29`; // up to and including 29
254
- oqx`label from ${events} where on in "2026-01-01".."2026-03-31"`; // dates in Q1
274
+ oqx`${people} { select name where age in 40..50 }`; // 40 ≤ age ≤ 50
275
+ oqx`${people} { select name where age in 40...50 }`; // 40 ≤ age < 50 (excludes 50)
276
+ oqx`${people} { select name where age in 40.. }`; // 40 and up
277
+ oqx`${people} { select name where age in ..29 }`; // up to and including 29
278
+ oqx`${events} { select label where on in "2026-01-01".."2026-03-31" }`; // dates in Q1
255
279
  ```
256
280
 
257
281
  Bounds may be interpolated (`where age in ${lo}..${hi}`). A range membership is
@@ -276,9 +300,9 @@ what you wrote), so it costs nothing at run time and a storage planner still
276
300
  sees an ordinary predicate:
277
301
 
278
302
  ```js
279
- oqx`select name, adult: age >= 18 from ${people} where adult`; // ≡ where age >= 18
280
- oqx`select name, active: age > 50 from ${people} where active`; // the ALIAS, not the field → Alice
281
- oqx`select name, current: jobs collect { employer where !end } from ${people} where current`;
303
+ oqx`${people} { select name, adult: age >= 18 where adult }`; // ≡ where age >= 18
304
+ oqx`${people} { select name, active: age > 50 where active }`; // the ALIAS, not the field → Alice
305
+ oqx`${people} { select name, current: jobs { employer where !end } where current }`;
282
306
  // a collect alias in predicate position means "non-empty" → Bob, Carol
283
307
  ```
284
308
 
@@ -303,10 +327,10 @@ const accounts = [
303
327
  { owner: "x", budget: 100, orders: [{ amount: 50 }, { amount: 150 }] },
304
328
  { owner: "y", budget: 200, orders: [{ amount: 250 }] },
305
329
  ];
306
- oqx`owner from ${accounts} where orders exists { where amount > ^budget }`;
330
+ oqx`${accounts} { select owner where orders exists { amount > ^budget } }`;
307
331
  // [{ owner: "x" }, { owner: "y" }] — `^budget` is the enclosing account's budget
308
332
 
309
- oqx`owner from ${accounts} where orders exists { where amount > budget }`;
333
+ oqx`${accounts} { select owner where orders exists { amount > budget } }`;
310
334
  // [] — a bare `budget` is the ORDER's own budget: absent, so `>` is false
311
335
  ```
312
336
 
@@ -324,7 +348,7 @@ from a row nested one level deeper. A receiver may start with `^` too, which is
324
348
  how a nested consumer runs over a named root or an enclosing row's relation:
325
349
 
326
350
  ```js
327
- execute("name, peers: ^people collect { name where city == ^city and name != ^name } from people", { people });
351
+ execute("people { select name, peers: ^people { name where city == ^city and name != ^name } }", { people });
328
352
  ```
329
353
 
330
354
  **Correlated subqueries.** Because inner and outer rows often share names, the
@@ -338,9 +362,10 @@ const family = [
338
362
  { name: "Cy", parent: "Sam" },
339
363
  ];
340
364
  oqx`
341
- name,
342
- siblings: ${family} collect { name where parent == ^parent and name != ^name }
343
- from ${family}
365
+ ${family} {
366
+ select name,
367
+ siblings: ${family} { name where parent == ^parent and name != ^name }
368
+ }
344
369
  `;
345
370
  // [{ name: "Ada", siblings: [{ name: "Ben" }] },
346
371
  // { name: "Ben", siblings: [{ name: "Ada" }] },
@@ -360,10 +385,10 @@ Methods on a value: `contains`, `startsWith`, `endsWith`, `matches` (regex),
360
385
  `has(x)`, `range(s)` (§3), `entries(x)` (§2).
361
386
 
362
387
  ```js
363
- oqx`name from ${people} where title.startsWith("Eng")`; // → Bob
364
- oqx`name from ${people} where title.lower() == "director"`; // → Alice
365
- oqx`name from ${people} where has(age) and not has(nickname)`; // present vs absent
366
- oqx`name from ${people} where tags.contains("admin")`; // array membership
388
+ oqx`${people} { select name where title.startsWith("Eng") }`; // → Bob
389
+ oqx`${people} { select name where title.lower() == "director" }`; // → Alice
390
+ oqx`${people} { select name where has(age) and not has(nickname) }`; // present vs absent
391
+ oqx`${people} { select name where tags.contains("admin") }`; // array membership
367
392
  ```
368
393
 
369
394
  - `has(x)` is true when `x` is **present** — anything other than `null` /
@@ -385,9 +410,9 @@ oqx`name from ${people} where tags.contains("admin")`; // array membership
385
410
  (`not a ~ b` is `(!a) ~ b`; write `a !~ b` or `not (a ~ b)`).
386
411
 
387
412
  ```js
388
- oqx`name from ${people} where title ~ "^Eng"`; // → Bob
389
- oqx`name from ${people} where title ~* "director"`; // → Alice
390
- oqx`name from ${people} where title !~ "Eng"`; // → Alice, Carol
413
+ oqx`${people} { select name where title ~ "^Eng" }`; // → Bob
414
+ oqx`${people} { select name where title ~* "director" }`; // → Alice
415
+ oqx`${people} { select name where title !~ "Eng" }`; // → Alice, Carol
391
416
  ```
392
417
 
393
418
  Anything not in these tables is an **eval error** (`unknown function 'f(…)'` /
@@ -418,11 +443,11 @@ baseline is `invalid regular expression …`; a bad flag is `unknown regex flag`
418
443
  `RegExp` sees it (`src/regex.ts`), so JavaScript-only behavior never leaks.
419
444
 
420
445
  ```js
421
- oqx`name from ${people} where title.matches("^(eng|dir)", "i")`; // Bob, Alice
422
- oqx`x: "a\nb".matches("a.b") from ${one}`; // false: . stops at \n
423
- oqx`x: "a\nb".matches("a.b", "s") from ${one}`; // true
424
- oqx`x: "٣".matches(${"\\d"}) from ${one}`; // false: \d is ASCII
425
- oqx`x: "é".matches(${"^\\u{e9}$"}) from ${one}`; // true
446
+ oqx`${people} { select name where title.matches("^(eng|dir)", "i") }`; // Bob, Alice
447
+ oqx`${one} { select x: "a\nb".matches("a.b") }`; // false: . stops at \n
448
+ oqx`${one} { select x: "a\nb".matches("a.b", "s") }`; // true
449
+ oqx`${one} { select x: "٣".matches(${"\\d"}) }`; // false: \d is ASCII
450
+ oqx`${one} { select x: "é".matches(${"^\\u{e9}$"}) }`; // true
426
451
  ```
427
452
 
428
453
  A pattern with backslashes is easiest to pass as a binding (`${"\\d"}`): the
@@ -447,11 +472,11 @@ A consumer shapes a result set. There are six:
447
472
  | `first` | one record, or `null` |
448
473
  | `single` | one record, or `null`; throws if more than one matches |
449
474
 
450
- The bare `from … ` form is always `collect`. To reduce the **whole** query with a
451
- different consumer, use the directive form `<source> <consumer> { <body> }` — note
452
- this is *not* SQL: `count from people` projects a field called `count`, and
453
- `from people count` is a parse error that points at the directive form. A real
454
- reduction is a directive:
475
+ A bare `${people} { … }` is always `collect`. To reduce the **whole** query with
476
+ a different consumer, put the consumer word between the collection and the brace
477
+ — `<source> <consumer> { <body> }` — note this is *not* SQL: `${people} { count }`
478
+ projects a field called `count` (and in the clause form `from people count` is a
479
+ parse error that points here). A real reduction is a directive:
455
480
 
456
481
  ```js
457
482
  oqx`${people} exists { where active }`; // true
@@ -459,11 +484,12 @@ oqx`${people} count { where active }`; // 2
459
484
  oqx`${people} first { name where age > 50 }`; // { name: "Alice" }
460
485
  ```
461
486
 
462
- (A consumer block follows the same clause grammar as the top level, with `from`
463
- optional. A leading bare identifier **projects** — `count { active }` selects a
464
- field named `active`; write `count { where active }` to filter, and always write
465
- `where` before a predicate: `exists { !end }` is an error, `exists { where !end }`
466
- is the test.)
487
+ (A consumer block follows the same clause grammar as every body — the receiver
488
+ supplies the rows, so a `from` inside it re-projects rather than naming a
489
+ source. A leading bare identifier **projects** — `count { active }` selects a
490
+ field named `active`; write `count { where active }` or `count { is active }` to
491
+ filter. A leading predicate-shaped expression is the filter without the
492
+ keyword: `exists { !end }` ≡ `exists { where !end }`, §10.)
467
493
 
468
494
  ### 6. Nested collections and relations
469
495
 
@@ -474,39 +500,40 @@ In `where`, an `exists { … }` tests non-emptiness and `count { … } <op> N` c
474
500
  cardinality:
475
501
 
476
502
  ```js
477
- oqx`name from ${people} where jobs exists { where !end }`; // has a current job → Bob, Carol
478
- oqx`name from ${people} where jobs count {} >= 2`; // ≥2 jobs → Bob, Alice
479
- oqx`name from ${people} where jobs none { where end }`; // no past job → Carol
503
+ oqx`${people} { select name where jobs exists { !end } }`; // has a current job → Bob, Carol
504
+ oqx`${people} { select name where jobs count >= 2 }`; // ≥2 jobs → Bob, Alice
505
+ oqx`${people} { select name where jobs none { where end } }`; // no past job → Carol
480
506
  ```
481
507
 
482
508
  `none { … }` is exactly `!… exists { … }`, kept as its own word because the
483
509
  cardinality is the point. It is also how you say "every": *all* members active is
484
- `members none { where !active }` — there is deliberately no `all { … }`, whose
510
+ `members none { !active }` — there is deliberately no `all { … }`, whose
485
511
  block would have to mean something different from every other consumer's.
486
512
 
487
- In a projection, `collect` yields a nested array; `first` / `single` yield one
488
- nested record:
513
+ In a projection, a bare block (`collect`) yields a nested array; `first` /
514
+ `single` yield one nested record:
489
515
 
490
516
  ```js
491
- oqx`name, current: jobs collect { employer where !end } from ${people} where name == "Bob"`;
517
+ oqx`${people} { select name, current: jobs { employer where !end } where name == "Bob" }`;
492
518
  // [{ name: "Bob", current: [{ employer: "Globocorp" }] }]
493
519
 
494
- oqx`name, firstJob: jobs first { employer } from ${people} where name == "Alice"`;
520
+ oqx`${people} { select name, firstJob: jobs first { employer } where name == "Alice" }`;
495
521
  // [{ name: "Alice", firstJob: { employer: "Initech" } }]
496
522
  ```
497
523
 
498
524
  A relation is just an expression evaluated on the row and coerced to a collection,
499
525
  so nested blocks compose to any depth and can navigate dotted paths
500
- (`author.books collect { … }`).
526
+ (`author.books { … }`).
501
527
 
502
528
  **`distinct`** dedups the rows a consumer sees by their **projected value**, so
503
529
  counts and collections are over distinct projections rather than raw rows. Spell
504
- it after the consumer (`count distinct { … }`) or inside via `select distinct`:
530
+ it after the consumer (`count distinct { … }`, `jobs distinct { … }` for a
531
+ `collect`) or inside via `select distinct`:
505
532
 
506
533
  ```js
507
- oqx`select distinct employer from ${jobs}`; // distinct employers
508
- oqx`n: jobs collect distinct { select employer } from ${people}`; // per person, unique employers
509
- oqx`name from ${people} where jobs count distinct { select employer } == 1`; // worked at exactly one employer
534
+ oqx`${jobs} { select distinct employer }`; // distinct employers
535
+ oqx`${people} { select n: jobs distinct { select employer } }`; // per person, unique employers
536
+ oqx`${people} { select name where jobs count distinct { select employer } == 1 }`; // worked at exactly one employer
510
537
  ```
511
538
 
512
539
  An empty projection dedups by row identity (`count distinct { }` = distinct rows).
@@ -514,15 +541,16 @@ An empty projection dedups by row identity (`count distinct { }` = distinct rows
514
541
  ### 7. Lifts (`^`)
515
542
 
516
543
  Sometimes you want to filter by a nested collection *and* keep a value from it.
517
- A `^name:` item inside a `collect { … }` that sits directly in the top-level
518
- `where` does both: it filters (non-empty) and binds `name` into the outer
544
+ A `^name:` item inside a `{ … }` block (a `collect`) that sits directly in the
545
+ top-level `where` does both: it filters (non-empty) and binds `name` into the outer
519
546
  projection as a per-row array.
520
547
 
521
548
  ```js
522
549
  oqx`
523
- name, currentEmployers
524
- from ${people}
525
- where jobs collect { ^currentEmployers: employer where !end }
550
+ ${people} {
551
+ select name, currentEmployers
552
+ where jobs { ^currentEmployers: employer where !end }
553
+ }
526
554
  `;
527
555
  // [{ name: "Bob", currentEmployers: ["Globocorp"] },
528
556
  // { name: "Carol", currentEmployers: ["Globocorp"] }]
@@ -541,9 +569,10 @@ const departments = [
541
569
  { name: "Sales", teams: [{ id: "t3", members: [{ name: "Dee" }] }] },
542
570
  ];
543
571
  oqx`
544
- name, teamIds, allMembers
545
- from ${departments}
546
- where teams collect { ^teamIds: id where members collect { ^^allMembers: name } }
572
+ ${departments} {
573
+ select name, teamIds, allMembers
574
+ where teams { ^teamIds: id where members { ^^allMembers: name } }
575
+ }
547
576
  `;
548
577
  // [{ name: "Eng", teamIds: ["t1", "t2"], allMembers: ["Ada", "Ben", "Cy"] },
549
578
  // { name: "Sales", teamIds: ["t3"], allMembers: ["Dee"] }]
@@ -562,7 +591,7 @@ intrinsics) — it is not rewritten against the `select` aliases, so
562
591
  `order by decade` sorts by a field called `decade`, not by `decade: age / 10`.
563
592
 
564
593
  ```js
565
- oqx`name from ${people} where city == "NYC" order by age desc`;
594
+ oqx`${people} { select name where city == "NYC" order by age desc }`;
566
595
  // [{ name: "Bob" }, { name: "Carol" }]
567
596
  ```
568
597
 
@@ -577,10 +606,10 @@ outer reference — `limit ^n` reads the enclosing row's `n`, as `^` does everyw
577
606
  inside `{ … }`; it must be a non-negative integer.
578
607
 
579
608
  ```js
580
- oqx`name values from ${people} order by age desc limit 2`; // ["Alice", "Bob"]
581
- oqx`name values from ${people} order by age desc limit 1 offset 1`; // ["Bob"]
582
- oqx`name, latest: jobs collect { employer values order by start desc limit 1 } from ${people}`;
583
- oqx`name from ${people} where jobs exists { offset 1 }`; // has a second job
609
+ oqx`${people} { select name values order by age desc limit 2 }`; // ["Alice", "Bob"]
610
+ oqx`${people} { select name values order by age desc limit 1 offset 1 }`; // ["Bob"]
611
+ oqx`${people} { select name, latest: jobs { employer values order by start desc limit 1 } }`;
612
+ oqx`${people} { select name where jobs exists { offset 1 } }`; // has a second job
584
613
  ```
585
614
 
586
615
  A storage adapter that pushes the whole query may translate them to SQL
@@ -595,16 +624,16 @@ aliased expression (`decade: (age - age % 10) / 10`); key values compare as
595
624
  identities (absent ≡ `null`), groups come in first-appearance order. After the
596
625
  grouping the body reads the **group**: a select item, `having` and `order by`
597
626
  may name a key, an **aggregate** over the group's rows, or `rows` — the group's
598
- rows, a value and a receiver (`rows collect { name values }`, `rows count { where
627
+ rows, a value and a receiver (`rows { name values }`, `rows count { where
599
628
  active }`, `rows first { … }`); any other bare name is a parse error naming it.
600
629
  Without a `select` the projection is the keys plus `count: count()`.
601
630
 
602
631
  ```js
603
- oqx`from ${people} group by city`;
632
+ oqx`${people} { group by city }`;
604
633
  // [{ city: "NYC", count: 2 }, { city: "SF", count: 1 }]
605
- oqx`select city, n: count(), oldest: max(age), names: collect(name) from ${people} group by city order by count() desc`;
606
- oqx`select city, active: rows count { where active } from ${people} group by city having count() > 1`;
607
- oqx`select decade, n: count() from ${people} group by decade: (age - age % 10) / 10 order by decade`;
634
+ oqx`${people} { select city, n: count(), oldest: max(age), names: collect(name) group by city order by count() desc }`;
635
+ oqx`${people} { select city, active: rows count { where active } group by city having count() > 1 }`;
636
+ oqx`${people} { select decade, n: count() group by decade: (age - age % 10) / 10 order by decade }`;
608
637
  oqx`${people} count { group by city }`; // the number of groups
609
638
  ```
610
639
 
@@ -619,11 +648,40 @@ Aggregates do not nest. `having` is the post-group `where` (it may name a select
619
648
  alias: `having n > 1`); `order by` reads the group scope and is not rewritten
620
649
  against aliases (`order by count()`, not `order by n`); `limit`/`offset` bound
621
650
  the groups; `distinct` with `group by` is an error (redundant). `count { … }` is
622
- now a value wherever a value stands (`n: jobs count { where pay > 1 }`), in or
623
- out of a group. A nested block may group too (`jobs collect { employer, total:
651
+ now a value wherever a value stands (`n: jobs count { pay > 1 }`), in or
652
+ out of a group. A nested block may group too (`jobs { employer, total:
624
653
  sum(pay) group by employer }`), and every consumer reduces the groups (`jobs
625
654
  count { group by employer }`). Spec: `spec/oqx/SEMANTICS.md` §24.
626
655
 
656
+ ### 8d. Flatten (0.25)
657
+
658
+ `flatten <expr> [{ … }] [as <name>]` — after `follow`, before `group by`; one or
659
+ more — splits one value into rows: each row becomes one row per element of the
660
+ value, the **parent row with `name` bound** beside its own properties at the
661
+ same depth (`$it`, `^` and a walk's intrinsics unchanged). An array → one row
662
+ per element (an empty list yields none); an object → one row; **absent → one
663
+ row with the name absent** (a missing field never drops the row); a scalar →
664
+ one row with the scalar (not the §3 error — nothing is sourced from it). A plain
665
+ navigation names itself by its last segment (`flatten tags` rebinds `tags`;
666
+ `flatten tags as t` keeps the list readable); anything else needs `as`. The
667
+ block is the receiver block over the elements, applied first (`flatten tags { $it
668
+ != "b" } as t`; `flatten jobs { employer values order by pay desc limit
669
+ 1 } as j`). Flattens chain (`flatten jobs as j flatten j.tags as t`), `where`
670
+ runs before them, `group by` groups the flattened rows (a bound name is a key),
671
+ `order by`/`limit` and every consumer apply to them, and a flattened row's
672
+ `$key` is the pair `[parent, element]`, so `distinct`/`is`/`follow` tell
673
+ `(doc, "a")` from `(doc, "b")`.
674
+
675
+ ```js
676
+ oqx`${people} { select name, tags flatten tags }`;
677
+ // [{ name: "Bob", tags: "a" }, { name: "Bob", tags: "b" }, { name: "Carol" }] (Carol has no tags)
678
+ oqx`${people} { select name, t flatten tags { $it != "b" } as t }`;
679
+ oqx`${people} { select t, n: count() flatten tags as t group by t order by count() desc }`;
680
+ oqx`${people} count { flatten tags as t }`; // the number of (person, tag) rows
681
+ ```
682
+
683
+ Spec: `spec/oqx/SEMANTICS.md` §25.
684
+
627
685
  ### 9. Recursion: `follow`
628
686
 
629
687
  `follow <destination>, …` turns a query into a bounded recursive traversal: the
@@ -637,7 +695,7 @@ const tree = [{ id: "root", children: [
637
695
  { id: "b", children: [] },
638
696
  ]}];
639
697
 
640
- oqx`id, depth: $depth from ${tree} follow children order by $depth, id`;
698
+ oqx`${tree} { select id, depth: $depth follow children order by $depth, id }`;
641
699
  // [{ id: "root", depth: 1 }, { id: "a", depth: 2 }, { id: "b", depth: 2 }, { id: "a1", depth: 3 }]
642
700
  ```
643
701
 
@@ -652,12 +710,12 @@ whose body has two clauses of its own after `where`: `depth <n>` and
652
710
  `frontier <pred>`:
653
711
 
654
712
  ```js
655
- oqx`id, stop: $stop from ${tree} follow children { depth 2 } order by $ordinal`;
713
+ oqx`${tree} { select id, stop: $stop follow children { depth 2 } order by $ordinal }`;
656
714
  // a1 is never reached; a and b report stop:"depth"
657
715
  ```
658
716
 
659
717
  The intrinsics belong to the reached row's own scope like any other name: inside
660
- a nested block (`kids: children collect { … }`) a bare `$depth` is the nested
718
+ a nested block (`kids: children { … }`) a bare `$depth` is the nested
661
719
  row's own `$depth` property (absent unless the data carries one), and the
662
720
  occurrence's depth is `^$depth`. The same holds everywhere the metadata is not
663
721
  present — outside a `follow`, `$depth`/`$ordinal`/`$stop`/`$leaf`/`$frontier`
@@ -677,7 +735,7 @@ A **plain destination** is a relation of the current row (`children`, `doc.out`,
677
735
  `entries(kids)`, a tuple `(before, after)` — one destination over the union of
678
736
  its elements; an outer reference `^rel` on its own or a literal is a parse
679
737
  error). A **destination block** is a receiver followed by a block — `children {
680
- depth 2 }`, `^people { where manager == ^id }`, `(before, after) { depth 2 }`,
738
+ depth 2 }`, `^people { manager == ^id }`, `(before, after) { depth 2 }`,
681
739
  or with its consumer, `^chain first distinct { … }` — the same directive you
682
740
  would write in `select`, re-evaluated per frontier row with `^` bound to that
683
741
  row. Its receiver *may* be an outer reference, so a walk can compute successors
@@ -686,13 +744,13 @@ that destination's block** (there is no options block any more):
686
744
 
687
745
  ```js
688
746
  const people = [{ id: 1, manager: null }, { id: 2, manager: 1 }, { id: 3, manager: 1 }, { id: 4, manager: 2 }];
689
- oqx`id values from ${people} where id == 1 follow ^people { where manager == ^id } order by $ordinal`;
747
+ oqx`${people} { select id values where id == 1 follow ^people { manager == ^id } order by $ordinal }`;
690
748
  // [1, 2, 3, 4] — the reporting tree under person 1 (`^people` names the root from a top-level walk)
691
- oqx`id values from ${people} where id == 1 follow ^people { where manager == ^id depth 2 } order by $ordinal`;
749
+ oqx`${people} { select id values where id == 1 follow ^people { manager == ^id depth 2 } order by $ordinal }`;
692
750
  // [1, 2, 3] — `depth` is a clause of the destination block, after its `where`
693
751
 
694
752
  const chain = [{ id: "a" }, { id: "b", prev: "a" }, { id: "c", prev: "b" }];
695
- oqx`id values from ${chain} where id == "b" follow distinct ^chain { where id == ^prev }, ^chain { where prev == ^id } order by $ordinal`;
753
+ oqx`${chain} { select id values where id == "b" follow distinct ^chain { id == ^prev }, ^chain { prev == ^id } order by $ordinal }`;
696
754
  // ["b", "a", "c"] — both directions of a chain; a block may order, bound and `distinct` its rows
697
755
  ```
698
756
 
@@ -712,14 +770,14 @@ and `depth <n>` (1–8: this destination expands a frontier row only while its
712
770
  the candidate successor's scope whose parent is the frontier row's — a bare name
713
771
  is the candidate's own property, `^name` is the row being expanded, `^^name` the
714
772
  walk's enclosing scope (language 0.14; before, `^` skipped the frontier row). So
715
- `follow doc.in { where before.contains(^$path) }` steps only into rows that
773
+ `follow doc.in { before.contains(^$path) }` steps only into rows that
716
774
  name the row they were reached from. `frontier` is not correlated: it reads the
717
775
  occurrence with `^` the enclosing scope, as the body does; seeds are reached
718
776
  through no destination and are never frontiers by a destination's rule. **The
719
777
  body that carries the `follow` may write `frontier <pred>` too**, after its
720
778
  `where` and before `follow`: it applies to the seeds — a matching seed stays in
721
- the result with `$stop == "frontier"` and is not expanded (`id from tree frontier
722
- !active follow children { depth 2 }`); read in the seed's scope like the body's
779
+ the result with `$stop == "frontier"` and is not expanded (`tree { id frontier
780
+ !active follow children { depth 2 } }`); read in the seed's scope like the body's
723
781
  `where`. Each destination's `frontier` is its own; without a `follow`, a
724
782
  body-level `frontier` is a parse error.
725
783
 
@@ -735,9 +793,9 @@ their shape):
735
793
 
736
794
  ```js
737
795
  const n1 = { id: 1 }, n2 = { id: 2, next: [n1] }; n1.next = [n2]; // a cyclic graph of fresh references
738
- oqx`id, stop: $stop, $key: id from ${[n1]} follow next { $key: id } order by $ordinal`;
796
+ oqx`${[n1]} { select id, stop: $stop, $key: id follow next { $key: id } order by $ordinal }`;
739
797
  // [{ id: 1, stop: "interior", $key: 1 }, { id: 2, stop: "interior", $key: 2 }, { id: 1, stop: "cycle", $key: 1 }]
740
- oqx`name, $key: email from ${people} follow next { $key: email }`; // known by email at every step
798
+ oqx`${people} { select name, $key: email follow next { $key: email } }`; // known by email at every step
741
799
  ```
742
800
 
743
801
  Identities are compared as values across the whole walk, so destinations that
@@ -751,7 +809,8 @@ In row position an **array** is a collection and a non-array **object** is one
751
809
  row: a block over it yields **one record** (or absent, when the row is filtered
752
810
  out) rather than a one-element list, `values` the one value, `first`/`single`
753
811
  that object — `doc { title }` is `{ title }`, `doc { status == "active" }` is the
754
- object or absent (a conditional reach-through), `title from doc` the same record.
812
+ object or absent (a conditional reach-through) — at the top level exactly as in
813
+ a nested block.
755
814
  A scalar in row position is an eval error (`list(s)` says "one element"
756
815
  explicitly); a `from` re-projection or a `follow` is plural. A host says which
757
816
  of its objects are collections through `DataContext.isSingle` (a lazy handle);
@@ -760,7 +819,7 @@ by default a non-iterable object is one row.
760
819
  ```js
761
820
  oqx`${settings} { theme }`; // { theme: "dark" } (one object → one record)
762
821
  oqx`${[settings]} { theme }`; // [{ theme: "dark" }] (a collection → a list)
763
- oqx`from list(${settings})`; // [settings] (the explicit one-element collection)
822
+ oqx`list(${settings}) { }`; // [settings] (the explicit one-element collection)
764
823
  ```
765
824
 
766
825
  A **tuple** `(e1, e2, …)` — two or more expressions in parentheses — is a value
@@ -769,10 +828,10 @@ collection contributes its items, a row itself; an absent element nothing, a
769
828
  `null` is kept; duplicates kept). One parenthesized expression is grouping.
770
829
 
771
830
  ```js
772
- oqx`n: size((tags, aliases)) from ${people}`; // one count over both lists
773
- oqx`name from ${people} where "x" in (tags, aliases)`; // membership in the union
774
- oqx`n: (mom, dad, siblings) { name values } from ${people}`; // one block over the union
775
- oqx`id from ${g} follow (before, after) { depth 2 }`; // one destination over the union
831
+ oqx`${people} { select n: size((tags, aliases)) }`; // one count over both lists
832
+ oqx`${people} { select name where "x" in (tags, aliases) }`; // membership in the union
833
+ oqx`${people} { select n: (mom, dad, siblings) { name values } }`; // one block over the union
834
+ oqx`${g} { select id follow (before, after) { depth 2 } }`; // one destination over the union
776
835
  ```
777
836
 
778
837
  ### 9c. Template literals (0.21)
@@ -787,10 +846,10 @@ literal may span lines. An interpolation holds any expression with balanced
787
846
  braces — a `{ … }` block or another template literal included.
788
847
 
789
848
  ```js
790
- oqx`label: \`\${title} (\${era})\` from ${docs}`; // "Alpha (3)"
791
- oqx`x: \`\${jobs single { employer values where primary }}\` from ${docs}`; // the block's one value
792
- oqx`title from ${docs} where \`\${title}!\` == "Alpha!"`;
793
- oqx`x: jobs collect { label: \`\${^title}: \${employer}\` values } from ${docs}`; // `^` reads the enclosing row
849
+ oqx`${docs} { select label: \`\${title} (\${era})\` }`; // "Alpha (3)"
850
+ oqx`${docs} { select x: \`\${jobs single { employer values where primary }}\` }`; // the block's one value
851
+ oqx`${docs} { select title where \`\${title}!\` == "Alpha!" }`;
852
+ oqx`${docs} { select x: jobs { label: \`\${^title}: \${employer}\` values } }`; // `^` reads the enclosing row
794
853
  ```
795
854
 
796
855
  Two rules keep it honest: **absent propagates** — any absent interpolation makes
@@ -807,59 +866,62 @@ unterminated-literal error).
807
866
  Every form below is shorthand for an explicit directive — it **desugars in the
808
867
  parser** to the nodes you already know (`print` writes the explicit form, the
809
868
  optimizer and any pushdown planner see it), so nothing here has semantics of its
810
- own except the postfix `!` and the infix `is`:
869
+ own except the postfix `!` and the infix `is`. (`print` writes the explicit
870
+ form — `where` for a leading predicate, `first { offset 0 }` for `[0]` — except
871
+ that `collect` itself prints as the bare brace, 0.26.)
811
872
 
812
873
  ```js
813
874
  // A receiver block without a consumer is `collect` — wherever a value stands
814
- // (language 0.20): a select item, a where leaf, a follow destination, a call
815
- // argument; `x distinct { … }` is `x collect distinct { … }`. A block whose
816
- // LEADING expression is syntactically a predicate drops `where`.
817
- oqx`name, current: jobs { !end_date } from ${people}`
818
- // ≡ name, current: jobs collect { where !end_date } from ${people}
875
+ // (language 0.20): the whole query, a select item, a where leaf, a follow
876
+ // destination, a call argument; `x distinct { … }` is `x collect distinct { … }`.
877
+ // A block whose LEADING expression is syntactically a predicate drops `where`.
878
+ oqx`${people} { name, current: jobs { !end_date } }`
879
+ // ≡ ${people} collect { select name, current: jobs collect { where !end_date } }
819
880
  oqx`${people} { age > 40 }` // ≡ ${people} collect { where age > 40 }
820
- oqx`n: size(jobs { !end_date }) from ${people}` // a block as a call argument
881
+ oqx`${people} { n: size(jobs { !end_date }) }` // a block as a call argument
821
882
  oqx`${people} distinct { city }` // ≡ ${people} collect distinct { city }
822
- oqx`${people} count { jobs exists { } }` // a consumer test is predicate-shaped too
883
+ oqx`${people} count { jobs exists }` // a consumer test is predicate-shaped too (a consumer word alone is the empty block, 0.26)
823
884
  oqx`${people} first { name }` // a bare name still projects — unchanged
824
885
  oqx`${people} first { is active }` // the bare-field filter: `is x` ≡ `!!x`, `not x` ≡ `!x`
825
886
 
826
887
  // Brackets: `[p]` is `first { where p }`, `[n]` is `first { offset n }`;
827
888
  // a trailing `!` requires the result (and makes the predicate form `single`).
828
- oqx`name, boss: ^people[id == ^manager].name from ${people}` // the first match, navigated
829
- oqx`name, first_job: jobs[0], primary: jobs[pay > 2]! from ${people}`
889
+ oqx`${people} { name, boss: ^people[id == ^manager].name }` // the first match, navigated
890
+ oqx`${people} { name, first_job: jobs[0], primary: jobs[pay > 2]! }`
830
891
  // jobs[0] ≡ jobs first { offset 0 } (out of range ⇒ absent)
831
892
  // jobs[pay > 2]! ≡ jobs single { where pay > 2 }! (zero or many matches ⇒ an eval error)
832
893
 
833
894
  // Postfix `!` — required: the value, or an eval error naming the expression
834
895
  // (and the row's `$key` when scalar). Never a filter, never a coercion: 0! and ""! are values.
835
- oqx`select id!, title from ${docs}` // insist on an identity before a write
896
+ oqx`${docs} { select id!, title }` // insist on an identity before a write
836
897
  // Tightest precedence: `refs(c)[0]!.name` requires the lookup, `refs(c)[0].name!` the name.
837
898
 
838
899
  // `is` / `is not` compare IDENTITY (`$key`: the host's, else structural — §9):
839
- oqx`name from ${people} where manager is null` // absent (same as == null)
900
+ oqx`${people} { name where manager is null }` // absent (same as == null)
840
901
  oqx`${people} count { ^people exists { $it is ^$it } }` // rows compare; `==` leaves that unspecified
841
902
  // `and` / `or` / `not` are the canonical connectives; `&&` / `||` / `!` are exact
842
903
  // synonyms (`title or path` coalesces). `print` writes the words (language 0.18); `!` stays `!`.
843
904
 
844
905
  // A select item may use the items to its left (inlined like a `where` alias):
845
- oqx`boss: ^people[id == ^manager], bossName: boss.name from ${people}`
906
+ oqx`${people} { boss: ^people[id == ^manager], bossName: boss.name }`
846
907
  ```
847
908
 
848
909
  Reserved words, never a bare field name: `from where select is not and or` and
849
- the literal words `true false null`. The top level is unchanged — `age > 15
850
- from ${people}` stays an error, because `where` would precede `from`. One
851
- carve-out: after a `follow` destination that is a relation of the current row
852
- (a bare name, a call, a binding) a brace is still the **options** block (`follow
853
- children { depth 2 }`), never a collect; only over an outer-reference receiver,
854
- which cannot be a plain destination, is the brace a destination block with the
855
- implicit `collect` (`follow ^people { where … }`, language 0.19).
910
+ the literal words `true false null`. The clause form's top level is unchanged —
911
+ `age > 15 from ${people}` stays an error, because `where` would precede `from`;
912
+ the collection-scoped `${people} { age > 15 }` is the filter. A brace after
913
+ *any* `follow` destination opens that destination's block with the implicit
914
+ `collect` (`follow children { depth 2 }`, `follow ^people { where … }`; the
915
+ options block of 0.19 is gone since language 0.20).
856
916
 
857
917
  ### Cheat-sheet
858
918
 
859
919
  ```
860
- select … from … where … follow … order by … limit N offset N the fixed clause order (each at most once)
861
- name, alias: expr, nested: rel collect { … } projection (`select` may be dropped only here, in first position)
862
- from ${source} source collection (required at the top level)
920
+ ${source} { … } / ${source} count { … } a query: the collection heads the body; a consumer word may stand before the brace; nothing follows it
921
+ select … from … where … follow … flatten … group by … having … order by … limit N offset N the fixed clause order inside a body (each at most once)
922
+ name, alias: expr, nested: rel { … } projection (`select` may be dropped only here, in first position)
923
+ from rel re-projection: each row replaced by the rows `rel` yields (flatMap) — a body never names its source
924
+ select name from ${source} where … the clause form (pre-0.26): still read, same tree, no longer printed
863
925
  where a == b and rel exists { where … } or not c predicate tree + nested ops (`&&`/`||`/`!` are accepted synonyms)
864
926
  rel { pay > 2 } / rel { employer } a block without a consumer is collect; a leading predicate is where-first, a bare name projects
865
927
  rel[p] / rel[p]! / rel[0] / rel[0]! first match / exactly one (required) / positional / required positional
@@ -871,17 +933,17 @@ where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl.
871
933
  where x in range(field) coerce a string field to a range, then test coverage
872
934
  name the CURRENT row's field only (never climbs)
873
935
  $it the current item itself (a scalar row, or the whole object)
874
- from entries(obj) … $key / $it a record's properties as a collection (key + value; bare names read the value)
936
+ entries(obj) { … $key / $it } a record's properties as a collection (key + value; bare names read the value)
875
937
  <expr> values scalar projection: the value, not a { name: value } record
876
938
  ^name / ^^name read an enclosing row's field (exactly N scopes out); ^$it = the enclosing row
877
939
  ^name: expr / ^^name: expr lift/export a value N scopes out (flatten-append)
878
- ^rel collect { … } / ^^root exists { … } nested consumer over an enclosing row's relation / a named root
940
+ ^rel { … } / ^^root exists { … } nested consumer over an enclosing row's relation / a named root
879
941
  entries(rel) exists { … } a free-function call may be a receiver
880
942
  order by expr desc, expr2 ordering
881
943
  limit n / offset n bound the row set (after where/order/distinct, before the consumer)
882
944
  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
883
945
  follow before, after / follow (before, after) several destinations: the union of their rows (per step, by identity) / one tuple destination
884
- follow ^people { where manager == ^id } a destination block: successors computed per frontier row (`^` = that row; `collect` implicit, 0.20)
946
+ follow ^people { manager == ^id } a destination block: successors computed per frontier row (`^` = that row; `collect` implicit, 0.20)
885
947
  $key / $key: expr the identity a row is known by (the host's, else structural) / give the rows a block yields that identity
886
948
  (a, b) / rel distinct { … } a tuple: the elements, one level flat (a value and a receiver) / collect distinct (0.20)
887
949
  `${title} (${era})` a template literal: text + string forms; absent anywhere → absent; a list/record → error (0.21)
@@ -914,7 +976,7 @@ data (no parent pointers, no methods) and *are* the JSON shape.
914
976
  ```js
915
977
  import { parse, visit, transform, print, printTemplate, toJSON, stripSpans, build } from "@omgbase/oqx";
916
978
 
917
- const q = parse('select name, n: jobs collect { employer } from people where jobs exists { where !end }');
979
+ const q = parse('people { select name, n: jobs { employer } where jobs exists { !end } }');
918
980
 
919
981
  // visit: every node, with its ancestors, the clause it sits in, and the scope depth
920
982
  const relations = [];
@@ -928,11 +990,12 @@ visit(q, {
928
990
  // transform: rebuild with every expression mapped (children first; untouched nodes keep identity + spans)
929
991
  const rewritten = transform(q, (e) => (e.kind === "ident" && e.name === "name" ? build.member(build.ident("person"), "name") : e));
930
992
 
931
- // print: the canonical source — single spaces, `select` written, minimal parentheses
993
+ // print: the canonical source — collection-scoped, single spaces, the projection leading
994
+ // without `select`, every item aliased, `collect` as the bare brace, minimal parentheses
932
995
  print(rewritten);
933
- // 'select name: person.name, n: jobs collect { employer: employer } from people where jobs exists { where !end }'
996
+ // 'people { name: person.name, n: jobs { employer: employer } where jobs exists { where !end } }'
934
997
 
935
- toJSON(q); // { oqx: "0.17", kind: "query", span: [0, 93], … } — the shared JSON document
998
+ toJSON(q); // { oqx: "0.26", kind: "query", span: [0, 71], … } — the shared JSON document
936
999
  stripSpans(q); // the same tree without spans, for shape comparisons
937
1000
  ```
938
1001
 
@@ -951,8 +1014,9 @@ place.
951
1014
  `print(parse(q)) === q` is *not* a law — spelling is normalized. A binding is a
952
1015
  value, never source text: `print` throws (`OqxError`, stage `"print"`) on one,
953
1016
  and `printTemplate(node)` returns `{ strings, count, indices }` — the fragments
954
- around each binding and the binding each gap stands for (the canonical clause
955
- order can move one past another), to re-run with `parseTemplate(strings,
1017
+ around each binding and the binding each gap stands for (the canonical form
1018
+ can move one past another: the clause form `select x: ${1} from ${0}` prints as
1019
+ `${0} { x: ${1} }`, `indices = [1, 0]`), to re-run with `parseTemplate(strings,
956
1020
  count)` and the values permuted by `indices`.
957
1021
 
958
1022
  `build.*` constructs nodes with the empty span `[0, 0]` and the materialized
@@ -967,8 +1031,8 @@ it is read from — so a host that knows what rows an expression yields can say
967
1031
  which names exist there:
968
1032
 
969
1033
  ```ts
970
- references(parse('select title, age from docs where blocks exists { where text == ^titel }'));
971
- // [ { name: "title", clause: "select", scope: { depth: 1, steps: [{ expr: <docs>, depth: 1 }] }, span: [7, 12] },
1034
+ references(parse('docs { select title, age where blocks exists { text == ^titel } }'));
1035
+ // [ { name: "title", clause: "select", scope: { depth: 1, steps: [{ expr: <docs>, depth: 1 }] }, span: [14, 19] },
972
1036
  // { name: "age", clause: "select", scope: { depth: 1, steps: [<docs>@1] }, … },
973
1037
  // { name: "docs", clause: "source", scope: { depth: 0, steps: [] }, … }, // a root name
974
1038
  // { name: "blocks", clause: "where", scope: { depth: 1, steps: [<docs>@1] }, … },
@@ -992,8 +1056,8 @@ When you don't need interpolation, `execute` runs a plain string query against a
992
1056
  ```js
993
1057
  import { execute } from "@omgbase/oqx";
994
1058
 
995
- execute("name from people where age >= 18", { people });
996
- // `from people` resolves the `people` root
1059
+ execute("people { select name where age >= 18 }", { people });
1060
+ // the collection `people` resolves the root of that name
997
1061
  ```
998
1062
 
999
1063
  `parse(source)` returns a reusable AST and `run(query, { values, roots })` returns
@@ -1041,7 +1105,7 @@ const graph = {
1041
1105
  toRows: (v) => v == null ? [] : Array.isArray(v) ? v : [v],
1042
1106
  identityOf: (row) => row.id, // `$key`: for follow dedup, distinct, `is` (optional; default: structural)
1043
1107
  };
1044
- run(parse("id, depth: $depth from tree follow children"), { context: graph });
1108
+ run(parse("tree { select id, depth: $depth follow children }"), { context: graph });
1045
1109
  ```
1046
1110
 
1047
1111
  #### Custom functions and methods
@@ -1072,7 +1136,7 @@ const ctx = {
1072
1136
  return builtins.callMethod(name, recv, args);
1073
1137
  },
1074
1138
  };
1075
- run(parse("id from tree where age(born) > 18 and title.slug() == 'x'"), { context: ctx });
1139
+ run(parse("tree { select id where age(born) > 18 and title.slug() == 'x' }"), { context: ctx });
1076
1140
  ```
1077
1141
 
1078
1142
  The simplest route is to `extends DefaultContext` and `super.callFunction(...)`
@@ -1098,7 +1162,7 @@ import { parse, PlannedEngine } from "@omgbase/oqx";
1098
1162
  import { SqliteTable } from "@omgbase/oqx/sqlite";
1099
1163
 
1100
1164
  const planner = new SqliteTable(db, "emp", { columns: ["id", "name", "dept", "level"] });
1101
- new PlannedEngine(planner).run(parse('name from emp where dept == "eng" and level >= 5'), []);
1165
+ new PlannedEngine(planner).run(parse('emp { select name where dept == "eng" and level >= 5 }'), []);
1102
1166
  // → `dept`/`level` pushed to SQL; anything untranslatable finishes in-memory
1103
1167
  ```
1104
1168
 
@@ -1113,9 +1177,9 @@ A nested block runs once per enclosing row, so the natural way to join two
1113
1177
  collections —
1114
1178
 
1115
1179
  ```js
1116
- oqx`select id, total, customer: ${customers} first { where id == ^customer_id } from ${orders}`;
1117
- oqx`select name, orders: ${orders} collect { id, total where customer_id == ^id } from ${customers}`;
1118
- oqx`select id from ${orders} where ${customers} exists { where id == ^customer_id }`;
1180
+ oqx`${orders} { select id, total, customer: ${customers} first { id == ^customer_id } }`;
1181
+ oqx`${customers} { select name, orders: ${orders} { id, total where customer_id == ^id } }`;
1182
+ oqx`${orders} { select id where ${customers} exists { id == ^customer_id } }`;
1119
1183
  ```
1120
1184
 
1121
1185
  — would be quadratic if the block scanned its receiver each time. It does not:
@@ -1141,7 +1205,7 @@ results and on errors.
1141
1205
  objects and arrays by reference. 5 000 orders × 5 000 customers zip in a few
1142
1206
  milliseconds; the scan takes seconds.
1143
1207
  - **Invariant blocks run once.** A block that reads nothing from any enclosing
1144
- row (`^customers collect { name values }` from a top-level row) and lifts
1208
+ row (`^customers { name values }` from a top-level row) and lifts
1145
1209
  nothing is evaluated the first time it is reached and its value reused for
1146
1210
  every other row (the same array/record by reference).
1147
1211
  - **Semi/anti-join short-circuit.** `exists` and `none` stop at the first bucket
@@ -1173,8 +1237,8 @@ bindings (`^customers`, `${customers}`, `^^config.items`) is evaluated once per
1173
1237
  run — its rows are read once and reused across the enclosing rows, with or
1174
1238
  without a correlation — and indexed once per path even if the context hands out
1175
1239
  a fresh array per read. Any other receiver is indexed the second time the
1176
- **same object** is probed, so a per-row relation (`lines collect { … where cid
1177
- == ^id }` with a different `lines` array per row) is scanned, not indexed. A
1240
+ **same object** is probed, so a per-row relation (`lines { … where cid ==
1241
+ ^id }` with a different `lines` array per row) is scanned, not indexed. A
1178
1242
  scalar receiver is one row.
1179
1243
 
1180
1244
  **Pre-built indexes.** `DataContext.indexFor(collection, path)` is an optional
@@ -1205,7 +1269,7 @@ materialize` is the same seam for a `Value` that cannot be lazy on its own.
1205
1269
  ```js
1206
1270
  import { parse, run, IndexedCollection } from "@omgbase/oqx";
1207
1271
  const orders = new IndexedCollection("orders", orderRows, ["customer_id"]);
1208
- run(parse("select name, o: ^orders collect { id values where customer_id == ^id } from customers"),
1272
+ run(parse("customers { select name, o: ^orders { id values where customer_id == ^id } }"),
1209
1273
  { context: orders.context({ customers }) }); // probes the pre-built index
1210
1274
  ```
1211
1275
 
@@ -1330,7 +1394,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1330
1394
  import { parse, run, IndexedCollection, PlannedEngine } from "@omgbase/oqx";
1331
1395
  const planner = new IndexedCollection("people", people, ["city", "title"]);
1332
1396
  const engine = new PlannedEngine(planner, planner.context());
1333
- run(parse('name from people where city == "NYC" and age > 30'), { engine });
1397
+ run(parse('people { select name where city == "NYC" and age > 30 }'), { engine });
1334
1398
  // city probe from the index; `age > 30` finished in-memory over the candidates
1335
1399
  ```
1336
1400
 
@@ -1360,7 +1424,8 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1360
1424
  (`Visitor`, `VisitContext`, `Clause` are the types); `CHILDREN` — the
1361
1425
  child-key table per node kind that drives them.
1362
1426
  - `print(node)` / `printTemplate(node)` — the canonical printer (`Template` is
1363
- `{ strings, count, indices }`).
1427
+ `{ strings, count, indices }`); it writes the collection-scoped form
1428
+ `S [consumer] [distinct] { … }` (0.26) and never the clause form.
1364
1429
  - `resolveAliases(query)` — the `select`-alias substitution the entry points
1365
1430
  apply before evaluation (SEMANTICS §14).
1366
1431
  - `references(query)` — every name the query reads from a row, with its
@@ -1371,7 +1436,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1371
1436
  the display source of a template.
1372
1437
  - `build` — the node builders (`build.query`, `build.op`, `build.subquery`,
1373
1438
  `build.field`, `build.ident`, …).
1374
- - `LANGUAGE_VERSION` — the `spec/oqx/VERSION` this implementation conforms to (`"0.17"`).
1439
+ - `LANGUAGE_VERSION` — the `spec/oqx/VERSION` this implementation conforms to (`"0.26"`).
1375
1440
 
1376
1441
  **Types**
1377
1442