@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/CHANGELOG.md +7 -0
- package/README.md +243 -207
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +38 -35
- package/dist/engine.js.map +1 -1
- package/dist/optimize/analysis.d.ts +3 -2
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +3 -2
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +133 -32
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts +2 -2
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +79 -25
- package/dist/print.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/engine.ts +32 -28
- package/src/optimize/analysis.ts +3 -2
- package/src/parser.ts +121 -30
- package/src/print.ts +81 -28
- package/src/version.ts +1 -1
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.
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 `${…}`
|
|
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
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
[
|
|
61
|
-
[
|
|
62
|
-
[
|
|
63
|
-
[
|
|
64
|
-
[
|
|
65
|
-
[
|
|
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** (
|
|
70
|
-
keyword — with one piece of sugar (0.17): a
|
|
71
|
-
is syntactically a predicate is a `where`-first body (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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:
|
|
119
|
+
### 1. Source: the collection before the brace
|
|
98
120
|
|
|
99
|
-
Every query reads from a source collection
|
|
100
|
-
normally an interpolated value; it may also be a
|
|
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
|
|
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
|
|
136
|
+
oqx`${people} { select name, id }`;
|
|
114
137
|
// [{ name: "Bob", id: 124 }, { name: "Alice", id: 7 }, { name: "Carol", id: 55 }]
|
|
115
138
|
|
|
116
|
-
oqx
|
|
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
|
|
149
|
+
- a **nested collection** — `current: jobs { … }` (see §6).
|
|
127
150
|
|
|
128
151
|
```js
|
|
129
|
-
oqx
|
|
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
|
-
|
|
139
|
-
`select distinct` (§6), and is the only way to write a projection
|
|
140
|
-
item would otherwise be
|
|
141
|
-
projection always comes *
|
|
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
|
|
149
|
-
oqx
|
|
150
|
-
oqx`${people} first { name values where age > 50 }`;
|
|
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` (
|
|
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`$
|
|
169
|
-
oqx`$
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
225
|
+
oqx`entries(${flags}) { select $key values where on }`; // ["beta"] (bare `on` reads the value)
|
|
203
226
|
|
|
204
|
-
oqx
|
|
205
|
-
oqx
|
|
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.
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
228
|
-
oqx
|
|
229
|
-
oqx
|
|
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
|
|
251
|
-
oqx
|
|
252
|
-
oqx
|
|
253
|
-
oqx
|
|
254
|
-
oqx
|
|
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
|
|
280
|
-
oqx
|
|
281
|
-
oqx
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
364
|
-
oqx
|
|
365
|
-
oqx
|
|
366
|
-
oqx
|
|
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
|
|
389
|
-
oqx
|
|
390
|
-
oqx
|
|
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
|
|
422
|
-
oqx
|
|
423
|
-
oqx
|
|
424
|
-
oqx
|
|
425
|
-
oqx
|
|
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
|
-
|
|
451
|
-
different consumer,
|
|
452
|
-
this is *not* SQL:
|
|
453
|
-
|
|
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
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
is the
|
|
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
|
|
478
|
-
oqx
|
|
479
|
-
oqx
|
|
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 {
|
|
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` /
|
|
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
|
|
517
|
+
oqx`${people} { select name, current: jobs { employer where !end } where name == "Bob" }`;
|
|
492
518
|
// [{ name: "Bob", current: [{ employer: "Globocorp" }] }]
|
|
493
519
|
|
|
494
|
-
oqx
|
|
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
|
|
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 { … }`
|
|
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
|
|
508
|
-
oqx
|
|
509
|
-
oqx
|
|
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 `
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
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
|
|
581
|
-
oqx
|
|
582
|
-
oqx
|
|
583
|
-
oqx
|
|
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
|
|
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
|
|
632
|
+
oqx`${people} { group by city }`;
|
|
604
633
|
// [{ city: "NYC", count: 2 }, { city: "SF", count: 1 }]
|
|
605
|
-
oqx
|
|
606
|
-
oqx
|
|
607
|
-
oqx
|
|
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 {
|
|
623
|
-
out of a group. A nested block may group too (`jobs
|
|
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
|
-
|
|
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
|
|
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
|
|
650
|
-
oqx
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 (`
|
|
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
|
|
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
|
|
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)
|
|
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`
|
|
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
|
|
802
|
-
oqx
|
|
803
|
-
oqx
|
|
804
|
-
oqx
|
|
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
|
|
820
|
-
oqx
|
|
821
|
-
oqx
|
|
822
|
-
oqx
|
|
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
|
|
844
|
-
// argument; `x distinct { … }` is `x collect distinct { … }`.
|
|
845
|
-
// LEADING expression is syntactically a predicate drops `where`.
|
|
846
|
-
oqx
|
|
847
|
-
// ≡ name, current: jobs collect { where !end_date }
|
|
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
|
|
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
|
|
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
|
|
858
|
-
oqx
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
879
|
-
from ${people}` stays an error, because `where` would precede `from
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
children { depth 2 }`
|
|
883
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
-
// '
|
|
996
|
+
// 'people { name: person.name, n: jobs { employer: employer } where jobs exists { where !end } }'
|
|
963
997
|
|
|
964
|
-
toJSON(q); // { oqx: "0.
|
|
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
|
|
984
|
-
|
|
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
|
|
1000
|
-
// [ { name: "title", clause: "select", scope: { depth: 1, steps: [{ expr: <docs>, depth: 1 }] }, span: [
|
|
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("
|
|
1025
|
-
// `
|
|
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
|
|
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("
|
|
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('
|
|
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
|
|
1146
|
-
oqx
|
|
1147
|
-
oqx
|
|
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
|
|
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
|
|
1206
|
-
|
|
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
|
|
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('
|
|
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.
|
|
1439
|
+
- `LANGUAGE_VERSION` — the `spec/oqx/VERSION` this implementation conforms to (`"0.26"`).
|
|
1404
1440
|
|
|
1405
1441
|
**Types**
|
|
1406
1442
|
|