@omgbase/oqx 0.25.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.
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,8 +648,8 @@ 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
 
@@ -635,8 +664,8 @@ row with the name absent** (a missing field never drops the row); a scalar →
635
664
  one row with the scalar (not the §3 error — nothing is sourced from it). A plain
636
665
  navigation names itself by its last segment (`flatten tags` rebinds `tags`;
637
666
  `flatten tags as t` keeps the list readable); anything else needs `as`. The
638
- block is the receiver block over the elements, applied first (`flatten tags {
639
- where $it != "b" } as t`; `flatten jobs { employer values order by pay desc limit
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
640
669
  1 } as j`). Flattens chain (`flatten jobs as j flatten j.tags as t`), `where`
641
670
  runs before them, `group by` groups the flattened rows (a bound name is a key),
642
671
  `order by`/`limit` and every consumer apply to them, and a flattened row's
@@ -644,10 +673,10 @@ runs before them, `group by` groups the flattened rows (a bound name is a key),
644
673
  `(doc, "a")` from `(doc, "b")`.
645
674
 
646
675
  ```js
647
- oqx`select name, tags from ${people} flatten tags`;
676
+ oqx`${people} { select name, tags flatten tags }`;
648
677
  // [{ name: "Bob", tags: "a" }, { name: "Bob", tags: "b" }, { name: "Carol" }] (Carol has no tags)
649
- oqx`select name, t from ${people} flatten tags { where $it != "b" } as t`;
650
- oqx`select t, n: count() from ${people} flatten tags as t group by t order by count() desc`;
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 }`;
651
680
  oqx`${people} count { flatten tags as t }`; // the number of (person, tag) rows
652
681
  ```
653
682
 
@@ -666,7 +695,7 @@ const tree = [{ id: "root", children: [
666
695
  { id: "b", children: [] },
667
696
  ]}];
668
697
 
669
- 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 }`;
670
699
  // [{ id: "root", depth: 1 }, { id: "a", depth: 2 }, { id: "b", depth: 2 }, { id: "a1", depth: 3 }]
671
700
  ```
672
701
 
@@ -681,12 +710,12 @@ whose body has two clauses of its own after `where`: `depth <n>` and
681
710
  `frontier <pred>`:
682
711
 
683
712
  ```js
684
- 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 }`;
685
714
  // a1 is never reached; a and b report stop:"depth"
686
715
  ```
687
716
 
688
717
  The intrinsics belong to the reached row's own scope like any other name: inside
689
- a nested block (`kids: children collect { … }`) a bare `$depth` is the nested
718
+ a nested block (`kids: children { … }`) a bare `$depth` is the nested
690
719
  row's own `$depth` property (absent unless the data carries one), and the
691
720
  occurrence's depth is `^$depth`. The same holds everywhere the metadata is not
692
721
  present — outside a `follow`, `$depth`/`$ordinal`/`$stop`/`$leaf`/`$frontier`
@@ -706,7 +735,7 @@ A **plain destination** is a relation of the current row (`children`, `doc.out`,
706
735
  `entries(kids)`, a tuple `(before, after)` — one destination over the union of
707
736
  its elements; an outer reference `^rel` on its own or a literal is a parse
708
737
  error). A **destination block** is a receiver followed by a block — `children {
709
- depth 2 }`, `^people { where manager == ^id }`, `(before, after) { depth 2 }`,
738
+ depth 2 }`, `^people { manager == ^id }`, `(before, after) { depth 2 }`,
710
739
  or with its consumer, `^chain first distinct { … }` — the same directive you
711
740
  would write in `select`, re-evaluated per frontier row with `^` bound to that
712
741
  row. Its receiver *may* be an outer reference, so a walk can compute successors
@@ -715,13 +744,13 @@ that destination's block** (there is no options block any more):
715
744
 
716
745
  ```js
717
746
  const people = [{ id: 1, manager: null }, { id: 2, manager: 1 }, { id: 3, manager: 1 }, { id: 4, manager: 2 }];
718
- 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 }`;
719
748
  // [1, 2, 3, 4] — the reporting tree under person 1 (`^people` names the root from a top-level walk)
720
- 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 }`;
721
750
  // [1, 2, 3] — `depth` is a clause of the destination block, after its `where`
722
751
 
723
752
  const chain = [{ id: "a" }, { id: "b", prev: "a" }, { id: "c", prev: "b" }];
