@omgbase/oqx 0.18.0 → 0.21.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 +30 -0
- package/README.md +177 -64
- package/dist/ast.d.ts +28 -5
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +11 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -3
- package/dist/build.js.map +1 -1
- package/dist/context.d.ts +19 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -9
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +12 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +263 -84
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +3 -3
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +79 -2
- 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 +18 -15
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +250 -118
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +47 -29
- package/dist/print.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +15 -8
- 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 +5 -3
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +34 -11
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +33 -6
- package/src/build.ts +12 -8
- package/src/context.ts +22 -11
- package/src/engine.ts +258 -91
- package/src/index.ts +2 -1
- package/src/lexer.ts +76 -6
- package/src/optimize/analysis.ts +17 -12
- package/src/parser.ts +227 -101
- package/src/print.ts +44 -16
- package/src/resolve.ts +15 -7
- package/src/version.ts +1 -1
- package/src/walk.ts +39 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to `@omgbase/oqx` are recorded here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/) (pre-1.0: a minor bump may break).
|
|
6
6
|
|
|
7
|
+
## [0.21.0] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Minor
|
|
10
|
+
- **OQX 0.21 — template literals** (`spec/oqx` 0.20 → 0.21; new suite `template-literals` (50 cases) + 2 in `errors-eval`: 1258 → 1310 fixtures). `` `text ${expr} text` `` is a backtick-delimited **string value** with `${…}` interpolations, usable wherever a value stands — a `select` item (`` label: `${title} (${era})` ``), a `where` operand, an `order by` key, a function argument or method receiver, the haystack of `in`, a nested block's item reading the enclosing row (`` jobs collect { label: `${^title}: ${employer}` values } ``). Semantics (SEMANTICS §7b): the text and each interpolated value's **string form** (§7: a number without a fraction when integer-valued, `true`/`false` as the words) concatenated; **absent propagates** — any absent interpolation makes the whole literal absent, as `+` does; **a list, a record or a range cannot be interpolated** — an eval error `cannot interpolate … it is a list / a record / a range` (where `+` leaves such operands unspecified, a template literal refuses them); every interpolation is evaluated, in order, before the result is decided. Lexically (GRAMMAR §1, §4): the text decodes with the **same escapes as a string** (`\n` `\t` `\r` `\0`, `\<c>` for any other `<c>` — so `` \` `` keeps a backtick, `\$` a dollar that opens nothing, `\\` a backslash); **newlines are kept verbatim** (a literal may span lines); a `$` not followed by `{`, braces outside an interpolation and quotes are text; an interpolation holds one full expression with **balanced braces**, so it may hold a `{ … }` block (`` `${jobs single { employer values where primary }}` ``) or another template literal, to any depth, read at the enclosing scope depth (`^`, `N^` as usual). Errors: `unterminated template literal at N` and `` unterminated ${…} interpolation … `` are lex errors; `` empty interpolation `${}` `` (offset of the `${`) and `expected '}' to close the `${…}` interpolation` are parse errors; a template literal is a literal — not a receiver, not a bracket receiver, and an unaliased item needs an alias. **Bindings**: a tagged-template fragment that ends inside a backtick literal is the lex error `unterminated template literal … a ${…} binding cannot stand inside a backtick literal` (fragments lex independently). **AST** (AST.md): new node `template { parts: (string | Expr)[] }` with `parts` strictly alternating decoded text strings and expressions (odd length, text first and last: `` `${a}` `` is `["", a, ""]`); the text parts are plain strings, not nodes — `visit`/`transform` skip them; `CHILDREN.template = [parts]`; `isExpr` true; `build.template(parts)`; a `template` node spans its backticks, each interpolated expression itself. **Printer**: a template literal prints as one, its text escaped (`\\`, `` \` ``, `\$` before a `{`, the control escapes) and each interpolation as `${…}`; round-trip law holds over every fixture. Lexer: new token types `tmpl_start` / `tmpl_text` / `tmpl_interp_start` / `tmpl_interp_end` / `tmpl_end`, a mode stack per fragment; the backtick is no longer an `unexpected character`. Planners: the SQLite adapter leaves a template residual (not translatable); `exprRaiseFree` is false for a literal with an interpolation (the data decides whether it raises). `LANGUAGE_VERSION` 0.21.
|
|
11
|
+
- `@omgbase/oqx-syntax`: `LANGUAGE_VERSION` 0.21; new token classes `template` (`string.template.oqx`) and `interpolation` (`punctuation.section.interpolation.oqx`); every grammar enters a template state at the backtick, re-enters the language at `${` and tracks the braces of the interpolation (TextMate begin/end rules `template` / `template-interpolation` / `template-block`, Monarch states `template` / `templateInterpolation` / `templateBlock`, a mode stack in the CodeMirror stream state, a Prism `template` token whose interpolations re-enter the grammar through `rest`, braces balanced to three levels); the backtick left `ILLEGAL_CHARACTERS`; `TEMPLATE` / `TEMPLATE_LITERAL` exported; backtick auto-closing pairs; assets regenerated; parity over every fixture.
|
|
12
|
+
|
|
13
|
+
## [0.20.0] - 2026-10-11
|
|
14
|
+
|
|
15
|
+
### Minor
|
|
16
|
+
- OQX 0.20 (`spec/oqx` 0.19 → 0.20; 1188 → 1258 fixtures, three new suites `tuples`, `identity`, `cardinality`):
|
|
17
|
+
|
|
18
|
+
- **`follow` loses its options block.** A brace after *any* destination opens that destination's block (`follow children { depth 2 }`, `follow a, children { id != "root" }`, `follow entries(kids) { where … }`), with the 0.17 body rules. Two clauses belong to a destination block alone, after `where` in the fixed order: `depth <1..8>` (this destination expands a frontier row only while its `$depth < n`) and `frontier <expr>` (an occurrence reached through this destination for which it holds is a frontier; read with `^` the walk's enclosing scope; seeds are never frontiers). The former `where` option is the block's `where`; `by` is gone. When several destinations reach a row in one step the first governs the occurrence. **The body that carries the `follow` may carry `frontier <expr>` too** (Brendan, 2026-10-11): it applies to the SEEDS — a seed for which it holds (read in the seed's own scope, `^` the enclosing scope, like `where`) is `$stop == "frontier"` and is not expanded, while it stays in the result — so every block that feeds rows into the walk says which of its rows are frontiers, and the frontier relevant to the current `$it` is defined separately from each destination's. The clause order of every body is `select from where frontier follow order by limit offset` (a destination block's adds `depth` after `frontier`); a body-level `frontier` without a `follow` is the parse error `` `frontier` needs a `follow` ``, a body-level `depth` an error naming the destination block; inside a destination block `frontier` is always the destination's. AST: `follow` keeps only `destinations`/`distinct`; `subquery` gains `frontier`/`depth`, `query` gains `frontier`; `print` writes `follow x collect { where … depth 2 }` and a body's `frontier` between `where` and `follow`.
|
|
19
|
+
- **Tuples.** `(e1, e2, …)` with two or more elements is a value and a receiver: the elements concatenated with one level of flattening (a collection contributes its items, a row itself; an absent element nothing, a `null` is kept; duplicates kept). `size((a, b))`, `"x" in (tags, aliases)`, `(mom, dad, siblings) { name values }`, `follow (before, after) { depth 2 }` (one destination over the union). New AST node `tuple { items }`.
|
|
20
|
+
- **Identity is `$key`.** One meaning — the key a row is known by — for `follow` cycle detection, the per-step union, `follow distinct`, `distinct` over unprojected rows, `is`, and reading `$key` on any row. The default is the host's identity through a new `DataContext` seam (TS: optional `identityOf(row)`, replacing the required `identity`, which stays as a deprecated alias for one minor; Rust: `identity` now has a structural default), else the row's structural value; an entry's identity is its key. The language never names an `id` property: the "`id` if present" rule is removed, and the in-memory `DefaultContext` names no identity. A block may project `$key: <expr>` to give the rows it yields that identity (`select name, $key: email from people follow next { $key: email }`); in a destination block `$key:` is not a column. Caveat: identities compare as values across the whole walk, so destinations that can reach the same rows should project the same `$key`.
|
|
21
|
+
- **Result shape by receiver cardinality.** In row position an array is a collection and a non-array object is one row: a `collect` over it — `doc { title }`, `title from doc`, a nested `d: dst { title }` — yields the one record (or absent when filtered out; `doc { status == "active" }` is a conditional reach-through), not a one-element list; `values` the one value; `first`/`single` that object. A scalar in row position is an eval error (`` `s` is a string ("abc"), not a collection or an object ``) instead of a one-row collection — `list(s)` says it explicitly. A `from` re-projection or a `follow` makes the result plural. Hosts say which of their objects are collections through the new optional `DataContext.isSingle` (TS; default: a non-iterable object is one row) / `is_single` (Rust; default `true`). TS `OqxResult` for `collect` gains `single: boolean` (`unwrap` yields the record or `null`); Rust gains `OqxResult::Record(Option<Value>)`.
|
|
22
|
+
- **Sugar closures.** `x { … }` ≡ `x collect { … }` wherever a value is accepted, function arguments included (`size(children { is checked })`); `x distinct { … }` ≡ `x collect distinct { … }`.
|
|
23
|
+
|
|
24
|
+
omgbase binding (surface stays 3.0; the dialect follows the language):
|
|
25
|
+
|
|
26
|
+
- `DataContext.identityOf` (TS) / `identity` (Rust) answers a store row's id for every kind (`doc_id`, `block_id`, `node_id`, `edge_id`), so `$key` on any hit is its id and walks dedup by it as before; the binding no longer relies on the removed `id`-property rule.
|
|
27
|
+
- The statically single relations `doc`, `block` and `dst` hand out an absent-single marker when they have no row, so `dst { title }` on a link row is one record and, on an external or dangling link, absent — not `[]`; as a value the marker is absent (`dst == null`, `has(dst)`, `dst.title` unchanged). The Rust context's `is_single` marks the lazy root scans as collections.
|
|
28
|
+
- The `graph` tool compiles to `follow distinct <dir>_links collect { from <dst|doc> depth <n> }`; `query_syntax`, the `query` tool description, both help cards, `docs/query-language.md`, `docs/graph-and-query.md`, `docs/mcp-api.md` and the OQX tutorial describe the 0.20 forms (body-level `frontier` included); the planners treat a destination block's `frontier`/`depth` as its own, never push a tuple down, and a body with `frontier` plans like one with a `follow` — in memory (pinned on both sides). `spec/surface` `query-follow::body-frontier-keeps-seeds-unexpanded` walks the processes with the unverified one kept but not expanded.
|
|
29
|
+
- `spec/surface` fixtures using the old options block are rewritten to the destination-block forms (`… collect { from dst depth 2 }`; `by type` → `{ $key: type from dst }`) and regenerated; `spec/cli` and `interop.json` regenerated with them.
|
|
30
|
+
- `@omgbase/oqx-syntax`: `LANGUAGE_VERSION` 0.20; `FOLLOW_OPTIONS` → `DESTINATION_CLAUSES` (`frontier`, `depth`; `by` is no longer a word of the language), assets regenerated.
|
|
31
|
+
|
|
32
|
+
## [0.19.0] - 2026-10-10
|
|
33
|
+
|
|
34
|
+
### Minor
|
|
35
|
+
- OQX language 0.19 — outer-reference `follow` destinations take the implicit `collect`. A `follow` destination whose receiver is headed by an outer reference (`^docs`, `^^root.rel`, `0^docs`, `^$it.docs`, with any member chain) can never be a plain destination (plain destinations are relations of the current row), so a `{` right after it now opens a **destination block** with the implicit `collect`: `follow refs(before), ^docs { where ^$path in list(after) }` walks `before` references forward and, backward, every document whose `after` names the frontier row — exactly `^docs collect { … }` with the 0.17 body rules (where-first, projections). Until 0.18 that query failed with the outer-reference error, because the 0.17 carve-out reserved a brace after the last destination for the options block. Any other receiver (a bare relation, a call like `refs(before)` or `entries(kids)`, a binding) keeps that reading: a brace after it is the options block when it is the last destination (`follow children { depth 2 }`, `follow a, children { depth 2 }`) and the "a destination block needs its consumer" error when the brace holds a predicate. An options block may still follow a trailing destination block (`follow ^docs { where … } { depth 2 }`). A plain `follow ^children` (no brace, no consumer) is still an error; its message now names both block spellings: `` `follow` takes a relation of the current row (`follow <relation>`); an outer reference walks a root collection only as a destination block — `follow ^docs { where … }` or `follow ^docs collect { … }` ``. Fixtures: 25 new cases across `follow`, `sugar`, `errors-parse` and `ast` (1188 total); the 0.17 sugar case "an outer-reference destination with a brace is the outer-reference error" is replaced by its positive form. `LANGUAGE_VERSION` is 0.19 in both engines and in `@omgbase/oqx-syntax`; the `query_syntax` reference (`@omgbase/core`, `omgbase-surface`) describes the new rule.
|
|
36
|
+
|
|
7
37
|
## [0.18.0] - 2026-10-10
|
|
8
38
|
|
|
9
39
|
### Minor
|
package/README.md
CHANGED
|
@@ -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
|
|
@@ -200,19 +200,22 @@ const flags = { beta: { on: true }, legacy: { on: false } };
|
|
|
200
200
|
oqx`$key values from entries(${flags}) where on`; // ["beta"] (bare `on` reads the value)
|
|
201
201
|
|
|
202
202
|
oqx`name, on: entries(prefs) collect { $key values where $it } from ${users}`; // as a nested receiver
|
|
203
|
-
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 }`;
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
`entries(array)` yields numeric index keys, a `Map` yields its entries, and
|
|
207
|
-
absence/scalars yield nothing. `$key`
|
|
208
|
-
|
|
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
|
|
209
211
|
ask for one. As a plain value (not a source) `entries(x)` is an array of
|
|
210
212
|
`{ key, value }` records.
|
|
211
213
|
|
|
212
214
|
### 3. Predicates (where)
|
|
213
215
|
|
|
214
216
|
`where` filters rows. The predicate language has comparisons (`== != < <= > >=`),
|
|
215
|
-
boolean operators
|
|
217
|
+
boolean operators — the words `and`, `or`, `not`; the symbols `&& || !` are
|
|
218
|
+
accepted synonyms — with grouping `( )`, membership (`in`), arithmetic
|
|
216
219
|
(`+ - * / %`), and bare truthiness. The `where` keyword is always written — a
|
|
217
220
|
leading expression without it is a projection, and a bare comparison in that
|
|
218
221
|
position is an error that points you at `where`.
|
|
@@ -319,7 +322,7 @@ from a row nested one level deeper. A receiver may start with `^` too, which is
|
|
|
319
322
|
how a nested consumer runs over a named root or an enclosing row's relation:
|
|
320
323
|
|
|
321
324
|
```js
|
|
322
|
-
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 });
|
|
323
326
|
```
|
|
324
327
|
|
|
325
328
|
**Correlated subqueries.** Because inner and outer rows often share names, the
|
|
@@ -334,7 +337,7 @@ const family = [
|
|
|
334
337
|
];
|
|
335
338
|
oqx`
|
|
336
339
|
name,
|
|
337
|
-
siblings: ${family} collect { name where parent == ^parent
|
|
340
|
+
siblings: ${family} collect { name where parent == ^parent and name != ^name }
|
|
338
341
|
from ${family}
|
|
339
342
|
`;
|
|
340
343
|
// [{ name: "Ada", siblings: [{ name: "Ben" }] },
|
|
@@ -357,7 +360,7 @@ Methods on a value: `contains`, `startsWith`, `endsWith`, `matches` (regex),
|
|
|
357
360
|
```js
|
|
358
361
|
oqx`name from ${people} where title.startsWith("Eng")`; // → Bob
|
|
359
362
|
oqx`name from ${people} where title.lower() == "director"`; // → Alice
|
|
360
|
-
oqx`name from ${people} where has(age)
|
|
363
|
+
oqx`name from ${people} where has(age) and not has(nickname)`; // present vs absent
|
|
361
364
|
oqx`name from ${people} where tags.contains("admin")`; // array membership
|
|
362
365
|
```
|
|
363
366
|
|
|
@@ -591,8 +594,10 @@ Reached rows expose recursion **intrinsics** in `select` / `order by`:
|
|
|
591
594
|
graph beyond — a boundary or the depth cap), `$ordinal` (a deterministic 1..N
|
|
592
595
|
rank over the walk, ordered by depth then path), and `$stop`
|
|
593
596
|
(`"interior"` / `"leaf"` / `"frontier"` / `"depth"` / `"cycle"`, precedence
|
|
594
|
-
cycle > frontier > depth > leaf > interior — only `interior` rows expand).
|
|
595
|
-
|
|
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>`:
|
|
596
601
|
|
|
597
602
|
```js
|
|
598
603
|
oqx`id, stop: $stop from ${tree} follow children { depth 2 } order by $ordinal`;
|
|
@@ -604,10 +609,10 @@ a nested block (`kids: children collect { … }`) a bare `$depth` is the nested
|
|
|
604
609
|
row's own `$depth` property (absent unless the data carries one), and the
|
|
605
610
|
occurrence's depth is `^$depth`. The same holds everywhere the metadata is not
|
|
606
611
|
present — outside a `follow`, `$depth`/`$ordinal`/`$stop`/`$leaf`/`$frontier`
|
|
607
|
-
are ordinary property reads (a row `{ "$depth": 3 }` projects `3`)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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).
|
|
611
616
|
|
|
612
617
|
The walk is **per-path**: a node reached by N distinct paths yields N
|
|
613
618
|
occurrences, and revisiting an identity already on the current path is admitted
|
|
@@ -616,42 +621,134 @@ runaway. `follow distinct` collapses occurrences to reached nodes (the minimal
|
|
|
616
621
|
`(depth, path)` per identity).
|
|
617
622
|
|
|
618
623
|
**Destinations** (language 0.14) are comma-separated; the walk is their union.
|
|
619
|
-
A **plain destination** is a relation of the current row (`children`, `doc.out
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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):
|
|
625
634
|
|
|
626
635
|
```js
|
|
627
636
|
const people = [{ id: 1, manager: null }, { id: 2, manager: 1 }, { id: 3, manager: 1 }, { id: 4, manager: 2 }];
|
|
628
|
-
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`;
|
|
629
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`
|
|
630
641
|
|
|
631
642
|
const chain = [{ id: "a" }, { id: "b", prev: "a" }, { id: "c", prev: "b" }];
|
|
632
|
-
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`;
|
|
633
644
|
// ["b", "a", "c"] — both directions of a chain; a block may order, bound and `distinct` its rows
|
|
634
645
|
```
|
|
635
646
|
|
|
636
|
-
A row's successors are, per destination in order, that destination's rows
|
|
637
|
-
|
|
638
|
-
node reached by two destinations (or twice by one relation) in the
|
|
639
|
-
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
|
|
640
652
|
where-position tests).
|
|
641
653
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
`
|
|
647
|
-
|
|
648
|
-
the
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
### 9c. Template literals (0.21)
|
|
727
|
+
|
|
728
|
+
`` `text ${expr} text` `` is a string value: the text and each interpolated
|
|
729
|
+
value's **string form** (numbers without a fraction when integer-valued,
|
|
730
|
+
booleans as the words) concatenated. It stands wherever a value does — a
|
|
731
|
+
`select` item, a `where` operand, an `order by` key, a function argument, a
|
|
732
|
+
method receiver. The text decodes with the same escapes as a string (`` \` ``
|
|
733
|
+
keeps a backtick, `\$` a dollar that opens nothing); newlines are kept, so a
|
|
734
|
+
literal may span lines. An interpolation holds any expression with balanced
|
|
735
|
+
braces — a `{ … }` block or another template literal included.
|
|
736
|
+
|
|
737
|
+
```js
|
|
738
|
+
oqx`label: \`\${title} (\${era})\` from ${docs}`; // "Alpha (3)"
|
|
739
|
+
oqx`x: \`\${jobs single { employer values where primary }}\` from ${docs}`; // the block's one value
|
|
740
|
+
oqx`title from ${docs} where \`\${title}!\` == "Alpha!"`;
|
|
741
|
+
oqx`x: jobs collect { label: \`\${^title}: \${employer}\` values } from ${docs}`; // `^` reads the enclosing row
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Two rules keep it honest: **absent propagates** — any absent interpolation makes
|
|
745
|
+
the whole literal absent (as `+` does), never `"undefined"` — and **a list, a
|
|
746
|
+
record or a range cannot be interpolated**: that is an eval error (`cannot
|
|
747
|
+
interpolate …`), not a silent `[object Object]`. Inside a JavaScript tagged
|
|
748
|
+
template write the backticks and `${` escaped as above (`` \` ``, `\${`), or
|
|
749
|
+
pass the whole query as a string: a `${…}` *binding* can stand beside a template
|
|
750
|
+
literal but never inside one (a fragment that ends inside the literal is an
|
|
751
|
+
unterminated-literal error).
|
|
655
752
|
|
|
656
753
|
### 10. Sugar (0.17)
|
|
657
754
|
|
|
@@ -661,11 +758,15 @@ optimizer and any pushdown planner see it), so nothing here has semantics of its
|
|
|
661
758
|
own except the postfix `!` and the infix `is`:
|
|
662
759
|
|
|
663
760
|
```js
|
|
664
|
-
// A receiver block without a consumer is `collect
|
|
665
|
-
//
|
|
761
|
+
// A receiver block without a consumer is `collect` — wherever a value stands
|
|
762
|
+
// (language 0.20): a select item, a where leaf, a follow destination, a call
|
|
763
|
+
// argument; `x distinct { … }` is `x collect distinct { … }`. A block whose
|
|
764
|
+
// LEADING expression is syntactically a predicate drops `where`.
|
|
666
765
|
oqx`name, current: jobs { !end_date } from ${people}`
|
|
667
766
|
// ≡ name, current: jobs collect { where !end_date } from ${people}
|
|
668
767
|
oqx`${people} { age > 40 }` // ≡ ${people} collect { where age > 40 }
|
|
768
|
+
oqx`n: size(jobs { !end_date }) from ${people}` // a block as a call argument
|
|
769
|
+
oqx`${people} distinct { city }` // ≡ ${people} collect distinct { city }
|
|
669
770
|
oqx`${people} count { jobs exists { } }` // a consumer test is predicate-shaped too
|
|
670
771
|
oqx`${people} first { name }` // a bare name still projects — unchanged
|
|
671
772
|
oqx`${people} first { is active }` // the bare-field filter: `is x` ≡ `!!x`, `not x` ≡ `!x`
|
|
@@ -678,15 +779,15 @@ oqx`name, first_job: jobs[0], primary: jobs[pay > 2]! from ${people}`
|
|
|
678
779
|
// jobs[pay > 2]! ≡ jobs single { where pay > 2 }! (zero or many matches ⇒ an eval error)
|
|
679
780
|
|
|
680
781
|
// Postfix `!` — required: the value, or an eval error naming the expression
|
|
681
|
-
// (and the row's
|
|
782
|
+
// (and the row's `$key` when scalar). Never a filter, never a coercion: 0! and ""! are values.
|
|
682
783
|
oqx`select id!, title from ${docs}` // insist on an identity before a write
|
|
683
784
|
// Tightest precedence: `refs(c)[0]!.name` requires the lookup, `refs(c)[0].name!` the name.
|
|
684
785
|
|
|
685
|
-
// `is` / `is not` compare IDENTITY (
|
|
786
|
+
// `is` / `is not` compare IDENTITY (`$key`: the host's, else structural — §9):
|
|
686
787
|
oqx`name from ${people} where manager is null` // absent (same as == null)
|
|
687
788
|
oqx`${people} count { ^people exists { $it is ^$it } }` // rows compare; `==` leaves that unspecified
|
|
688
|
-
// `and` / `or` are
|
|
689
|
-
// `print` writes the words (language 0.18); `!` stays `!`.
|
|
789
|
+
// `and` / `or` / `not` are the canonical connectives; `&&` / `||` / `!` are exact
|
|
790
|
+
// synonyms (`title or path` coalesces). `print` writes the words (language 0.18); `!` stays `!`.
|
|
690
791
|
|
|
691
792
|
// A select item may use the items to its left (inlined like a `where` alias):
|
|
692
793
|
oqx`boss: ^people[id == ^manager], bossName: boss.name from ${people}`
|
|
@@ -694,7 +795,12 @@ oqx`boss: ^people[id == ^manager], bossName: boss.name from ${people}`
|
|
|
694
795
|
|
|
695
796
|
Reserved words, never a bare field name: `from where select is not and or` and
|
|
696
797
|
the literal words `true false null`. The top level is unchanged — `age > 15
|
|
697
|
-
from ${people}` stays an error, because `where` would precede `from`.
|
|
798
|
+
from ${people}` stays an error, because `where` would precede `from`. One
|
|
799
|
+
carve-out: after a `follow` destination that is a relation of the current row
|
|
800
|
+
(a bare name, a call, a binding) a brace is still the **options** block (`follow
|
|
801
|
+
children { depth 2 }`), never a collect; only over an outer-reference receiver,
|
|
802
|
+
which cannot be a plain destination, is the brace a destination block with the
|
|
803
|
+
implicit `collect` (`follow ^people { where … }`, language 0.19).
|
|
698
804
|
|
|
699
805
|
### Cheat-sheet
|
|
700
806
|
|
|
@@ -702,11 +808,11 @@ from ${people}` stays an error, because `where` would precede `from`.
|
|
|
702
808
|
select … from … where … follow … order by … limit N offset N the fixed clause order (each at most once)
|
|
703
809
|
name, alias: expr, nested: rel collect { … } projection (`select` may be dropped only here, in first position)
|
|
704
810
|
from ${source} source collection (required at the top level)
|
|
705
|
-
where a == b
|
|
811
|
+
where a == b and rel exists { where … } or not c predicate tree + nested ops (`&&`/`||`/`!` are accepted synonyms)
|
|
706
812
|
rel { pay > 2 } / rel { employer } a block without a consumer is collect; a leading predicate is where-first, a bare name projects
|
|
707
813
|
rel[p] / rel[p]! / rel[0] / rel[0]! first match / exactly one (required) / positional / required positional
|
|
708
814
|
x! required: the value, or an error when absent (tightest; `a!.b` vs `a.b!`)
|
|
709
|
-
is x / not x / x is y / x is not y truthiness (≡ !!x / !x) / identity (
|
|
815
|
+
is x / not x / x is y / x is not y truthiness (≡ !!x / !x) / identity ($key: the host's, else structural; absent ≡ null)
|
|
710
816
|
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)
|
|
711
817
|
where rel none { … } zero rows (≡ !rel exists { … }; "all" = none over the complement)
|
|
712
818
|
where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl. / open-ended)
|
|
@@ -721,9 +827,13 @@ from entries(obj) … $key / $it a record's properties as a co
|
|
|
721
827
|
entries(rel) exists { … } a free-function call may be a receiver
|
|
722
828
|
order by expr desc, expr2 ordering
|
|
723
829
|
limit n / offset n bound the row set (after where/order/distinct, before the consumer)
|
|
724
|
-
follow rel { where … frontier … depth n
|
|
725
|
-
follow before, after
|
|
726
|
-
follow ^people
|
|
830
|
+
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
|
|
831
|
+
follow before, after / follow (before, after) several destinations: the union of their rows (per step, by identity) / one tuple destination
|
|
832
|
+
follow ^people { where manager == ^id } a destination block: successors computed per frontier row (`^` = that row; `collect` implicit, 0.20)
|
|
833
|
+
$key / $key: expr the identity a row is known by (the host's, else structural) / give the rows a block yields that identity
|
|
834
|
+
(a, b) / rel distinct { … } a tuple: the elements, one level flat (a value and a receiver) / collect distinct (0.20)
|
|
835
|
+
`${title} (${era})` a template literal: text + string forms; absent anywhere → absent; a list/record → error (0.21)
|
|
836
|
+
obj { title } one object → one record (or absent); a collection → a list; a scalar → an error (0.20)
|
|
727
837
|
${source} <collect|exists|none|count|first|single> { … } whole-query consumer
|
|
728
838
|
```
|
|
729
839
|
|
|
@@ -836,9 +946,12 @@ than approximate it. The conformance suite verifies this.
|
|
|
836
946
|
|
|
837
947
|
The engine never touches host objects directly; it asks a `DataContext` to
|
|
838
948
|
resolve named roots, read properties/relations, coerce results to rows, and
|
|
839
|
-
|
|
949
|
+
optionally to name a row's identity (`identityOf` — `$key`; the default is the
|
|
950
|
+
row's structural value), to say which of its objects are collections rather
|
|
951
|
+
than one row (`isSingle`; by default a non-iterable object is one row, so a
|
|
952
|
+
block over it is one record), and to supply a `rootObject()` — the root
|
|
840
953
|
scope's row, what `$it` is at the root and `^$it` from a top-level row; without
|
|
841
|
-
one the root row is absent
|
|
954
|
+
one the root row is absent. Implement it to query an ORM graph, a remote API,
|
|
842
955
|
or lazily loaded relations — the query *semantics* stay in OQX. Name resolution is simple
|
|
843
956
|
for a context: a bare `field`, a `.field` segment, and a `^field` outer
|
|
844
957
|
reference each become one `get(row, key)` against exactly the row of the scope
|
|
@@ -851,7 +964,7 @@ const graph = {
|
|
|
851
964
|
root: (name) => name === "tree" ? [nodes.get(1)] : undefined,
|
|
852
965
|
get: (row, key) => key === "children" ? row.childIds.map(id => nodes.get(id)) : row[key],
|
|
853
966
|
toRows: (v) => v == null ? [] : Array.isArray(v) ? v : [v],
|
|
854
|
-
|
|
967
|
+
identityOf: (row) => row.id, // `$key`: for follow dedup, distinct, `is` (optional; default: structural)
|
|
855
968
|
};
|
|
856
969
|
run(parse("id, depth: $depth from tree follow children"), { context: graph });
|
|
857
970
|
```
|
|
@@ -884,7 +997,7 @@ const ctx = {
|
|
|
884
997
|
return builtins.callMethod(name, recv, args);
|
|
885
998
|
},
|
|
886
999
|
};
|
|
887
|
-
run(parse("id from tree where age(born) > 18
|
|
1000
|
+
run(parse("id from tree where age(born) > 18 and title.slug() == 'x'"), { context: ctx });
|
|
888
1001
|
```
|
|
889
1002
|
|
|
890
1003
|
The simplest route is to `extends DefaultContext` and `super.callFunction(...)`
|
|
@@ -910,7 +1023,7 @@ import { parse, PlannedEngine } from "@omgbase/oqx";
|
|
|
910
1023
|
import { SqliteTable } from "@omgbase/oqx/sqlite";
|
|
911
1024
|
|
|
912
1025
|
const planner = new SqliteTable(db, "emp", { columns: ["id", "name", "dept", "level"] });
|
|
913
|
-
new PlannedEngine(planner).run(parse('name from emp where dept == "eng"
|
|
1026
|
+
new PlannedEngine(planner).run(parse('name from emp where dept == "eng" and level >= 5'), []);
|
|
914
1027
|
// → `dept`/`level` pushed to SQL; anything untranslatable finishes in-memory
|
|
915
1028
|
```
|
|
916
1029
|
|
|
@@ -941,7 +1054,7 @@ results and on errors.
|
|
|
941
1054
|
|
|
942
1055
|
**What is optimized**
|
|
943
1056
|
|
|
944
|
-
- **Correlated equality → hash probe.** A top-level
|
|
1057
|
+
- **Correlated equality → hash probe.** A top-level `and` conjunct of the block's
|
|
945
1058
|
`where` of the form `local == outer` (either side) — `local` a bare identifier
|
|
946
1059
|
or member chain on the block's row (`customer_id`, `meta.id`, `$it`),
|
|
947
1060
|
`outer` anything that reads nothing from that row (`^customer_id`, `^^x.id`,
|
|
@@ -963,7 +1076,7 @@ results and on errors.
|
|
|
963
1076
|
by`) are answered from the bucket's size after the bound without entering a
|
|
964
1077
|
row.
|
|
965
1078
|
|
|
966
|
-
**The sound rule.**
|
|
1079
|
+
**The sound rule.** `and` is strictly left to right and short-circuiting (§13),
|
|
967
1080
|
so a probe may skip a row only if everything the scan would have evaluated for
|
|
968
1081
|
it could neither raise nor bind: an equality is hoisted only when **every
|
|
969
1082
|
conjunct to its left is raise-free** — no function or method call, no `single`,
|
|
@@ -973,7 +1086,7 @@ too. A conjunct that could raise *right* of the equality is simply evaluated
|
|
|
973
1086
|
over the bucket, where the scan would have evaluated it on the same rows. A
|
|
974
1087
|
block whose `where` lifts anywhere is never probed (a lift could change what
|
|
975
1088
|
the outer side reads between rows). Otherwise the block scans as before, so
|
|
976
|
-
`where nope(x)
|
|
1089
|
+
`where nope(x) and id == ^id` still raises `unknown function`.
|
|
977
1090
|
|
|
978
1091
|
Property reads are treated as total (§23: evaluation is otherwise total). A
|
|
979
1092
|
`DataContext.get` that throws is caught where the engine relies on reads it
|
|
@@ -1082,7 +1195,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
1082
1195
|
(the optimizer rules for nested blocks, default `DEFAULT_RULES`; `[]` is the
|
|
1083
1196
|
naive scan) and `trace` (a `(event: TraceEvent) => void` sink for index /
|
|
1084
1197
|
probe / memo / cardinality / fallback events). See [Performance](#performance-relational-patterns).
|
|
1085
|
-
- `DataContext` — the tier-2 interface: `root`, `get`, `toRows`, `
|
|
1198
|
+
- `DataContext` — the tier-2 interface: `root`, `get`, `toRows`, `identityOf`, `isSingle`,
|
|
1086
1199
|
plus optional `callFunction` / `callMethod` (above), an optional
|
|
1087
1200
|
`indexFor(collection, path)` returning a `RowIndex` for a pre-built equality
|
|
1088
1201
|
index (`lookup` → positions; optionally `lookupRows` → rows, probed without
|
|
@@ -1095,7 +1208,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
1095
1208
|
"native")`; `BUILTIN_METHODS.matches` is always the baseline.
|
|
1096
1209
|
- `CallResult` — `{ handled: boolean; value?: unknown }`, returned by those hooks.
|
|
1097
1210
|
- `DefaultContext` — `new DefaultContext(roots?, options?)`; plain-object
|
|
1098
|
-
access,
|
|
1211
|
+
access, structural identity (`$key` is the row itself unless projected), and the builtin function/method tables. `options`:
|
|
1099
1212
|
`{ regexDialect?: RegexDialect }` (see the regex baseline, §4).
|
|
1100
1213
|
- `compileRegex(pattern, flags?, dialect?)` — the `matches()` compiler: a
|
|
1101
1214
|
`RegExp`, or an `OqxError` for a bad pattern/flag. `RegexDialect` /
|
|
@@ -1142,7 +1255,7 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
1142
1255
|
import { parse, run, IndexedCollection, PlannedEngine } from "@omgbase/oqx";
|
|
1143
1256
|
const planner = new IndexedCollection("people", people, ["city", "title"]);
|
|
1144
1257
|
const engine = new PlannedEngine(planner, planner.context());
|
|
1145
|
-
run(parse('name from people where city == "NYC"
|
|
1258
|
+
run(parse('name from people where city == "NYC" and age > 30'), { engine });
|
|
1146
1259
|
// city probe from the index; `age > 30` finished in-memory over the candidates
|
|
1147
1260
|
```
|
|
1148
1261
|
|
package/dist/ast.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ export type BinaryOp = RelOp | IdentityOp | "+" | "-" | "*" | "/" | "%";
|
|
|
15
15
|
export type UnaryOp = "!" | "-";
|
|
16
16
|
/** The operator of a `logical` expression. */
|
|
17
17
|
export type LogicalOp = "&&" | "||";
|
|
18
|
+
/** One part of a template literal (since 0.21): a decoded text run, or an
|
|
19
|
+
* interpolated expression. The strings are not nodes (no `kind`, no span). */
|
|
20
|
+
export type TemplatePart = string | Expr;
|
|
18
21
|
/** Scalar value/predicate expression, evaluated against a row scope + bindings. */
|
|
19
22
|
export type Expr = {
|
|
20
23
|
kind: "lit";
|
|
@@ -76,6 +79,14 @@ export type Expr = {
|
|
|
76
79
|
lo: Expr | null;
|
|
77
80
|
hi: Expr | null;
|
|
78
81
|
exclusiveEnd: boolean;
|
|
82
|
+
} | {
|
|
83
|
+
kind: "tuple";
|
|
84
|
+
span: Span;
|
|
85
|
+
items: Expr[];
|
|
86
|
+
} | {
|
|
87
|
+
kind: "template";
|
|
88
|
+
span: Span;
|
|
89
|
+
parts: TemplatePart[];
|
|
79
90
|
} | OpNode;
|
|
80
91
|
/** One `order by` term. */
|
|
81
92
|
export interface OrderSpec {
|
|
@@ -123,16 +134,13 @@ export interface OpNode {
|
|
|
123
134
|
* destination block — a select-position directive (`collect`/`first`/`single`) re-evaluated
|
|
124
135
|
* per frontier row, inside which `^` is that row. Discriminate on `kind === "op"`. */
|
|
125
136
|
export type FollowDestination = Expr | OpNode;
|
|
126
|
-
/** The recursive `follow` clause.
|
|
137
|
+
/** The recursive `follow` clause (since 0.20 just the destinations: a
|
|
138
|
+
* destination's `where`, `frontier` and `depth` live in its own block). */
|
|
127
139
|
export interface Follow {
|
|
128
140
|
kind: "follow";
|
|
129
141
|
span: Span;
|
|
130
142
|
destinations: FollowDestination[];
|
|
131
143
|
distinct: boolean;
|
|
132
|
-
where: Expr | null;
|
|
133
|
-
frontier: Expr | null;
|
|
134
|
-
depth: number | null;
|
|
135
|
-
by: Expr | null;
|
|
136
144
|
}
|
|
137
145
|
/** A nested (sub)query body — the `{ … }` of a directive. */
|
|
138
146
|
export interface Subquery {
|
|
@@ -143,6 +151,16 @@ export interface Subquery {
|
|
|
143
151
|
select: SelectItem[];
|
|
144
152
|
orderBy: OrderSpec[] | null;
|
|
145
153
|
follow: Follow | null;
|
|
154
|
+
/** `frontier P` / `depth n` (since 0.20). In a `follow` destination block
|
|
155
|
+
* `frontier` marks the occurrences reached through this destination for
|
|
156
|
+
* which `P` holds (not expanded; `P` read one level out, under the walk's
|
|
157
|
+
* enclosing scope), and `depth n` makes the destination expand a frontier
|
|
158
|
+
* row only while that row's `$depth < n` (1..8; `null` = the hard cap 8).
|
|
159
|
+
* In any other block `frontier` is legal only with a `follow` and marks the
|
|
160
|
+
* walk's SEEDS, read in the seed's scope like `where` (as on `Query`);
|
|
161
|
+
* `depth` is destination-only. Both `null` when absent. */
|
|
162
|
+
frontier: Expr | null;
|
|
163
|
+
depth: number | null;
|
|
146
164
|
/** `values` — scalar projection mode: the (single) projected expression is the
|
|
147
165
|
* row's result itself rather than being wrapped in a `{ name: value }` record,
|
|
148
166
|
* so `name values` yields `["Bob", …]` and `$it values` yields the rows. */
|
|
@@ -186,6 +204,11 @@ export interface Query {
|
|
|
186
204
|
orderBy: OrderSpec[] | null;
|
|
187
205
|
consumer: Consumer;
|
|
188
206
|
follow: Follow | null;
|
|
207
|
+
/** `frontier P` (since 0.20) — legal only when the body has a `follow`: a
|
|
208
|
+
* seed for which `P` holds (read in the seed's own scope, `^` the enclosing
|
|
209
|
+
* scope, like `where`) is a frontier (`$stop == "frontier"`, not expanded).
|
|
210
|
+
* `null` without one. */
|
|
211
|
+
frontier: Expr | null;
|
|
189
212
|
/** `distinct` — dedup the result rows by their projected value (see OpNode). */
|
|
190
213
|
distinct: boolean;
|
|
191
214
|
/** `values` — scalar projection mode (see Subquery). */
|
package/dist/ast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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"}
|
package/dist/ast.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// empty span `[0, 0]` (see `build.ts`).
|
|
13
13
|
/** The expression kinds (`Expr["kind"]`), for `isExpr`. */
|
|
14
14
|
const EXPR_KINDS = new Set([
|
|
15
|
-
"lit", "ident", "outer", "binding", "member", "call", "unary", "required", "binary", "logical", "in", "range",
|
|
15
|
+
"lit", "ident", "outer", "binding", "member", "call", "unary", "required", "binary", "logical", "in", "range", "tuple", "template",
|
|
16
16
|
]);
|
|
17
17
|
/** Whether a node is a scalar `Expr` (an `op` in expression position is not: it
|
|
18
18
|
* is an expression only by position, see AST.md §1). */
|
package/dist/ast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,2EAA2E;AAC3E,gFAAgF;AAChF,wDAAwD;AACxD,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,4EAA4E;AAC5E,wCAAwC;
|
|
1
|
+
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,2EAA2E;AAC3E,gFAAgF;AAChF,wDAAwD;AACxD,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,4EAA4E;AAC5E,wCAAwC;AAgMxC,2DAA2D;AAC3D,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC;IAC9C,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU;CACnI,CAAC,CAAC;AAEH;wDACwD;AACxD,MAAM,UAAU,MAAM,CAAC,IAAa;IAClC,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC"}
|