@omgbase/oqx 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +267 -202
- package/dist/ast.d.ts +19 -1
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +3 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +4 -2
- package/dist/build.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +95 -44
- 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 +12 -2
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +4 -1
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +1 -0
- package/dist/optimize/ir.js.map +1 -1
- package/dist/optimize/rules.d.ts.map +1 -1
- package/dist/optimize/rules.js +3 -2
- package/dist/optimize/rules.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +206 -42
- 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 +88 -25
- package/dist/print.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +39 -1
- package/dist/references.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +9 -2
- package/dist/resolve.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +1 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +5 -0
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +20 -1
- package/src/build.ts +5 -3
- package/src/engine.ts +89 -39
- package/src/optimize/analysis.ts +9 -2
- package/src/optimize/ir.ts +5 -1
- package/src/optimize/rules.ts +3 -2
- package/src/parser.ts +193 -41
- package/src/print.ts +92 -30
- package/src/references.ts +34 -2
- package/src/resolve.ts +10 -3
- package/src/version.ts +1 -1
- package/src/walk.ts +6 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ JavaScript tagged template.
|
|
|
16
16
|
> OQX is a language with more than one implementation; the specification is
|
|
17
17
|
> [`spec/oqx`](https://github.com/omgbase/omgbase/tree/main/spec/oqx). The
|
|
18
18
|
> language version is the package version's `major.minor` (`LANGUAGE_VERSION`,
|
|
19
|
-
> `"0.
|
|
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,11 +648,40 @@ Aggregates do not nest. `having` is the post-group `where` (it may name a select
|
|
|
619
648
|
alias: `having n > 1`); `order by` reads the group scope and is not rewritten
|
|
620
649
|
against aliases (`order by count()`, not `order by n`); `limit`/`offset` bound
|
|
621
650
|
the groups; `distinct` with `group by` is an error (redundant). `count { … }` is
|
|
622
|
-
now a value wherever a value stands (`n: jobs count {
|
|
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
|
|
|
656
|
+
### 8d. Flatten (0.25)
|
|
657
|
+
|
|
658
|
+
`flatten <expr> [{ … }] [as <name>]` — after `follow`, before `group by`; one or
|
|
659
|
+
more — splits one value into rows: each row becomes one row per element of the
|
|
660
|
+
value, the **parent row with `name` bound** beside its own properties at the
|
|
661
|
+
same depth (`$it`, `^` and a walk's intrinsics unchanged). An array → one row
|
|
662
|
+
per element (an empty list yields none); an object → one row; **absent → one
|
|
663
|
+
row with the name absent** (a missing field never drops the row); a scalar →
|
|
664
|
+
one row with the scalar (not the §3 error — nothing is sourced from it). A plain
|
|
665
|
+
navigation names itself by its last segment (`flatten tags` rebinds `tags`;
|
|
666
|
+
`flatten tags as t` keeps the list readable); anything else needs `as`. The
|
|
667
|
+
block is the receiver block over the elements, applied first (`flatten tags { $it
|
|
668
|
+
!= "b" } as t`; `flatten jobs { employer values order by pay desc limit
|
|
669
|
+
1 } as j`). Flattens chain (`flatten jobs as j flatten j.tags as t`), `where`
|
|
670
|
+
runs before them, `group by` groups the flattened rows (a bound name is a key),
|
|
671
|
+
`order by`/`limit` and every consumer apply to them, and a flattened row's
|
|
672
|
+
`$key` is the pair `[parent, element]`, so `distinct`/`is`/`follow` tell
|
|
673
|
+
`(doc, "a")` from `(doc, "b")`.
|
|
674
|
+
|
|
675
|
+
```js
|
|
676
|
+
oqx`${people} { select name, tags flatten tags }`;
|
|
677
|
+
// [{ name: "Bob", tags: "a" }, { name: "Bob", tags: "b" }, { name: "Carol" }] (Carol has no tags)
|
|
678
|
+
oqx`${people} { select name, t flatten tags { $it != "b" } as t }`;
|
|
679
|
+
oqx`${people} { select t, n: count() flatten tags as t group by t order by count() desc }`;
|
|
680
|
+
oqx`${people} count { flatten tags as t }`; // the number of (person, tag) rows
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Spec: `spec/oqx/SEMANTICS.md` §25.
|
|
684
|
+
|
|
627
685
|
### 9. Recursion: `follow`
|
|
628
686
|
|
|
629
687
|
`follow <destination>, …` turns a query into a bounded recursive traversal: the
|
|
@@ -637,7 +695,7 @@ const tree = [{ id: "root", children: [
|
|
|
637
695
|
{ id: "b", children: [] },
|
|
638
696
|
]}];
|
|
639
697
|
|
|
640
|
-
oqx
|
|
698
|
+
oqx`${tree} { select id, depth: $depth follow children order by $depth, id }`;
|
|
641
699
|
// [{ id: "root", depth: 1 }, { id: "a", depth: 2 }, { id: "b", depth: 2 }, { id: "a1", depth: 3 }]
|
|
642
700
|
```
|
|
643
701
|
|
|
@@ -652,12 +710,12 @@ whose body has two clauses of its own after `where`: `depth <n>` and
|
|
|
652
710
|
`frontier <pred>`:
|
|
653
711
|
|
|
654
712
|
```js
|
|
655
|
-
oqx
|
|
713
|
+
oqx`${tree} { select id, stop: $stop follow children { depth 2 } order by $ordinal }`;
|
|
656
714
|
// a1 is never reached; a and b report stop:"depth"
|
|
657
715
|
```
|
|
658
716
|
|
|
659
717
|
The intrinsics belong to the reached row's own scope like any other name: inside
|
|
660
|
-
a nested block (`kids: children
|
|
718
|
+
a nested block (`kids: children { … }`) a bare `$depth` is the nested
|
|
661
719
|
row's own `$depth` property (absent unless the data carries one), and the
|
|
662
720
|
occurrence's depth is `^$depth`. The same holds everywhere the metadata is not
|
|
663
721
|
present — outside a `follow`, `$depth`/`$ordinal`/`$stop`/`$leaf`/`$frontier`
|
|
@@ -677,7 +735,7 @@ A **plain destination** is a relation of the current row (`children`, `doc.out`,
|
|
|
677
735
|
`entries(kids)`, a tuple `(before, after)` — one destination over the union of
|
|
678
736
|
its elements; an outer reference `^rel` on its own or a literal is a parse
|
|
679
737
|
error). A **destination block** is a receiver followed by a block — `children {
|
|
680
|
-
depth 2 }`, `^people {
|
|
738
|
+
depth 2 }`, `^people { manager == ^id }`, `(before, after) { depth 2 }`,
|
|
681
739
|
or with its consumer, `^chain first distinct { … }` — the same directive you
|
|
682
740
|
would write in `select`, re-evaluated per frontier row with `^` bound to that
|
|
683
741
|
row. Its receiver *may* be an outer reference, so a walk can compute successors
|
|
@@ -686,13 +744,13 @@ that destination's block** (there is no options block any more):
|
|
|
686
744
|
|
|
687
745
|
```js
|
|
688
746
|
const people = [{ id: 1, manager: null }, { id: 2, manager: 1 }, { id: 3, manager: 1 }, { id: 4, manager: 2 }];
|
|
689
|
-
oqx
|
|
747
|
+
oqx`${people} { select id values where id == 1 follow ^people { manager == ^id } order by $ordinal }`;
|
|
690
748
|
// [1, 2, 3, 4] — the reporting tree under person 1 (`^people` names the root from a top-level walk)
|
|
691
|
-
oqx
|
|
749
|
+
oqx`${people} { select id values where id == 1 follow ^people { manager == ^id depth 2 } order by $ordinal }`;
|
|
692
750
|
// [1, 2, 3] — `depth` is a clause of the destination block, after its `where`
|
|
693
751
|
|
|
694
752
|
const chain = [{ id: "a" }, { id: "b", prev: "a" }, { id: "c", prev: "b" }];
|
|
695
|
-
oqx
|
|
753
|
+
oqx`${chain} { select id values where id == "b" follow distinct ^chain { id == ^prev }, ^chain { prev == ^id } order by $ordinal }`;
|
|
696
754
|
// ["b", "a", "c"] — both directions of a chain; a block may order, bound and `distinct` its rows
|
|
697
755
|
```
|
|
698
756
|
|
|
@@ -712,14 +770,14 @@ and `depth <n>` (1–8: this destination expands a frontier row only while its
|
|
|
712
770
|
the candidate successor's scope whose parent is the frontier row's — a bare name
|
|
713
771
|
is the candidate's own property, `^name` is the row being expanded, `^^name` the
|
|
714
772
|
walk's enclosing scope (language 0.14; before, `^` skipped the frontier row). So
|
|
715
|
-
`follow doc.in {
|
|
773
|
+
`follow doc.in { before.contains(^$path) }` steps only into rows that
|
|
716
774
|
name the row they were reached from. `frontier` is not correlated: it reads the
|
|
717
775
|
occurrence with `^` the enclosing scope, as the body does; seeds are reached
|
|
718
776
|
through no destination and are never frontiers by a destination's rule. **The
|
|
719
777
|
body that carries the `follow` may write `frontier <pred>` too**, after its
|
|
720
778
|
`where` and before `follow`: it applies to the seeds — a matching seed stays in
|
|
721
|
-
the result with `$stop == "frontier"` and is not expanded (`
|
|
722
|
-
!active follow children { depth 2 }`); read in the seed's scope like the body's
|
|
779
|
+
the result with `$stop == "frontier"` and is not expanded (`tree { id frontier
|
|
780
|
+
!active follow children { depth 2 } }`); read in the seed's scope like the body's
|
|
723
781
|
`where`. Each destination's `frontier` is its own; without a `follow`, a
|
|
724
782
|
body-level `frontier` is a parse error.
|
|
725
783
|
|
|
@@ -735,9 +793,9 @@ their shape):
|
|
|
735
793
|
|
|
736
794
|
```js
|
|
737
795
|
const n1 = { id: 1 }, n2 = { id: 2, next: [n1] }; n1.next = [n2]; // a cyclic graph of fresh references
|
|
738
|
-
oqx
|
|
796
|
+
oqx`${[n1]} { select id, stop: $stop, $key: id follow next { $key: id } order by $ordinal }`;
|
|
739
797
|
// [{ id: 1, stop: "interior", $key: 1 }, { id: 2, stop: "interior", $key: 2 }, { id: 1, stop: "cycle", $key: 1 }]
|
|
740
|
-
oqx
|
|
798
|
+
oqx`${people} { select name, $key: email follow next { $key: email } }`; // known by email at every step
|
|
741
799
|
```
|
|
742
800
|
|
|
743
801
|
Identities are compared as values across the whole walk, so destinations that
|
|
@@ -751,7 +809,8 @@ In row position an **array** is a collection and a non-array **object** is one
|
|
|
751
809
|
row: a block over it yields **one record** (or absent, when the row is filtered
|
|
752
810
|
out) rather than a one-element list, `values` the one value, `first`/`single`
|
|
753
811
|
that object — `doc { title }` is `{ title }`, `doc { status == "active" }` is the
|
|
754
|
-
object or absent (a conditional reach-through)
|
|
812
|
+
object or absent (a conditional reach-through) — at the top level exactly as in
|
|
813
|
+
a nested block.
|
|
755
814
|
A scalar in row position is an eval error (`list(s)` says "one element"
|
|
756
815
|
explicitly); a `from` re-projection or a `follow` is plural. A host says which
|
|
757
816
|
of its objects are collections through `DataContext.isSingle` (a lazy handle);
|
|
@@ -760,7 +819,7 @@ by default a non-iterable object is one row.
|
|
|
760
819
|
```js
|
|
761
820
|
oqx`${settings} { theme }`; // { theme: "dark" } (one object → one record)
|
|
762
821
|
oqx`${[settings]} { theme }`; // [{ theme: "dark" }] (a collection → a list)
|
|
763
|
-
oqx`
|
|
822
|
+
oqx`list(${settings}) { }`; // [settings] (the explicit one-element collection)
|
|
764
823
|
```
|
|
765
824
|
|
|
766
825
|
A **tuple** `(e1, e2, …)` — two or more expressions in parentheses — is a value
|
|
@@ -769,10 +828,10 @@ collection contributes its items, a row itself; an absent element nothing, a
|
|
|
769
828
|
`null` is kept; duplicates kept). One parenthesized expression is grouping.
|
|
770
829
|
|
|
771
830
|
```js
|
|
772
|
-
oqx
|
|
773
|
-
oqx
|
|
774
|
-
oqx
|
|
775
|
-
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
|
|
776
835
|
```
|
|
777
836
|
|
|
778
837
|
### 9c. Template literals (0.21)
|
|
@@ -787,10 +846,10 @@ literal may span lines. An interpolation holds any expression with balanced
|
|
|
787
846
|
braces — a `{ … }` block or another template literal included.
|
|
788
847
|
|
|
789
848
|
```js
|
|
790
|
-
oqx
|
|
791
|
-
oqx
|
|
792
|
-
oqx
|
|
793
|
-
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
|
|
794
853
|
```
|
|
795
854
|
|
|
796
855
|
Two rules keep it honest: **absent propagates** — any absent interpolation makes
|
|
@@ -807,59 +866,62 @@ unterminated-literal error).
|
|
|
807
866
|
Every form below is shorthand for an explicit directive — it **desugars in the
|
|
808
867
|
parser** to the nodes you already know (`print` writes the explicit form, the
|
|
809
868
|
optimizer and any pushdown planner see it), so nothing here has semantics of its
|
|
810
|
-
own except the postfix `!` and the infix `is
|
|
869
|
+
own except the postfix `!` and the infix `is`. (`print` writes the explicit
|
|
870
|
+
form — `where` for a leading predicate, `first { offset 0 }` for `[0]` — except
|
|
871
|
+
that `collect` itself prints as the bare brace, 0.26.)
|
|
811
872
|
|
|
812
873
|
```js
|
|
813
874
|
// A receiver block without a consumer is `collect` — wherever a value stands
|
|
814
|
-
// (language 0.20): a select item, a where leaf, a follow
|
|
815
|
-
// argument; `x distinct { … }` is `x collect distinct { … }`.
|
|
816
|
-
// LEADING expression is syntactically a predicate drops `where`.
|
|
817
|
-
oqx
|
|
818
|
-
// ≡ 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 } }
|
|
819
880
|
oqx`${people} { age > 40 }` // ≡ ${people} collect { where age > 40 }
|
|
820
|
-
oqx
|
|
881
|
+
oqx`${people} { n: size(jobs { !end_date }) }` // a block as a call argument
|
|
821
882
|
oqx`${people} distinct { city }` // ≡ ${people} collect distinct { city }
|
|
822
|
-
oqx`${people} count { jobs exists
|
|
883
|
+
oqx`${people} count { jobs exists }` // a consumer test is predicate-shaped too (a consumer word alone is the empty block, 0.26)
|
|
823
884
|
oqx`${people} first { name }` // a bare name still projects — unchanged
|
|
824
885
|
oqx`${people} first { is active }` // the bare-field filter: `is x` ≡ `!!x`, `not x` ≡ `!x`
|
|
825
886
|
|
|
826
887
|
// Brackets: `[p]` is `first { where p }`, `[n]` is `first { offset n }`;
|
|
827
888
|
// a trailing `!` requires the result (and makes the predicate form `single`).
|
|
828
|
-
oqx
|
|
829
|
-
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]! }`
|
|
830
891
|
// jobs[0] ≡ jobs first { offset 0 } (out of range ⇒ absent)
|
|
831
892
|
// jobs[pay > 2]! ≡ jobs single { where pay > 2 }! (zero or many matches ⇒ an eval error)
|
|
832
893
|
|
|
833
894
|
// Postfix `!` — required: the value, or an eval error naming the expression
|
|
834
895
|
// (and the row's `$key` when scalar). Never a filter, never a coercion: 0! and ""! are values.
|
|
835
|
-
oqx
|
|
896
|
+
oqx`${docs} { select id!, title }` // insist on an identity before a write
|
|
836
897
|
// Tightest precedence: `refs(c)[0]!.name` requires the lookup, `refs(c)[0].name!` the name.
|
|
837
898
|
|
|
838
899
|
// `is` / `is not` compare IDENTITY (`$key`: the host's, else structural — §9):
|
|
839
|
-
oqx
|
|
900
|
+
oqx`${people} { name where manager is null }` // absent (same as == null)
|
|
840
901
|
oqx`${people} count { ^people exists { $it is ^$it } }` // rows compare; `==` leaves that unspecified
|
|
841
902
|
// `and` / `or` / `not` are the canonical connectives; `&&` / `||` / `!` are exact
|
|
842
903
|
// synonyms (`title or path` coalesces). `print` writes the words (language 0.18); `!` stays `!`.
|
|
843
904
|
|
|
844
905
|
// A select item may use the items to its left (inlined like a `where` alias):
|
|
845
|
-
oqx
|
|
906
|
+
oqx`${people} { boss: ^people[id == ^manager], bossName: boss.name }`
|
|
846
907
|
```
|
|
847
908
|
|
|
848
909
|
Reserved words, never a bare field name: `from where select is not and or` and
|
|
849
|
-
the literal words `true false null`. The top level is unchanged —
|
|
850
|
-
from ${people}` stays an error, because `where` would precede `from
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
children { depth 2 }`
|
|
854
|
-
|
|
855
|
-
implicit `collect` (`follow ^people { where … }`, language 0.19).
|
|
910
|
+
the literal words `true false null`. The clause form's top level is unchanged —
|
|
911
|
+
`age > 15 from ${people}` stays an error, because `where` would precede `from`;
|
|
912
|
+
the collection-scoped `${people} { age > 15 }` is the filter. A brace after
|
|
913
|
+
*any* `follow` destination opens that destination's block with the implicit
|
|
914
|
+
`collect` (`follow children { depth 2 }`, `follow ^people { where … }`; the
|
|
915
|
+
options block of 0.19 is gone since language 0.20).
|
|
856
916
|
|
|
857
917
|
### Cheat-sheet
|
|
858
918
|
|
|
859
919
|
```
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
920
|
+
${source} { … } / ${source} count { … } a query: the collection heads the body; a consumer word may stand before the brace; nothing follows it
|
|
921
|
+
select … from … where … follow … flatten … group by … having … order by … limit N offset N the fixed clause order inside a body (each at most once)
|
|
922
|
+
name, alias: expr, nested: rel { … } projection (`select` may be dropped only here, in first position)
|
|
923
|
+
from rel re-projection: each row replaced by the rows `rel` yields (flatMap) — a body never names its source
|
|
924
|
+
select name from ${source} where … the clause form (pre-0.26): still read, same tree, no longer printed
|
|
863
925
|
where a == b and rel exists { where … } or not c predicate tree + nested ops (`&&`/`||`/`!` are accepted synonyms)
|
|
864
926
|
rel { pay > 2 } / rel { employer } a block without a consumer is collect; a leading predicate is where-first, a bare name projects
|
|
865
927
|
rel[p] / rel[p]! / rel[0] / rel[0]! first match / exactly one (required) / positional / required positional
|
|
@@ -871,17 +933,17 @@ where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl.
|
|
|
871
933
|
where x in range(field) coerce a string field to a range, then test coverage
|
|
872
934
|
name the CURRENT row's field only (never climbs)
|
|
873
935
|
$it the current item itself (a scalar row, or the whole object)
|
|
874
|
-
|
|
936
|
+
entries(obj) { … $key / $it } a record's properties as a collection (key + value; bare names read the value)
|
|
875
937
|
<expr> values scalar projection: the value, not a { name: value } record
|
|
876
938
|
^name / ^^name read an enclosing row's field (exactly N scopes out); ^$it = the enclosing row
|
|
877
939
|
^name: expr / ^^name: expr lift/export a value N scopes out (flatten-append)
|
|
878
|
-
^rel
|
|
940
|
+
^rel { … } / ^^root exists { … } nested consumer over an enclosing row's relation / a named root
|
|
879
941
|
entries(rel) exists { … } a free-function call may be a receiver
|
|
880
942
|
order by expr desc, expr2 ordering
|
|
881
943
|
limit n / offset n bound the row set (after where/order/distinct, before the consumer)
|
|
882
944
|
follow rel { where … frontier … depth n } recursion ($depth/$stop/$leaf/$frontier/$key); the brace is the destination's block, `^name` in its where the frontier row
|
|
883
945
|
follow before, after / follow (before, after) several destinations: the union of their rows (per step, by identity) / one tuple destination
|
|
884
|
-
follow ^people {
|
|
946
|
+
follow ^people { manager == ^id } a destination block: successors computed per frontier row (`^` = that row; `collect` implicit, 0.20)
|
|
885
947
|
$key / $key: expr the identity a row is known by (the host's, else structural) / give the rows a block yields that identity
|
|
886
948
|
(a, b) / rel distinct { … } a tuple: the elements, one level flat (a value and a receiver) / collect distinct (0.20)
|
|
887
949
|
`${title} (${era})` a template literal: text + string forms; absent anywhere → absent; a list/record → error (0.21)
|
|
@@ -914,7 +976,7 @@ data (no parent pointers, no methods) and *are* the JSON shape.
|
|
|
914
976
|
```js
|
|
915
977
|
import { parse, visit, transform, print, printTemplate, toJSON, stripSpans, build } from "@omgbase/oqx";
|
|
916
978
|
|
|
917
|
-
const q = parse('select name, n: jobs
|
|
979
|
+
const q = parse('people { select name, n: jobs { employer } where jobs exists { !end } }');
|
|
918
980
|
|
|
919
981
|
// visit: every node, with its ancestors, the clause it sits in, and the scope depth
|
|
920
982
|
const relations = [];
|
|
@@ -928,11 +990,12 @@ visit(q, {
|
|
|
928
990
|
// transform: rebuild with every expression mapped (children first; untouched nodes keep identity + spans)
|
|
929
991
|
const rewritten = transform(q, (e) => (e.kind === "ident" && e.name === "name" ? build.member(build.ident("person"), "name") : e));
|
|
930
992
|
|
|
931
|
-
// print: the canonical source — single spaces,
|
|
993
|
+
// print: the canonical source — collection-scoped, single spaces, the projection leading
|
|
994
|
+
// without `select`, every item aliased, `collect` as the bare brace, minimal parentheses
|
|
932
995
|
print(rewritten);
|
|
933
|
-
// '
|
|
996
|
+
// 'people { name: person.name, n: jobs { employer: employer } where jobs exists { where !end } }'
|
|
934
997
|
|
|
935
|
-
toJSON(q); // { oqx: "0.
|
|
998
|
+
toJSON(q); // { oqx: "0.26", kind: "query", span: [0, 71], … } — the shared JSON document
|
|
936
999
|
stripSpans(q); // the same tree without spans, for shape comparisons
|
|
937
1000
|
```
|
|
938
1001
|
|
|
@@ -951,8 +1014,9 @@ place.
|
|
|
951
1014
|
`print(parse(q)) === q` is *not* a law — spelling is normalized. A binding is a
|
|
952
1015
|
value, never source text: `print` throws (`OqxError`, stage `"print"`) on one,
|
|
953
1016
|
and `printTemplate(node)` returns `{ strings, count, indices }` — the fragments
|
|
954
|
-
around each binding and the binding each gap stands for (the canonical
|
|
955
|
-
|
|
1017
|
+
around each binding and the binding each gap stands for (the canonical form
|
|
1018
|
+
can move one past another: the clause form `select x: ${1} from ${0}` prints as
|
|
1019
|
+
`${0} { x: ${1} }`, `indices = [1, 0]`), to re-run with `parseTemplate(strings,
|
|
956
1020
|
count)` and the values permuted by `indices`.
|
|
957
1021
|
|
|
958
1022
|
`build.*` constructs nodes with the empty span `[0, 0]` and the materialized
|
|
@@ -967,8 +1031,8 @@ it is read from — so a host that knows what rows an expression yields can say
|
|
|
967
1031
|
which names exist there:
|
|
968
1032
|
|
|
969
1033
|
```ts
|
|
970
|
-
references(parse('select title, age
|
|
971
|
-
// [ { 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] },
|
|
972
1036
|
// { name: "age", clause: "select", scope: { depth: 1, steps: [<docs>@1] }, … },
|
|
973
1037
|
// { name: "docs", clause: "source", scope: { depth: 0, steps: [] }, … }, // a root name
|
|
974
1038
|
// { name: "blocks", clause: "where", scope: { depth: 1, steps: [<docs>@1] }, … },
|
|
@@ -992,8 +1056,8 @@ When you don't need interpolation, `execute` runs a plain string query against a
|
|
|
992
1056
|
```js
|
|
993
1057
|
import { execute } from "@omgbase/oqx";
|
|
994
1058
|
|
|
995
|
-
execute("
|
|
996
|
-
// `
|
|
1059
|
+
execute("people { select name where age >= 18 }", { people });
|
|
1060
|
+
// the collection `people` resolves the root of that name
|
|
997
1061
|
```
|
|
998
1062
|
|
|
999
1063
|
`parse(source)` returns a reusable AST and `run(query, { values, roots })` returns
|
|
@@ -1041,7 +1105,7 @@ const graph = {
|
|
|
1041
1105
|
toRows: (v) => v == null ? [] : Array.isArray(v) ? v : [v],
|
|
1042
1106
|
identityOf: (row) => row.id, // `$key`: for follow dedup, distinct, `is` (optional; default: structural)
|
|
1043
1107
|
};
|
|
1044
|
-
run(parse("id, depth: $depth
|
|
1108
|
+
run(parse("tree { select id, depth: $depth follow children }"), { context: graph });
|
|
1045
1109
|
```
|
|
1046
1110
|
|
|
1047
1111
|
#### Custom functions and methods
|
|
@@ -1072,7 +1136,7 @@ const ctx = {
|
|
|
1072
1136
|
return builtins.callMethod(name, recv, args);
|
|
1073
1137
|
},
|
|
1074
1138
|
};
|
|
1075
|
-
run(parse("
|
|
1139
|
+
run(parse("tree { select id where age(born) > 18 and title.slug() == 'x' }"), { context: ctx });
|
|
1076
1140
|
```
|
|
1077
1141
|
|
|
1078
1142
|
The simplest route is to `extends DefaultContext` and `super.callFunction(...)`
|
|
@@ -1098,7 +1162,7 @@ import { parse, PlannedEngine } from "@omgbase/oqx";
|
|
|
1098
1162
|
import { SqliteTable } from "@omgbase/oqx/sqlite";
|
|
1099
1163
|
|
|
1100
1164
|
const planner = new SqliteTable(db, "emp", { columns: ["id", "name", "dept", "level"] });
|
|
1101
|
-
new PlannedEngine(planner).run(parse('
|
|
1165
|
+
new PlannedEngine(planner).run(parse('emp { select name where dept == "eng" and level >= 5 }'), []);
|
|
1102
1166
|
// → `dept`/`level` pushed to SQL; anything untranslatable finishes in-memory
|
|
1103
1167
|
```
|
|
1104
1168
|
|
|
@@ -1113,9 +1177,9 @@ A nested block runs once per enclosing row, so the natural way to join two
|
|
|
1113
1177
|
collections —
|
|
1114
1178
|
|
|
1115
1179
|
```js
|
|
1116
|
-
oqx
|
|
1117
|
-
oqx
|
|
1118
|
-
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 } }`;
|
|
1119
1183
|
```
|
|
1120
1184
|
|
|
1121
1185
|
— would be quadratic if the block scanned its receiver each time. It does not:
|
|
@@ -1141,7 +1205,7 @@ results and on errors.
|
|
|
1141
1205
|
objects and arrays by reference. 5 000 orders × 5 000 customers zip in a few
|
|
1142
1206
|
milliseconds; the scan takes seconds.
|
|
1143
1207
|
- **Invariant blocks run once.** A block that reads nothing from any enclosing
|
|
1144
|
-
row (`^customers
|
|
1208
|
+
row (`^customers { name values }` from a top-level row) and lifts
|
|
1145
1209
|
nothing is evaluated the first time it is reached and its value reused for
|
|
1146
1210
|
every other row (the same array/record by reference).
|
|
1147
1211
|
- **Semi/anti-join short-circuit.** `exists` and `none` stop at the first bucket
|
|
@@ -1173,8 +1237,8 @@ bindings (`^customers`, `${customers}`, `^^config.items`) is evaluated once per
|
|
|
1173
1237
|
run — its rows are read once and reused across the enclosing rows, with or
|
|
1174
1238
|
without a correlation — and indexed once per path even if the context hands out
|
|
1175
1239
|
a fresh array per read. Any other receiver is indexed the second time the
|
|
1176
|
-
**same object** is probed, so a per-row relation (`lines
|
|
1177
|
-
|
|
1240
|
+
**same object** is probed, so a per-row relation (`lines { … where cid ==
|
|
1241
|
+
^id }` with a different `lines` array per row) is scanned, not indexed. A
|
|
1178
1242
|
scalar receiver is one row.
|
|
1179
1243
|
|
|
1180
1244
|
**Pre-built indexes.** `DataContext.indexFor(collection, path)` is an optional
|
|
@@ -1205,7 +1269,7 @@ materialize` is the same seam for a `Value` that cannot be lazy on its own.
|
|
|
1205
1269
|
```js
|
|
1206
1270
|
import { parse, run, IndexedCollection } from "@omgbase/oqx";
|
|
1207
1271
|
const orders = new IndexedCollection("orders", orderRows, ["customer_id"]);
|
|
1208
|
-
run(parse("select name, o: ^orders
|
|
1272
|
+
run(parse("customers { select name, o: ^orders { id values where customer_id == ^id } }"),
|
|
1209
1273
|
{ context: orders.context({ customers }) }); // probes the pre-built index
|
|
1210
1274
|
```
|
|
1211
1275
|
|
|
@@ -1330,7 +1394,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
1330
1394
|
import { parse, run, IndexedCollection, PlannedEngine } from "@omgbase/oqx";
|
|
1331
1395
|
const planner = new IndexedCollection("people", people, ["city", "title"]);
|
|
1332
1396
|
const engine = new PlannedEngine(planner, planner.context());
|
|
1333
|
-
run(parse('
|
|
1397
|
+
run(parse('people { select name where city == "NYC" and age > 30 }'), { engine });
|
|
1334
1398
|
// city probe from the index; `age > 30` finished in-memory over the candidates
|
|
1335
1399
|
```
|
|
1336
1400
|
|
|
@@ -1360,7 +1424,8 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
1360
1424
|
(`Visitor`, `VisitContext`, `Clause` are the types); `CHILDREN` — the
|
|
1361
1425
|
child-key table per node kind that drives them.
|
|
1362
1426
|
- `print(node)` / `printTemplate(node)` — the canonical printer (`Template` is
|
|
1363
|
-
`{ strings, count, indices }`)
|
|
1427
|
+
`{ strings, count, indices }`); it writes the collection-scoped form
|
|
1428
|
+
`S [consumer] [distinct] { … }` (0.26) and never the clause form.
|
|
1364
1429
|
- `resolveAliases(query)` — the `select`-alias substitution the entry points
|
|
1365
1430
|
apply before evaluation (SEMANTICS §14).
|
|
1366
1431
|
- `references(query)` — every name the query reads from a row, with its
|
|
@@ -1371,7 +1436,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
1371
1436
|
the display source of a template.
|
|
1372
1437
|
- `build` — the node builders (`build.query`, `build.op`, `build.subquery`,
|
|
1373
1438
|
`build.field`, `build.ident`, …).
|
|
1374
|
-
- `LANGUAGE_VERSION` — the `spec/oqx/VERSION` this implementation conforms to (`"0.
|
|
1439
|
+
- `LANGUAGE_VERSION` — the `spec/oqx/VERSION` this implementation conforms to (`"0.26"`).
|
|
1375
1440
|
|
|
1376
1441
|
**Types**
|
|
1377
1442
|
|