724
- 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 }`;
725
754
  // ["b", "a", "c"] — both directions of a chain; a block may order, bound and `distinct` its rows
726
755
  ```
727
756
 
@@ -741,14 +770,14 @@ and `depth <n>` (1–8: this destination expands a frontier row only while its
741
770
  the candidate successor's scope whose parent is the frontier row's — a bare name
742
771
  is the candidate's own property, `^name` is the row being expanded, `^^name` the
743
772
  walk's enclosing scope (language 0.14; before, `^` skipped the frontier row). So
744
- `follow doc.in { where before.contains(^$path) }` steps only into rows that
773
+ `follow doc.in { before.contains(^$path) }` steps only into rows that
745
774
  name the row they were reached from. `frontier` is not correlated: it reads the
746
775
  occurrence with `^` the enclosing scope, as the body does; seeds are reached
747
776
  through no destination and are never frontiers by a destination's rule. **The
748
777
  body that carries the `follow` may write `frontier <pred>` too**, after its
749
778
  `where` and before `follow`: it applies to the seeds — a matching seed stays in
750
- the result with `$stop == "frontier"` and is not expanded (`id from tree frontier
751
- !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
752
781
  `where`. Each destination's `frontier` is its own; without a `follow`, a
753
782
  body-level `frontier` is a parse error.
754
783
 
@@ -764,9 +793,9 @@ their shape):
764
793
 
765
794
  ```js
766
795
  const n1 = { id: 1 }, n2 = { id: 2, next: [n1] }; n1.next = [n2]; // a cyclic graph of fresh references
767
- 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 }`;
768
797
  // [{ id: 1, stop: "interior", $key: 1 }, { id: 2, stop: "interior", $key: 2 }, { id: 1, stop: "cycle", $key: 1 }]
769
- 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
770
799
  ```
771
800
 
772
801
  Identities are compared as values across the whole walk, so destinations that
@@ -780,7 +809,8 @@ In row position an **array** is a collection and a non-array **object** is one
780
809
  row: a block over it yields **one record** (or absent, when the row is filtered
781
810
  out) rather than a one-element list, `values` the one value, `first`/`single`
782
811
  that object — `doc { title }` is `{ title }`, `doc { status == "active" }` is the
783
- 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.
784
814
  A scalar in row position is an eval error (`list(s)` says "one element"
785
815
  explicitly); a `from` re-projection or a `follow` is plural. A host says which
786
816
  of its objects are collections through `DataContext.isSingle` (a lazy handle);
@@ -789,7 +819,7 @@ by default a non-iterable object is one row.
789
819
  ```js
790
820
  oqx`${settings} { theme }`; // { theme: "dark" } (one object → one record)
791
821
  oqx`${[settings]} { theme }`; // [{ theme: "dark" }] (a collection → a list)
792
- oqx`from list(${settings})`; // [settings] (the explicit one-element collection)
822
+ oqx`list(${settings}) { }`; // [settings] (the explicit one-element collection)
793
823
  ```
794
824
 
795
825
  A **tuple** `(e1, e2, …)` — two or more expressions in parentheses — is a value
@@ -798,10 +828,10 @@ collection contributes its items, a row itself; an absent element nothing, a
798
828
  `null` is kept; duplicates kept). One parenthesized expression is grouping.
799
829
 
800
830
  ```js
801
- oqx`n: size((tags, aliases)) from ${people}`; // one count over both lists
802
- oqx`name from ${people} where "x" in (tags, aliases)`; // membership in the union
803
- oqx`n: (mom, dad, siblings) { name values } from ${people}`; // one block over the union
804
- 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
805
835
  ```
806
836
 
807
837
  ### 9c. Template literals (0.21)
@@ -816,10 +846,10 @@ literal may span lines. An interpolation holds any expression with balanced
816
846
  braces — a `{ … }` block or another template literal included.
817
847
 
818
848
  ```js
819
- oqx`label: \`\${title} (\${era})\` from ${docs}`; // "Alpha (3)"
820
- oqx`x: \`\${jobs single { employer values where primary }}\` from ${docs}`; // the block's one value
821
- oqx`title from ${docs} where \`\${title}!\` == "Alpha!"`;
822
- 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
823
853
  ```
824
854
 
825
855
  Two rules keep it honest: **absent propagates** — any absent interpolation makes
@@ -836,59 +866,62 @@ unterminated-literal error).
836
866
  Every form below is shorthand for an explicit directive — it **desugars in the
837
867
  parser** to the nodes you already know (`print` writes the explicit form, the
838
868
  optimizer and any pushdown planner see it), so nothing here has semantics of its
839
- 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.)
840
872
 
841
873
  ```js
