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