842
874
  // A receiver block without a consumer is `collect` — wherever a value stands
843
- // (language 0.20): a select item, a where leaf, a follow destination, a call
844
- // argument; `x distinct { … }` is `x collect distinct { … }`. A block whose
845
- // LEADING expression is syntactically a predicate drops `where`.
846
- oqx`name, current: jobs { !end_date } from ${people}`
847
- // ≡ 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 } }
848
880
  oqx`${people} { age > 40 }` // ≡ ${people} collect { where age > 40 }
849
- 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
850
882
  oqx`${people} distinct { city }` // ≡ ${people} collect distinct { city }
851
- 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)
852
884
  oqx`${people} first { name }` // a bare name still projects — unchanged
853
885
  oqx`${people} first { is active }` // the bare-field filter: `is x` ≡ `!!x`, `not x` ≡ `!x`
854
886
 
855
887
  // Brackets: `[p]` is `first { where p }`, `[n]` is `first { offset n }`;
856
888
  // a trailing `!` requires the result (and makes the predicate form `single`).
857
- oqx`name, boss: ^people[id == ^manager].name from ${people}` // the first match, navigated
858
- 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]! }`
859
891
  // jobs[0] ≡ jobs first { offset 0 } (out of range ⇒ absent)
860
892
  // jobs[pay > 2]! ≡ jobs single { where pay > 2 }! (zero or many matches ⇒ an eval error)
861
893
 
862
894
  // Postfix `!` — required: the value, or an eval error naming the expression
863
895
  // (and the row's `$key` when scalar). Never a filter, never a coercion: 0! and ""! are values.
864
- 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
865
897
  // Tightest precedence: `refs(c)[0]!.name` requires the lookup, `refs(c)[0].name!` the name.
866
898
 
867
899
  // `is` / `is not` compare IDENTITY (`$key`: the host's, else structural — §9):
868
- oqx`name from ${people} where manager is null` // absent (same as == null)
900
+ oqx`${people} { name where manager is null }` // absent (same as == null)
869
901
  oqx`${people} count { ^people exists { $it is ^$it } }` // rows compare; `==` leaves that unspecified
870
902
  // `and` / `or` / `not` are the canonical connectives; `&&` / `||` / `!` are exact
871
903
  // synonyms (`title or path` coalesces). `print` writes the words (language 0.18); `!` stays `!`.
872
904
 
873
905
  // A select item may use the items to its left (inlined like a `where` alias):
874
- oqx`boss: ^people[id == ^manager], bossName: boss.name from ${people}`
906
+ oqx`${people} { boss: ^people[id == ^manager], bossName: boss.name }`
875
907
  ```
876
908
 
877
909
  Reserved words, never a bare field name: `from where select is not and or` and
878
- the literal words `true false null`. The top level is unchanged — `age > 15
879
- from ${people}` stays an error, because `where` would precede `from`. One
880
- carve-out: after a `follow` destination that is a relation of the current row
881
- (a bare name, a call, a binding) a brace is still the **options** block (`follow
882
- children { depth 2 }`), never a collect; only over an outer-reference receiver,
883
- which cannot be a plain destination, is the brace a destination block with the
884
- 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).
885
916
 
886
917
  ### Cheat-sheet
887
918
 
888
919
  ```
889
- select … from … where … follow … order by … limit N offset N the fixed clause order (each at most once)
890
- name, alias: expr, nested: rel collect { … } projection (`select` may be dropped only here, in first position)
891
- 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
892
925
  where a == b and rel exists { where … } or not c predicate tree + nested ops (`&&`/`||`/`!` are accepted synonyms)
893
926
  rel { pay > 2 } / rel { employer } a block without a consumer is collect; a leading predicate is where-first, a bare name projects
894
927
  rel[p] / rel[p]! / rel[0] / rel[0]! first match / exactly one (required) / positional / required positional
@@ -900,17 +933,17 @@ where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl.
900
933
  where x in range(field) coerce a string field to a range, then test coverage
901
934
  name the CURRENT row's field only (never climbs)
902
935
  $it the current item itself (a scalar row, or the whole object)
903
- 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)
904
937
  <expr> values scalar projection: the value, not a { name: value } record
905
938
  ^name / ^^name read an enclosing row's field (exactly N scopes out); ^$it = the enclosing row
906
939
  ^name: expr / ^^name: expr lift/export a value N scopes out (flatten-append)
907
- ^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
908
941
  entries(rel) exists { … } a free-function call may be a receiver
909
942
  order by expr desc, expr2 ordering
910
943
  limit n / offset n bound the row set (after where/order/distinct, before the consumer)
911
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
912
945
  follow before, after / follow (before, after) several destinations: the union of their rows (per step, by identity) / one tuple destination
913
- 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)
914
947
  $key / $key: expr the identity a row is known by (the host's, else structural) / give the rows a block yields that identity
915
948
  (a, b) / rel distinct { … } a tuple: the elements, one level flat (a value and a receiver) / collect distinct (0.20)
916
949
  `${title} (${era})` a template literal: text + string forms; absent anywhere → absent; a list/record → error (0.21)
@@ -943,7 +976,7 @@ data (no parent pointers, no methods) and *are* the JSON shape.
943
976
  ```js
944
977
  import { parse, visit, transform, print, printTemplate, toJSON, stripSpans, build } from "@omgbase/oqx";
945
978
 
946
- 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 } }');
947
980
 
948
981
  // visit: every node, with its ancestors, the clause it sits in, and the scope depth
949
982
  const relations = [];
@@ -957,11 +990,12 @@ visit(q, {
957
990
  // transform: rebuild with every expression mapped (children first; untouched nodes keep identity + spans)
958
991
  const rewritten = transform(q, (e) => (e.kind === "ident" && e.name === "name" ? build.member(build.ident("person"), "name") : e));
959
992
 
960
- // 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
961
995
  print(rewritten);
962
- // '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 } }'
963
997
 
964
- 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
965
999
  stripSpans(q); // the same tree without spans, for shape comparisons
966
1000
  ```
967
1001
 
@@ -980,8 +1014,9 @@ place.
980
1014
  `print(parse(q)) === q` is *not* a law — spelling is normalized. A binding is a
981
1015
  value, never source text: `print` throws (`OqxError`, stage `"print"`) on one,
982
1016
  and `printTemplate(node)` returns `{ strings, count, indices }` — the fragments
983
- around each binding and the binding each gap stands for (the canonical clause
984
- 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,
985
1020
  count)` and the values permuted by `indices`.
986
1021
 
987
1022
  `build.*` constructs nodes with the empty span `[0, 0]` and the materialized
@@ -996,8 +1031,8 @@ it is read from — so a host that knows what rows an expression yields can say
996
1031
  which names exist there:
997
1032
 
998
1033
  ```ts
999
- references(parse('select title, age from docs where blocks exists { where text == ^titel }'));
1000
- // [ { 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] },
1001
1036
  // { name: "age", clause: "select", scope: { depth: 1, steps: [<docs>@1] }, … },
1002
1037
  // { name: "docs", clause: "source", scope: { depth: 0, steps: [] }, … }, // a root name
1003
1038
  // { name: "blocks", clause: "where", scope: { depth: 1, steps: [<docs>@1] }, … },
@@ -1021,8 +1056,8 @@ When you don't need interpolation, `execute` runs a plain string query against a
1021
1056
  ```js
1022
1057
  import { execute } from "@omgbase/oqx";
1023
1058
 
1024
- execute("name from people where age >= 18", { people });
1025
- // `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
1026
1061
  ```
1027
1062
 
1028
1063
  `parse(source)` returns a reusable AST and `run(query, { values, roots })` returns
@@ -1070,7 +1105,7 @@ const graph = {
1070
1105
  toRows: (v) => v == null ? [] : Array.isArray(v) ? v : [v],
1071
1106
  identityOf: (row) => row.id, // `$key`: for follow dedup, distinct, `is` (optional; default: structural)
1072
1107
  };
1073
- run(parse("id, depth: $depth from tree follow children"), { context: graph });
1108
+ run(parse("tree { select id, depth: $depth follow children }"), { context: graph });
1074
1109
  ```
1075
1110
 
1076
1111
  #### Custom functions and methods
@@ -1101,7 +1136,7 @@ const ctx = {
1101
1136
  return builtins.callMethod(name, recv, args);
1102
1137
  },
1103
1138
  };
1104
- 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 });
1105
1140
  ```
1106
1141
 
1107
1142
  The simplest route is to `extends DefaultContext` and `super.callFunction(...)`
@@ -1127,7 +1162,7 @@ import { parse, PlannedEngine } from "@omgbase/oqx";
1127
1162
  import { SqliteTable } from "@omgbase/oqx/sqlite";
1128
1163
 
1129
1164
  const planner = new SqliteTable(db, "emp", { columns: ["id", "name", "dept", "level"] });
1130
- 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 }'), []);
1131
1166
  // → `dept`/`level` pushed to SQL; anything untranslatable finishes in-memory
1132
1167
  ```
1133
1168
 
@@ -1142,9 +1177,9 @@ A nested block runs once per enclosing row, so the natural way to join two
1142
1177
  collections —
1143
1178
 
1144
1179
  ```js
1145
- oqx`select id, total, customer: ${customers} first { where id == ^customer_id } from ${orders}`;
1146
- oqx`select name, orders: ${orders} collect { id, total where customer_id == ^id } from ${customers}`;
1147
- 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 } }`;
1148
1183
  ```
1149
1184
 
1150
1185
  — would be quadratic if the block scanned its receiver each time. It does not:
@@ -1170,7 +1205,7 @@ results and on errors.
1170
1205
  objects and arrays by reference. 5 000 orders × 5 000 customers zip in a few
1171
1206
  milliseconds; the scan takes seconds.
1172
1207
  - **Invariant blocks run once.** A block that reads nothing from any enclosing
1173
- row (`^customers collect { name values }` from a top-level row) and lifts
1208
+ row (`^customers { name values }` from a top-level row) and lifts
1174
1209
  nothing is evaluated the first time it is reached and its value reused for
1175
1210
  every other row (the same array/record by reference).
1176
1211
  - **Semi/anti-join short-circuit.** `exists` and `none` stop at the first bucket
@@ -1202,8 +1237,8 @@ bindings (`^customers`, `${customers}`, `^^config.items`) is evaluated once per
1202
1237
  run — its rows are read once and reused across the enclosing rows, with or
1203
1238
  without a correlation — and indexed once per path even if the context hands out
1204
1239
  a fresh array per read. Any other receiver is indexed the second time the
1205
- **same object** is probed, so a per-row relation (`lines collect { … where cid
1206
- == ^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
1207
1242
  scalar receiver is one row.
1208
1243
 
1209
1244
  **Pre-built indexes.** `DataContext.indexFor(collection, path)` is an optional
@@ -1234,7 +1269,7 @@ materialize` is the same seam for a `Value` that cannot be lazy on its own.
1234
1269
  ```js
1235
1270
  import { parse, run, IndexedCollection } from "@omgbase/oqx";
1236
1271
  const orders = new IndexedCollection("orders", orderRows, ["customer_id"]);
1237
- 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 } }"),
1238
1273
  { context: orders.context({ customers }) }); // probes the pre-built index
1239
1274
  ```
1240
1275
 
@@ -1359,7 +1394,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1359
1394
  import { parse, run, IndexedCollection, PlannedEngine } from "@omgbase/oqx";
1360
1395
  const planner = new IndexedCollection("people", people, ["city", "title"]);
1361
1396
  const engine = new PlannedEngine(planner, planner.context());
1362
- 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 });
1363
1398
  // city probe from the index; `age > 30` finished in-memory over the candidates
1364
1399
  ```
1365
1400
 
@@ -1389,7 +1424,8 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1389
1424
  (`Visitor`, `VisitContext`, `Clause` are the types); `CHILDREN` — the
1390
1425
  child-key table per node kind that drives them.
1391
1426
  - `print(node)` / `printTemplate(node)` — the canonical printer (`Template` is
1392
- `{ strings, count, indices }`).
1427
+ `{ strings, count, indices }`); it writes the collection-scoped form
1428
+ `S [consumer] [distinct] { … }` (0.26) and never the clause form.
1393
1429
  - `resolveAliases(query)` — the `select`-alias substitution the entry points
1394
1430
  apply before evaluation (SEMANTICS §14).
1395
1431
  - `references(query)` — every name the query reads from a row, with its
@@ -1400,7 +1436,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
1400
1436
  the display source of a template.
1401
1437
  - `build` — the node builders (`build.query`, `build.op`, `build.subquery`,
1402
1438
  `build.field`, `build.ident`, …).
1403
- - `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"`).
1404
1440
 
1405
1441
  **Types**
1406
1442