@omgbase/oqx 0.14.1 → 0.18.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 +41 -0
- package/README.md +192 -34
- package/dist/ast.d.ts +77 -23
- 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 +37 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +54 -0
- package/dist/build.js.map +1 -0
- package/dist/context.d.ts +13 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -3
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +1 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +55 -26
- 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 +34 -12
- 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 +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +7 -5
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +3 -3
- 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 +616 -251
- package/dist/parser.js.map +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +5 -3
- 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 +330 -0
- package/dist/resolve.js.map +1 -0
- package/dist/semantics.js +1 -1
- package/dist/semantics.js.map +1 -1
- 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 +54 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +139 -0
- package/dist/walk.js.map +1 -0
- package/package.json +1 -1
- package/src/ast.ts +105 -36
- package/src/build.ts +72 -0
- package/src/context.ts +16 -3
- package/src/engine.ts +57 -28
- package/src/errors.ts +3 -2
- package/src/index.ts +38 -13
- package/src/lexer.ts +102 -51
- package/src/optimize/analysis.ts +6 -5
- package/src/optimize/ir.ts +5 -5
- package/src/parser.ts +573 -225
- package/src/plan.ts +4 -3
- package/src/print.ts +274 -0
- package/src/resolve.ts +305 -0
- package/src/semantics.ts +1 -1
- package/src/version.ts +4 -0
- package/src/walk.ts +180 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ 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.18.0] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Minor
|
|
10
|
+
- **OQX 0.17 — join syntax sugar** (`spec/oqx` 0.16 → 0.17; `cases/sugar.json`, 134 new cases, 1131 in all; GRAMMAR §2/§4, SEMANTICS §5b/§14, AST.md). Everything that is pure sugar desugars **in the parser** to existing nodes, so the optimizer, the pushdown planners and `print` see the explicit form; two shapes are new.
|
|
11
|
+
|
|
12
|
+
- **A receiver block without a consumer is `collect`**: `x { … }` ≡ `x collect { … }` in select position, where position and as a whole query (`docs { age > 15 }`). Carve-out: after the last `follow` destination a brace is still the options block (`follow children { depth 2 }`); a destination block keeps its consumer.
|
|
13
|
+
- **Where-first bodies**: a block whose leading expression is *syntactically* a predicate (a comparison, `in`, `&&`/`||`, a prefix `!`/`is`/`not`, an infix `is`, a call, a literal, a binding, a parenthesized expression, a postfix `!` or bracket chain, a consumer test — anything that is not a bare name, a dotted navigation or a lift) drops `where`: `people exists { age > 50 }`, `jobs collect { pay > 2 }`. Bare names stay projections (`docs { is active }` is the bare-field filter); `{ name where age >= 18 }` keeps the keyword; `{ age >= 18, name }` is an error suggesting that form. The top level is unchanged: `age > 15 from people` is still the parse error (where would precede from).
|
|
14
|
+
- **Brackets**: `x[p]` → `x first { where p }`; `x[p]!` → `x single { where p }` under a `required` (zero matches is the required error, many the `single` error); `x[n]` / `x[${i}]` → `x first { offset n }` (out of range is absent; a non-integer binding is the `offset` eval error); `x[n]!` → required positional. Chains compose (`refs(company)[0].name`, `jobs[pay > 2][0]`); a negative or fractional literal index is a parse error; `[`/`]` are tokens.
|
|
15
|
+
- **Postfix `!` = required** — new node `{ kind: "required", expr }` / `Expr::Required`: the value, or the eval error `` `<expr>!` is absent `` (`on <id>` when the row has a scalar identity). Never a filter or a coercion (`0!`, `""!` are values); tightest precedence (`refs(c)[0]!.name` vs `refs(c)[0].name!`); a `!` right after a complete operand is postfix, a `!` at operand start is the negation (`!=` stays one token: write `a! == b` with the space).
|
|
16
|
+
- **`is` / `not` / `and` / `or`** become keywords (reserved: `from where select is not and or` + the literal words). Prefix `is x` ≡ `!!x`, `not x` ≡ `!x`. Infix `x is y` / `x is not y` — new `binary` ops `"is"` / `"is not"` (`BinaryOp::Is` / `IsNot`) — compare **identity** per SEMANTICS §16 (a row's `id` when present, else structural), so `$it is ^$it` compares rows and for scalars `is` ≡ `==`; comparison precedence, non-chainable. `and` / `or` are exact synonyms of `&&` / `||` (same nodes, precedence, short-circuit and value semantics; `print` still writes the symbols). `in` over rows is unchanged (deferred).
|
|
17
|
+
- **Alias reuse in `select`**: an item may reference the items to its left (`boss: ^people[id == ^manager], bossName: boss.name`), inlined by `resolveAliases` / `resolve_aliases` like a `where` alias; a reference to an item to the right is a parse error (a cycle when it refers back). **Behavior change:** `a: b, b: a` is now the cycle error (it was a field swap), and `senior: age > 50, s: senior` projects the alias (it was the row field `senior`).
|
|
18
|
+
- **A directive may stand in value position** (`jobs first { }.pay`): this is how `print` writes a bracket chain back; `op` joins the `Expr` family by position (`isExpr` stays false for it; `transform` rebuilds it without handing it to the mapping). AST.md §8 records the additions (minor; nothing renamed or removed).
|
|
19
|
+
- Existing fixtures that turned from error into meaning: `people exists { age > 50 }`-style blocks (`where` ×4, `errors-parse` consumer-after-projection re-queried as `{ name, jobs exists { } }`, `projection` call-in-block re-queried as `{ name, has(budget) }`), the `[` lex-error case (now a token).
|
|
20
|
+
- **omgbase binding**: the pushdown planners decline `is` / `is not` (identity is the engine's structural notion, not SQL's `IS`), `required` and value-position directives (they may raise), so they stay residual; `query_syntax` gains a "Sugar" block; `docs/query-language.md` §3.6. The surface fixture `implicit-where-in-block` re-queried as `{ kind == "md:task", name }` (still the loud error pointing at `where`).
|
|
21
|
+
- `@omgbase/oqx-syntax`: `[`/`]` are bracket punctuation (no longer lex errors), `is`/`not`/`and`/`or` are keywords highlighted as word operators, `LANGUAGE_VERSION` 0.17; assets regenerated.
|
|
22
|
+
- **OQX 0.18 — the root scope is a row, absolute scope references, canonical `and`/`or`** (`spec/oqx` 0.17 → 0.18; `cases/absolute-refs.json` new (24), `outer-refs` +6 (the `$it is absent at the root scope` case re-queried as the root-object cases), `limit-offset` +2: 1163 cases in all; GRAMMAR §1/§3/§4/§5/§6, SEMANTICS §2/§8/§18/§19, AST.md §1/§3/§6/§8, README).
|
|
23
|
+
|
|
24
|
+
- **The root scope is a row: the host's root object.** SEMANTICS §2 step 1 no longer reads "absent at the root": `$it` at the root scope is the host's root object, so from a top-level row `^$it` is that object, `entries(^$it)` enumerates the named roots in insertion order, `^$it.people` ≡ `^people` (and works as a receiver), `^^$it` from depth two is the same object, and `$it values from $it` is the object as one row. The seam gained **`DataContext.rootObject?(): unknown`** (TypeScript, optional — absent keeps the root row absent) / **`fn root_object(&self) -> Value`** (Rust, provided method defaulting to `Value::Undefined`); `DefaultContext` returns the roots record it was constructed with. Step 5 at the root still reads `ctx.root(name)` — a bare name is never a property of the root object — so hosts with lazy roots are unaffected, and a host-provided root-level name (omgbase will expose the repository id as `$id`, cashing in `$repo.$id` → `^$id` from depth one, `^^$id` from depth two) is readable through `root(name)`. A top-level `limit`/`offset` is evaluated in the root scope *without* its row (SEMANTICS §18): `^$it` reached through a bracket inside the bound is absent, pinned.
|
|
25
|
+
- **`N^name` — absolute scope references.** An unsigned integer literal immediately followed by `^` (no whitespace) names a scope by its syntactic depth (root 0, top-level rows 1, +1 per block body, a destination block's rows one below the frontier row, a follow `where` one deeper than the frontier row — the `visit` depth): `0^docs`, `0^$it`, `1^$path`, `0^$id`, and as a lift target `1^tasks: text`. It **desugars in the parser** to the existing `outer` node (or lift) with `levels = currentDepth − N`; no new AST shape, `print` writes the carets. `N ≥ currentDepth` is the parse error `scope N does not enclose this block (the current scope is depth D)`; `1 ^x` (a space) and `1.0^x` stay the stray-`^` error. Composes with brackets, calls and receivers (`0^docs[0]`, `x[p == 0^flag]`, `entries(0^$it)`, `0^$it.docs collect { … }`, `follow 0^people collect { where manager == 1^id }`, `follow next { where prev == 1^id && 0^k == 2 }`, `limit 1^n`). Both parsers now track the scope depth of every position.
|
|
26
|
+
- **Canonical `print` writes `and` / `or`** for the `where` tree's `and`/`or` nodes and for `logical` expressions (both engines); `!` stays `!`, `is`/`is not` were already words, the JSON `logical.op` stays `"&&"`/`"||"`. Consequence: every query text omgbase's `graph` macro prints flips from `||` to `or` — the pinned `graph` query strings in `spec/surface` (`reads.json`, and the `omgbase-surface` unit test for the multi-root seed) are regenerated; nothing else in the surface fixtures changes.
|
|
27
|
+
- **omgbase binding** (`@omgbase/core` `oqx-js/context.ts`, `omgbase-surface` `context.rs`): the root object is the repository root (`$repo`), so `^$it` from a top-level row is `$repo` and `^$it.docs` the docs scan. The `^docs`/`$repo` redesign and the `$id` root name are a separate change.
|
|
28
|
+
- `@omgbase/oqx-syntax`: the `N^` lexeme highlights as one outer-reference token (`lift`), `LANGUAGE_VERSION` 0.18; assets regenerated.
|
|
29
|
+
|
|
30
|
+
## [0.16.0] - 2026-10-09
|
|
31
|
+
|
|
32
|
+
### Minor
|
|
33
|
+
- **OQX 0.16 — the AST is a first-class contract** (`spec/oqx/AST.md`, fixtures `cases/ast.json` + `cases/ast-spans.json`, 70 new cases; 959 in all). Both engines now produce one normalized tree a tool can reflect on without re-parsing:
|
|
34
|
+
|
|
35
|
+
- **Spans on every node**: `span: [start, end)` in Unicode code points over the raw source (a template's `rawSource`, where a binding occupies its `${n}` marker). The TypeScript lexer counted UTF-16 units; it now counts code points like the Rust one, so the `(at offset N)` of a lex/parse error is the same number in both engines. `toUtf16(span, source)` converts for editors.
|
|
36
|
+
- **Normalized shape**: `kind` on every node (`query`, `subquery`, `follow`, `order` added), optionals materialized (`where`/`limit`/`offset`/`countCmp: null`, `distinct`/`values: false`), operators and consumers serialized as their source words, the never-produced `Expr.index` variant removed. `toJSON(query)` / `ast_to_json(&query)` emit `{ "oqx": "0.16", "kind": "query", … }`; the Rust crate derives serde under the `json` feature (`query_from_json` reads it back).
|
|
37
|
+
- **`where` keeps its surface form**: the parser no longer inlines `select` aliases into `where` (it only validates them — every `errors-parse` fixture is unchanged); the substitution is the exported pure `resolveAliases` / `resolve_aliases`, applied exactly once by the run entry points (`oqx`, `run`, `execute`, `runQuery`; `run_query`, `execute`). **An `Engine.run` now evaluates the query it is given** — code that drives `InMemoryEngine`/`PlannedEngine` directly with a query whose `where` uses aliases must resolve first (resolution is not idempotent: `name: name.upper() … where name`).
|
|
38
|
+
- **Traversal**: `visit(root, { enter, leave })` with `{ path, clause, depth }` (scope depth, 0 = root) and `transform(root, f)`, both driven by one child-key table (`CHILDREN`; Rust `walk::visit`/`transform` with a `Visitor` trait, `Node`, `Clause`); `stripSpans` / `strip_spans`; builders `build.*` / `oqx::build::*` for hand-made nodes (empty span `[0, 0]`).
|
|
39
|
+
- **Canonical printer**: `print(node)` / `print_query` — single spaces, `select` written at the top level, every item aliased, double-quoted strings, minimal parentheses from the precedence table; `printTemplate` → `{ strings, count, indices }` for a tree with bindings (`print` throws, new error stage `print`). Law, enforced by both spec runners over every fixture query: `strip(parse(print(parse(q)))) ≡ strip(parse(q))`.
|
|
40
|
+
- **Migration** for anyone constructing AST literals (after 0.14's `receiver` → `destinations`): add `kind` to `Query`/`Subquery`/`Follow`/`OrderSpec`, `span` to every node, write the materialized fields (`limit: null`, `countCmp: null`, `distinct: false`, `values: false`), drop `index`; or use `build.*`. Rust: struct variants gained `span` (match with `..`), `Expr::Lit(v)` is `Expr::Lit { value, span }`, `Token` gained `end`.
|
|
41
|
+
- **omgbase** (`@omgbase/core`, `omgbase-surface`): the runner's `$self` row-function rewrite is a `transform`, the `semantic("…")` scan a `visit`, aliases are resolved once before the runner's own rewrites; the `graph` macro builds its `follow` walk with the builders and `print`s it instead of splicing strings — the generated query text and every `spec/surface` result are unchanged. `@omgbase/oqx-syntax`'s `LANGUAGE_VERSION` moves to `0.16` (assets regenerated).
|
|
42
|
+
|
|
43
|
+
## [0.15.0] - 2026-10-09
|
|
44
|
+
|
|
45
|
+
### Minor
|
|
46
|
+
- OQX language **0.15**: the intrinsic naming the current item is **`$it`**; `$value` is no longer an intrinsic and has no synonym. `$it` is the scope's row itself — the top-level row, a nested block's inner item, each element of a scalar collection, the entry's value inside `entries(x)` — absent at the root, with `^$it` the enclosing row, `$it.field` navigation, `$it values`, `$it` as a `follow … { by $it }` identity. A row property literally named `$it` is shadowed by the intrinsic. `$value` is now an ordinary property name under SEMANTICS §2 step 5, exactly like the non-metadata `$` names since 0.13: `{ "$value": 3 }` projects `3`, a row without one reads absent, over scalars it is absent, and inside an `entries()` scope it is a property of the entry's *value*. Why: "self" implies a fixed referent while the row changes per scope; "it" is the pronoun for whatever is being handled; keeping `$value` as a synonym would spell one concept two ways. `$key` is unchanged, and the `{ key, value }` record of `entries(x)` used as a plain value is unchanged (that is a value shape, not the intrinsic). **Migration:** replace `$value` with `$it` in every query (`s/\$value/\$it/g`); nothing else moves. New `intrinsics` fixtures pin the rule; every other fixture query was rewritten with its expectation untouched. omgbase's query surface (`query`, `query_syntax`, the CLI help, `docs/query-language.md`, the tutorial, `spec/surface` fixture queries) follows: a `$value` in an omgbase query now reads a frontmatter/attrs key so named.
|
|
47
|
+
|
|
7
48
|
## [0.14.1] - 2026-10-09
|
|
8
49
|
|
|
9
50
|
### Patch
|
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
|
|
|
@@ -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
|
|
@@ -151,45 +154,53 @@ projection and inside any `collect` / `first` / `single` block, and composes wit
|
|
|
151
154
|
employers as strings, not `{ employer }` records). An alias, if present, is
|
|
152
155
|
ignored; a lift (`^name:`) cannot be combined with it.
|
|
153
156
|
|
|
154
|
-
**`$
|
|
157
|
+
**`$it` — the current item.** Every scope has a current value; `$it` is
|
|
155
158
|
that exact value, whatever its type (an object row or a plain scalar). Bare names
|
|
156
|
-
still navigate it (`name` ≡ `$
|
|
159
|
+
still navigate it (`name` ≡ `$it.name`), so `$it` matters exactly where
|
|
157
160
|
there is nothing to navigate: collections of numbers or strings, or handing the
|
|
158
161
|
whole row somewhere. Together with `values` this makes scalar collections
|
|
159
162
|
first-class:
|
|
160
163
|
|
|
161
164
|
```js
|
|
162
165
|
const scores = [10, 60, 70, 45];
|
|
163
|
-
oqx`$
|
|
164
|
-
oqx`$
|
|
166
|
+
oqx`$it values from ${scores} where $it > 50`; // [60, 70]
|
|
167
|
+
oqx`$it values from ${scores} order by $it desc`; // [70, 60, 45, 10]
|
|
165
168
|
|
|
166
169
|
const players = [{ name: "Ann", scores: [10, 60, 70] }, { name: "Ben", scores: [45] }];
|
|
167
|
-
oqx`name, big: scores collect { $
|
|
170
|
+
oqx`name, big: scores collect { $it values where $it > 50 } from ${players}`;
|
|
168
171
|
// [{ name: "Ann", big: [60, 70] }, { name: "Ben", big: [] }]
|
|
169
172
|
|
|
170
|
-
oqx`employee: $
|
|
173
|
+
oqx`employee: $it from ${people} where name == "Bob"`; // [{ employee: <Bob> }]
|
|
171
174
|
```
|
|
172
175
|
|
|
173
|
-
Inside a nested block `$
|
|
174
|
-
(§3). At the root scope
|
|
176
|
+
Inside a nested block `$it` is the inner item; the enclosing row is `^$it`
|
|
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
|
|
181
|
+
for the current item (language 0.15; through 0.14 it was spelled `$value`, and
|
|
182
|
+
there is no synonym): `$value` is now an ordinary property read, like any other
|
|
183
|
+
non-metadata `$name` — a row `{ "$value": 3 }` projects `3`, a row without one
|
|
184
|
+
reads absent. A row that itself owns a `$it` property is shadowed by the
|
|
185
|
+
intrinsic; reach it with `^` from a nested scope or rename it upstream.
|
|
175
186
|
|
|
176
187
|
**`entries(x)` and `$key` — records to collections, explicitly.** A plain object
|
|
177
188
|
is **not** iterable: `from ${obj}` is one row (the object). `entries(obj)`
|
|
178
189
|
converts it into a collection of entries, and inside such a scope the current
|
|
179
|
-
item is the property's **value** — `$
|
|
190
|
+
item is the property's **value** — `$it` and bare names read it — while
|
|
180
191
|
**`$key`** is the property's key:
|
|
181
192
|
|
|
182
193
|
```js
|
|
183
194
|
const settings = { theme: "dark", fontSize: 14, autosave: true };
|
|
184
|
-
oqx`key: $key, value: $
|
|
195
|
+
oqx`key: $key, value: $it from entries(${settings})`;
|
|
185
196
|
// [{ key: "theme", value: "dark" }, { key: "fontSize", value: 14 }, { key: "autosave", value: true }]
|
|
186
|
-
oqx`$key values from entries(${settings}) where $
|
|
197
|
+
oqx`$key values from entries(${settings}) where $it != "dark"`; // ["fontSize", "autosave"]
|
|
187
198
|
|
|
188
199
|
const flags = { beta: { on: true }, legacy: { on: false } };
|
|
189
200
|
oqx`$key values from entries(${flags}) where on`; // ["beta"] (bare `on` reads the value)
|
|
190
201
|
|
|
191
|
-
oqx`name, on: entries(prefs) collect { $key values where $
|
|
192
|
-
oqx`name from ${users} where entries(prefs) exists { where $key == "dark" && $
|
|
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" && $it }`;
|
|
193
204
|
```
|
|
194
205
|
|
|
195
206
|
`entries(array)` yields numeric index keys, a `Map` yields its entries, and
|
|
@@ -254,9 +265,10 @@ against the value of `x`; a string in `x` can't inject operators or identifiers.
|
|
|
254
265
|
|
|
255
266
|
**`where` sees the `select` aliases.** A name defined in the same body's
|
|
256
267
|
projection may be used in its `where`; an alias shadows a same-named field
|
|
257
|
-
there.
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
there. The alias's expression is substituted inline before evaluation
|
|
269
|
+
(`resolveAliases`, applied once by the run entry points — the parsed tree keeps
|
|
270
|
+
what you wrote), so it costs nothing at run time and a storage planner still
|
|
271
|
+
sees an ordinary predicate:
|
|
260
272
|
|
|
261
273
|
```js
|
|
262
274
|
oqx`select name, adult: age >= 18 from ${people} where adult`; // ≡ where age >= 18
|
|
@@ -275,7 +287,11 @@ an alias. `order by` is not rewritten — it reads row fields (§8).
|
|
|
275
287
|
against the **current row only**. If the row lacks that property the value is
|
|
276
288
|
absent — it never falls through to an enclosing row. To correlate with an
|
|
277
289
|
enclosing scope you say so explicitly with `^name` ("exactly one scope out";
|
|
278
|
-
`^^name` for two, and so on):
|
|
290
|
+
`^^name` for two, and so on) — or by **depth** with `N^name` (language 0.18):
|
|
291
|
+
`0^people` is the root scope's `people` from any depth, `1^name` the top-level
|
|
292
|
+
row's `name`, `0^$it` the root object. `N^` is an integer touching a caret
|
|
293
|
+
(`1 ^x` is not one); it is pure sugar for the caret form with `levels = depth −
|
|
294
|
+
N`, and `N ≥ the current depth` is a parse error.
|
|
279
295
|
|
|
280
296
|
```js
|
|
281
297
|
const accounts = [
|
|
@@ -524,7 +540,7 @@ fully), not a short-circuiting `exists`.
|
|
|
524
540
|
### 8. Ordering
|
|
525
541
|
|
|
526
542
|
`order by <expr> [asc|desc]`, comma-separated for tie-breaks. Absent values sort
|
|
527
|
-
last. The sort expression reads the row (fields, `$
|
|
543
|
+
last. The sort expression reads the row (fields, `$it`, recursion
|
|
528
544
|
intrinsics) — it is not rewritten against the `select` aliases, so
|
|
529
545
|
`order by decade` sorts by a field called `decade`, not by `decade: age / 10`.
|
|
530
546
|
|
|
@@ -637,22 +653,69 @@ with `^` the enclosing scope, as the body does. Give `follow` a stable identity
|
|
|
637
653
|
(`.id` or `by`) when your relation returns fresh objects rather than shared
|
|
638
654
|
references.
|
|
639
655
|
|
|
656
|
+
### 10. Sugar (0.17)
|
|
657
|
+
|
|
658
|
+
Every form below is shorthand for an explicit directive — it **desugars in the
|
|
659
|
+
parser** to the nodes you already know (`print` writes the explicit form, the
|
|
660
|
+
optimizer and any pushdown planner see it), so nothing here has semantics of its
|
|
661
|
+
own except the postfix `!` and the infix `is`:
|
|
662
|
+
|
|
663
|
+
```js
|
|
664
|
+
// A receiver block without a consumer is `collect`; a block whose LEADING
|
|
665
|
+
// expression is syntactically a predicate drops `where`.
|
|
666
|
+
oqx`name, current: jobs { !end_date } from ${people}`
|
|
667
|
+
// ≡ name, current: jobs collect { where !end_date } from ${people}
|
|
668
|
+
oqx`${people} { age > 40 }` // ≡ ${people} collect { where age > 40 }
|
|
669
|
+
oqx`${people} count { jobs exists { } }` // a consumer test is predicate-shaped too
|
|
670
|
+
oqx`${people} first { name }` // a bare name still projects — unchanged
|
|
671
|
+
oqx`${people} first { is active }` // the bare-field filter: `is x` ≡ `!!x`, `not x` ≡ `!x`
|
|
672
|
+
|
|
673
|
+
// Brackets: `[p]` is `first { where p }`, `[n]` is `first { offset n }`;
|
|
674
|
+
// a trailing `!` requires the result (and makes the predicate form `single`).
|
|
675
|
+
oqx`name, boss: ^people[id == ^manager].name from ${people}` // the first match, navigated
|
|
676
|
+
oqx`name, first_job: jobs[0], primary: jobs[pay > 2]! from ${people}`
|
|
677
|
+
// jobs[0] ≡ jobs first { offset 0 } (out of range ⇒ absent)
|
|
678
|
+
// jobs[pay > 2]! ≡ jobs single { where pay > 2 }! (zero or many matches ⇒ an eval error)
|
|
679
|
+
|
|
680
|
+
// Postfix `!` — required: the value, or an eval error naming the expression
|
|
681
|
+
// (and the row's id). Never a filter, never a coercion: 0! and ""! are values.
|
|
682
|
+
oqx`select id!, title from ${docs}` // insist on an identity before a write
|
|
683
|
+
// Tightest precedence: `refs(c)[0]!.name` requires the lookup, `refs(c)[0].name!` the name.
|
|
684
|
+
|
|
685
|
+
// `is` / `is not` compare IDENTITY (a row's id when present, else structurally):
|
|
686
|
+
oqx`name from ${people} where manager is null` // absent (same as == null)
|
|
687
|
+
oqx`${people} count { ^people exists { $it is ^$it } }` // rows compare; `==` leaves that unspecified
|
|
688
|
+
// `and` / `or` are exact synonyms of `&&` / `||`: `title or path` coalesces.
|
|
689
|
+
// `print` writes the words (language 0.18); `!` stays `!`.
|
|
690
|
+
|
|
691
|
+
// A select item may use the items to its left (inlined like a `where` alias):
|
|
692
|
+
oqx`boss: ^people[id == ^manager], bossName: boss.name from ${people}`
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Reserved words, never a bare field name: `from where select is not and or` and
|
|
696
|
+
the literal words `true false null`. The top level is unchanged — `age > 15
|
|
697
|
+
from ${people}` stays an error, because `where` would precede `from`.
|
|
698
|
+
|
|
640
699
|
### Cheat-sheet
|
|
641
700
|
|
|
642
701
|
```
|
|
643
702
|
select … from … where … follow … order by … limit N offset N the fixed clause order (each at most once)
|
|
644
703
|
name, alias: expr, nested: rel collect { … } projection (`select` may be dropped only here, in first position)
|
|
645
704
|
from ${source} source collection (required at the top level)
|
|
646
|
-
where a == b && rel exists { where … } || !c predicate tree + nested ops (`
|
|
647
|
-
|
|
705
|
+
where a == b && rel exists { where … } || !c predicate tree + nested ops (`and`/`or` ≡ `&&`/`||`)
|
|
706
|
+
rel { pay > 2 } / rel { employer } a block without a consumer is collect; a leading predicate is where-first, a bare name projects
|
|
707
|
+
rel[p] / rel[p]! / rel[0] / rel[0]! first match / exactly one (required) / positional / required positional
|
|
708
|
+
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 (a row's id, else structural; absent ≡ null)
|
|
710
|
+
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)
|
|
648
711
|
where rel none { … } zero rows (≡ !rel exists { … }; "all" = none over the complement)
|
|
649
712
|
where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl. / open-ended)
|
|
650
713
|
where x in range(field) coerce a string field to a range, then test coverage
|
|
651
714
|
name the CURRENT row's field only (never climbs)
|
|
652
|
-
$
|
|
653
|
-
from entries(obj) … $key / $
|
|
715
|
+
$it the current item itself (a scalar row, or the whole object)
|
|
716
|
+
from entries(obj) … $key / $it a record's properties as a collection (key + value; bare names read the value)
|
|
654
717
|
<expr> values scalar projection: the value, not a { name: value } record
|
|
655
|
-
^name / ^^name read an enclosing row's field (exactly N scopes out); ^$
|
|
718
|
+
^name / ^^name read an enclosing row's field (exactly N scopes out); ^$it = the enclosing row
|
|
656
719
|
^name: expr / ^^name: expr lift/export a value N scopes out (flatten-append)
|
|
657
720
|
^rel collect { … } / ^^root exists { … } nested consumer over an enclosing row's relation / a named root
|
|
658
721
|
entries(rel) exists { … } a free-function call may be a receiver
|
|
@@ -664,6 +727,78 @@ follow ^people collect { where manager == ^id } a destination block: successors
|
|
|
664
727
|
${source} <collect|exists|none|count|first|single> { … } whole-query consumer
|
|
665
728
|
```
|
|
666
729
|
|
|
730
|
+
## The AST: reflecting on a query
|
|
731
|
+
|
|
732
|
+
`parse(source)` returns the query's abstract syntax tree, and since language
|
|
733
|
+
0.16 that tree is a **public contract** shared by both implementations
|
|
734
|
+
([`spec/oqx/AST.md`](https://github.com/omgbase/omgbase/tree/main/spec/oqx/AST.md)):
|
|
735
|
+
a tool reflects on a query — which relations it names, which clauses it has,
|
|
736
|
+
where in the text each piece sits — without re-parsing. The objects are plain
|
|
737
|
+
data (no parent pointers, no methods) and *are* the JSON shape.
|
|
738
|
+
|
|
739
|
+
- **Every node has `kind` and `span`.** `span` is `[start, end)` in Unicode
|
|
740
|
+
**code points** over the raw source (for a template, `rawSource(strings)`,
|
|
741
|
+
where a binding occupies its `${n}` marker); `toUtf16(span, source)` converts
|
|
742
|
+
for an editor that counts UTF-16 units. A parenthesized operand spans its
|
|
743
|
+
parentheses; a block (`subquery`) spans its braces.
|
|
744
|
+
- **Optionals are materialized** — `where: null`, `limit: null`, `countCmp: null`,
|
|
745
|
+
`distinct: false`, `values: false` — so nothing has to be defaulted.
|
|
746
|
+
- **`where` keeps its surface form.** A `select` alias used in `where` stays an
|
|
747
|
+
`ident` in the tree; `resolveAliases(query)` is the pure substitution the run
|
|
748
|
+
entry points (`oqx`, `run`, `execute`, `runQuery`) apply exactly once before
|
|
749
|
+
evaluation. An `Engine.run` evaluates the query it is given — if you drive an
|
|
750
|
+
engine yourself, resolve first.
|
|
751
|
+
|
|
752
|
+
```js
|
|
753
|
+
import { parse, visit, transform, print, printTemplate, toJSON, stripSpans, build } from "@omgbase/oqx";
|
|
754
|
+
|
|
755
|
+
const q = parse('select name, n: jobs collect { employer } from people where jobs exists { where !end }');
|
|
756
|
+
|
|
757
|
+
// visit: every node, with its ancestors, the clause it sits in, and the scope depth
|
|
758
|
+
const relations = [];
|
|
759
|
+
visit(q, {
|
|
760
|
+
enter(node, { clause, depth }) {
|
|
761
|
+
if (node.kind === "op") relations.push({ relation: print(node.receiver), clause, depth });
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
// → [{ relation: "jobs", clause: "select", depth: 1 }, { relation: "jobs", clause: "where", depth: 1 }]
|
|
765
|
+
|
|
766
|
+
// transform: rebuild with every expression mapped (children first; untouched nodes keep identity + spans)
|
|
767
|
+
const rewritten = transform(q, (e) => (e.kind === "ident" && e.name === "name" ? build.member(build.ident("person"), "name") : e));
|
|
768
|
+
|
|
769
|
+
// print: the canonical source — single spaces, `select` written, minimal parentheses
|
|
770
|
+
print(rewritten);
|
|
771
|
+
// 'select name: person.name, n: jobs collect { employer: employer } from people where jobs exists { where !end }'
|
|
772
|
+
|
|
773
|
+
toJSON(q); // { oqx: "0.17", kind: "query", span: [0, 93], … } — the shared JSON document
|
|
774
|
+
stripSpans(q); // the same tree without spans, for shape comparisons
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
`visit(root, { enter?, leave? })` walks depth-first in canonical source order;
|
|
778
|
+
`enter` may return `false` to skip a subtree. Its context is `{ path, clause,
|
|
779
|
+
depth }` — the ancestors, the clause (`source` · `from` · `where` · `select` ·
|
|
780
|
+
`orderBy` · `limit` · `offset` · `follow` · `follow.destination` · `follow.where`
|
|
781
|
+
· `follow.frontier` · `follow.by`; `null` at the root) and the **scope depth**
|
|
782
|
+
the node is evaluated at (0 = the root scope: a top-level source or bound; 1 =
|
|
783
|
+
a top-level row; a block's body is one deeper than its receiver). Both walks
|
|
784
|
+
are driven by one child-key table, `CHILDREN`, so a future field is added in one
|
|
785
|
+
place.
|
|
786
|
+
|
|
787
|
+
`print(node)` is canonical, and the law `stripSpans(parse(print(parse(q))))`
|
|
788
|
+
≡ `stripSpans(parse(q))` holds for every spec fixture (both runners check it);
|
|
789
|
+
`print(parse(q)) === q` is *not* a law — spelling is normalized. A binding is a
|
|
790
|
+
value, never source text: `print` throws (`OqxError`, stage `"print"`) on one,
|
|
791
|
+
and `printTemplate(node)` returns `{ strings, count, indices }` — the fragments
|
|
792
|
+
around each binding and the binding each gap stands for (the canonical clause
|
|
793
|
+
order can move one past another), to re-run with `parseTemplate(strings,
|
|
794
|
+
count)` and the values permuted by `indices`.
|
|
795
|
+
|
|
796
|
+
`build.*` constructs nodes with the empty span `[0, 0]` and the materialized
|
|
797
|
+
defaults (`build.query(source, { select, where, … })`, `build.op(receiver,
|
|
798
|
+
"collect", build.subquery({ … }))`, `build.field`, `build.collect`,
|
|
799
|
+
`build.follow`, `build.ident`, `build.lit`, `build.binary`, `build.and`, …), so
|
|
800
|
+
a tree you assemble prints with `print` and has the shape of a parsed one.
|
|
801
|
+
|
|
667
802
|
## Data context: string queries and named roots
|
|
668
803
|
|
|
669
804
|
When you don't need interpolation, `execute` runs a plain string query against a
|
|
@@ -682,7 +817,8 @@ the full discriminated result (`{ consumer, … }`).
|
|
|
682
817
|
## Architecture: adapting to other storage & query systems
|
|
683
818
|
|
|
684
819
|
OQX is layered so it can be the front-end for query systems far beyond in-memory
|
|
685
|
-
objects. The parsed `Query` AST is the host-agnostic
|
|
820
|
+
objects. The parsed `Query` AST is the host-agnostic contract (above); execution
|
|
821
|
+
is pluggable.
|
|
686
822
|
|
|
687
823
|
```
|
|
688
824
|
Query AST ─┬─ InMemoryEngine(DataContext) tier 1/2 — drive any data model
|
|
@@ -700,8 +836,10 @@ than approximate it. The conformance suite verifies this.
|
|
|
700
836
|
|
|
701
837
|
The engine never touches host objects directly; it asks a `DataContext` to
|
|
702
838
|
resolve named roots, read properties/relations, coerce results to rows, and
|
|
703
|
-
compute identity
|
|
704
|
-
|
|
839
|
+
compute identity (and, optionally, to supply a `rootObject()` — the root
|
|
840
|
+
scope's row, what `$it` is at the root and `^$it` from a top-level row; without
|
|
841
|
+
one the root row is absent). Implement it to query an ORM graph, a remote API,
|
|
842
|
+
or lazily loaded relations — the query *semantics* stay in OQX. Name resolution is simple
|
|
705
843
|
for a context: a bare `field`, a `.field` segment, and a `^field` outer
|
|
706
844
|
reference each become one `get(row, key)` against exactly the row of the scope
|
|
707
845
|
they name, so a computed relation only needs `get` to know about it:
|
|
@@ -805,7 +943,7 @@ results and on errors.
|
|
|
805
943
|
|
|
806
944
|
- **Correlated equality → hash probe.** A top-level `&&` conjunct of the block's
|
|
807
945
|
`where` of the form `local == outer` (either side) — `local` a bare identifier
|
|
808
|
-
or member chain on the block's row (`customer_id`, `meta.id`, `$
|
|
946
|
+
or member chain on the block's row (`customer_id`, `meta.id`, `$it`),
|
|
809
947
|
`outer` anything that reads nothing from that row (`^customer_id`, `^^x.id`,
|
|
810
948
|
a `${…}` binding, a literal, arithmetic over them) — is answered from a hash
|
|
811
949
|
index on the receiver built **once per run** per collection and path. The
|
|
@@ -903,7 +1041,8 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
903
1041
|
consumer-shaped value: an array for `collect`, a boolean for `exists` / `none`,
|
|
904
1042
|
a number for `count`, a record or `null` for `first` / `single`.
|
|
905
1043
|
- `parse(source)` — a query string → reusable `Query` AST (an `OqxError` with
|
|
906
|
-
`stage: "lex" | "parse"` on bad input)
|
|
1044
|
+
`stage: "lex" | "parse"` on bad input); `parseTemplate(strings, count)` the
|
|
1045
|
+
tagged-template form, with `binding` nodes for the `${…}` slots.
|
|
907
1046
|
- `execute(source, roots?)` — parse and run a string against named roots
|
|
908
1047
|
(`{ people }`), returning the consumer-shaped value.
|
|
909
1048
|
- `run(query, opts?)` — run a parsed `Query`, returning the full `OqxResult`.
|
|
@@ -1027,11 +1166,30 @@ adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
|
1027
1166
|
- `map?: (raw) => row` — a custom raw-SQL-row → query-row mapper (overrides
|
|
1028
1167
|
`jsonColumns`).
|
|
1029
1168
|
|
|
1169
|
+
**The AST** (see [above](#the-ast-reflecting-on-a-query); `spec/oqx/AST.md` is normative)
|
|
1170
|
+
|
|
1171
|
+
- `visit(root, { enter?, leave? })` / `transform(root, f)` — the generic walks
|
|
1172
|
+
(`Visitor`, `VisitContext`, `Clause` are the types); `CHILDREN` — the
|
|
1173
|
+
child-key table per node kind that drives them.
|
|
1174
|
+
- `print(node)` / `printTemplate(node)` — the canonical printer (`Template` is
|
|
1175
|
+
`{ strings, count, indices }`).
|
|
1176
|
+
- `resolveAliases(query)` — the `select`-alias substitution the entry points
|
|
1177
|
+
apply before evaluation (SEMANTICS §14).
|
|
1178
|
+
- `toJSON(query)` — the tree stamped with `oqx: LANGUAGE_VERSION`; `stripSpans(node)`
|
|
1179
|
+
— the tree without spans (`Stripped<T>`); `toUtf16(span, source)` and
|
|
1180
|
+
`codePointLength(s)` — code-point ↔ UTF-16 offsets; `rawSource(strings)` —
|
|
1181
|
+
the display source of a template.
|
|
1182
|
+
- `build` — the node builders (`build.query`, `build.op`, `build.subquery`,
|
|
1183
|
+
`build.field`, `build.ident`, …).
|
|
1184
|
+
- `LANGUAGE_VERSION` — the `spec/oqx/VERSION` this implementation conforms to (`"0.17"`).
|
|
1185
|
+
|
|
1030
1186
|
**Types**
|
|
1031
1187
|
|
|
1032
1188
|
- Every AST node type from `src/ast.ts` is re-exported (`Query`, `Subquery`,
|
|
1033
|
-
`Where`, `Expr`, `OpNode`, `SelectItem`, `OrderSpec`, `Follow`,
|
|
1034
|
-
|
|
1189
|
+
`Where`, `Expr`, `OpNode`, `SelectItem`, `OrderSpec`, `Follow`,
|
|
1190
|
+
`AstNode`, `AstKind`, `Span`, …) for tools that reflect on queries and
|
|
1191
|
+
planners that walk the tree; `isExpr(node)` tells an expression from the
|
|
1192
|
+
structural nodes.
|
|
1035
1193
|
|
|
1036
1194
|
## Requirements
|
|
1037
1195
|
|
package/dist/ast.d.ts
CHANGED
|
@@ -1,91 +1,123 @@
|
|
|
1
|
+
/** A half-open source range `[start, end)` in Unicode code points over the raw
|
|
2
|
+
* source (`rawSource` for a template, where a binding occupies its `${n}`
|
|
3
|
+
* marker). The empty span `[0, 0]` marks a node that did not come from source. */
|
|
4
|
+
export type Span = [start: number, end: number];
|
|
1
5
|
/** Query consumers — how a (sub)query's row set is shaped. `none` is the
|
|
2
6
|
* zero-cardinality complement of `exists` (true iff the block yields no rows). */
|
|
3
7
|
export type Consumer = "collect" | "exists" | "none" | "count" | "first" | "single";
|
|
4
8
|
/** Comparison operators usable in a `count { … } <op> <int>` test. */
|
|
5
9
|
export type RelOp = "==" | "!=" | "<" | "<=" | ">" | ">=";
|
|
10
|
+
/** The identity operators (SEMANTICS §5b, since 0.17). */
|
|
11
|
+
export type IdentityOp = "is" | "is not";
|
|
12
|
+
/** The operator of a `binary` expression: a comparison, an identity test, or arithmetic. */
|
|
13
|
+
export type BinaryOp = RelOp | IdentityOp | "+" | "-" | "*" | "/" | "%";
|
|
14
|
+
/** The operator of a `unary` expression. */
|
|
15
|
+
export type UnaryOp = "!" | "-";
|
|
16
|
+
/** The operator of a `logical` expression. */
|
|
17
|
+
export type LogicalOp = "&&" | "||";
|
|
6
18
|
/** Scalar value/predicate expression, evaluated against a row scope + bindings. */
|
|
7
19
|
export type Expr = {
|
|
8
20
|
kind: "lit";
|
|
21
|
+
span: Span;
|
|
9
22
|
value: string | number | boolean | null;
|
|
10
23
|
} | {
|
|
11
24
|
kind: "ident";
|
|
25
|
+
span: Span;
|
|
12
26
|
name: string;
|
|
13
27
|
} | {
|
|
14
28
|
kind: "outer";
|
|
29
|
+
span: Span;
|
|
15
30
|
levels: number;
|
|
16
31
|
name: string;
|
|
17
32
|
} | {
|
|
18
33
|
kind: "binding";
|
|
34
|
+
span: Span;
|
|
19
35
|
index: number;
|
|
20
36
|
} | {
|
|
21
37
|
kind: "member";
|
|
38
|
+
span: Span;
|
|
22
39
|
recv: Expr;
|
|
23
40
|
name: string;
|
|
24
|
-
} | {
|
|
25
|
-
kind: "index";
|
|
26
|
-
recv: Expr;
|
|
27
|
-
index: Expr;
|
|
28
41
|
} | {
|
|
29
42
|
kind: "call";
|
|
43
|
+
span: Span;
|
|
30
44
|
recv: Expr | null;
|
|
31
45
|
name: string;
|
|
32
46
|
args: Expr[];
|
|
33
47
|
} | {
|
|
34
48
|
kind: "unary";
|
|
35
|
-
|
|
49
|
+
span: Span;
|
|
50
|
+
op: UnaryOp;
|
|
51
|
+
expr: Expr;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "required";
|
|
54
|
+
span: Span;
|
|
36
55
|
expr: Expr;
|
|
37
56
|
} | {
|
|
38
57
|
kind: "binary";
|
|
39
|
-
|
|
58
|
+
span: Span;
|
|
59
|
+
op: BinaryOp;
|
|
40
60
|
left: Expr;
|
|
41
61
|
right: Expr;
|
|
42
62
|
} | {
|
|
43
63
|
kind: "logical";
|
|
44
|
-
|
|
64
|
+
span: Span;
|
|
65
|
+
op: LogicalOp;
|
|
45
66
|
left: Expr;
|
|
46
67
|
right: Expr;
|
|
47
68
|
} | {
|
|
48
69
|
kind: "in";
|
|
70
|
+
span: Span;
|
|
49
71
|
left: Expr;
|
|
50
72
|
right: Expr;
|
|
51
73
|
} | {
|
|
52
74
|
kind: "range";
|
|
75
|
+
span: Span;
|
|
53
76
|
lo: Expr | null;
|
|
54
77
|
hi: Expr | null;
|
|
55
78
|
exclusiveEnd: boolean;
|
|
56
|
-
};
|
|
79
|
+
} | OpNode;
|
|
80
|
+
/** One `order by` term. */
|
|
57
81
|
export interface OrderSpec {
|
|
82
|
+
kind: "order";
|
|
83
|
+
span: Span;
|
|
58
84
|
expr: Expr;
|
|
59
85
|
desc: boolean;
|
|
60
86
|
}
|
|
61
87
|
/** One projection item: a named scalar/navigation value, or a named nested
|
|
62
|
-
* collection consumer. `lift` marks a `^name`
|
|
88
|
+
* collection consumer. `lift` marks a `^name` lift. */
|
|
63
89
|
export type SelectItem = {
|
|
64
90
|
kind: "field";
|
|
91
|
+
span: Span;
|
|
65
92
|
name: string;
|
|
66
93
|
expr: Expr;
|
|
67
94
|
lift: number;
|
|
68
95
|
} | {
|
|
69
96
|
kind: "collect";
|
|
97
|
+
span: Span;
|
|
70
98
|
name: string;
|
|
71
99
|
op: OpNode;
|
|
72
100
|
};
|
|
101
|
+
/** The `count { … } <op> <int>` test attached to an `OpNode` — an attribute of
|
|
102
|
+
* the directive, not a node of its own. */
|
|
103
|
+
export interface CountCmp {
|
|
104
|
+
op: RelOp;
|
|
105
|
+
value: number;
|
|
106
|
+
}
|
|
73
107
|
/** A postfix consumer directive over a receiver collection:
|
|
74
108
|
* `<receiver> <op> { <sub> }`, optionally `count { … } <relop> <int>`. */
|
|
75
109
|
export interface OpNode {
|
|
76
110
|
kind: "op";
|
|
111
|
+
span: Span;
|
|
77
112
|
receiver: Expr;
|
|
78
113
|
op: Consumer;
|
|
79
114
|
sub: Subquery;
|
|
80
|
-
countCmp
|
|
81
|
-
op: RelOp;
|
|
82
|
-
value: number;
|
|
83
|
-
};
|
|
115
|
+
countCmp: CountCmp | null;
|
|
84
116
|
/** `distinct` — dedup the rows this directive consumes by their projected
|
|
85
117
|
* value (the `select`), so `count distinct { … }` counts distinct projections
|
|
86
118
|
* and `collect distinct { … }` yields distinct rows. Empty select ⇒ dedup by
|
|
87
119
|
* row identity. Spellable as `<op> distinct { … }` or `{ select distinct … }`. */
|
|
88
|
-
distinct
|
|
120
|
+
distinct: boolean;
|
|
89
121
|
}
|
|
90
122
|
/** One `follow` destination: a relation of the current row (an `Expr` receiver), or a
|
|
91
123
|
* destination block — a select-position directive (`collect`/`first`/`single`) re-evaluated
|
|
@@ -93,6 +125,8 @@ export interface OpNode {
|
|
|
93
125
|
export type FollowDestination = Expr | OpNode;
|
|
94
126
|
/** The recursive `follow` clause. */
|
|
95
127
|
export interface Follow {
|
|
128
|
+
kind: "follow";
|
|
129
|
+
span: Span;
|
|
96
130
|
destinations: FollowDestination[];
|
|
97
131
|
distinct: boolean;
|
|
98
132
|
where: Expr | null;
|
|
@@ -100,8 +134,10 @@ export interface Follow {
|
|
|
100
134
|
depth: number | null;
|
|
101
135
|
by: Expr | null;
|
|
102
136
|
}
|
|
103
|
-
/** A nested (sub)query body. */
|
|
137
|
+
/** A nested (sub)query body — the `{ … }` of a directive. */
|
|
104
138
|
export interface Subquery {
|
|
139
|
+
kind: "subquery";
|
|
140
|
+
span: Span;
|
|
105
141
|
from: Expr[];
|
|
106
142
|
where: Where | null;
|
|
107
143
|
select: SelectItem[];
|
|
@@ -109,34 +145,40 @@ export interface Subquery {
|
|
|
109
145
|
follow: Follow | null;
|
|
110
146
|
/** `values` — scalar projection mode: the (single) projected expression is the
|
|
111
147
|
* row's result itself rather than being wrapped in a `{ name: value }` record,
|
|
112
|
-
* so `name values` yields `["Bob", …]` and `$
|
|
113
|
-
values
|
|
148
|
+
* so `name values` yields `["Bob", …]` and `$it values` yields the rows. */
|
|
149
|
+
values: boolean;
|
|
114
150
|
/** `limit N` / `offset N` — bound the row set AFTER where/order/distinct and
|
|
115
151
|
* BEFORE the consumer reduces it, so `count { … limit 5 }` is at most 5 and
|
|
116
152
|
* `first { … offset 1 }` is the second row. Each is a value expression
|
|
117
153
|
* (a literal, a `${…}` binding, or an outer reference) read as part of the
|
|
118
154
|
* block — `^n` is the enclosing row's field, as everywhere inside `{ … }` —
|
|
119
155
|
* and must yield a non-negative integer. */
|
|
120
|
-
limit
|
|
121
|
-
offset
|
|
156
|
+
limit: Expr | null;
|
|
157
|
+
offset: Expr | null;
|
|
122
158
|
}
|
|
123
159
|
/** The where-clause boolean tree: OQX owns &&/||/!/grouping so consumer ops
|
|
124
160
|
* (invisible to the scalar evaluator) compose with scalar predicates. */
|
|
125
161
|
export type Where = {
|
|
126
162
|
kind: "and";
|
|
163
|
+
span: Span;
|
|
127
164
|
parts: Where[];
|
|
128
165
|
} | {
|
|
129
166
|
kind: "or";
|
|
167
|
+
span: Span;
|
|
130
168
|
parts: Where[];
|
|
131
169
|
} | {
|
|
132
170
|
kind: "not";
|
|
171
|
+
span: Span;
|
|
133
172
|
expr: Where;
|
|
134
173
|
} | {
|
|
135
174
|
kind: "scalar";
|
|
175
|
+
span: Span;
|
|
136
176
|
expr: Expr;
|
|
137
177
|
} | OpNode;
|
|
138
178
|
/** A top-level OQX query. */
|
|
139
179
|
export interface Query {
|
|
180
|
+
kind: "query";
|
|
181
|
+
span: Span;
|
|
140
182
|
source: Expr;
|
|
141
183
|
from: Expr[];
|
|
142
184
|
where: Where | null;
|
|
@@ -145,11 +187,23 @@ export interface Query {
|
|
|
145
187
|
consumer: Consumer;
|
|
146
188
|
follow: Follow | null;
|
|
147
189
|
/** `distinct` — dedup the result rows by their projected value (see OpNode). */
|
|
148
|
-
distinct
|
|
190
|
+
distinct: boolean;
|
|
149
191
|
/** `values` — scalar projection mode (see Subquery). */
|
|
150
|
-
values
|
|
192
|
+
values: boolean;
|
|
151
193
|
/** `limit N` / `offset N` (see Subquery); evaluated at the root scope. */
|
|
152
|
-
limit
|
|
153
|
-
offset
|
|
194
|
+
limit: Expr | null;
|
|
195
|
+
offset: Expr | null;
|
|
154
196
|
}
|
|
197
|
+
/** Every node of the tree. `AstNode["kind"]` is the closed set of node kinds. */
|
|
198
|
+
export type AstNode = Query | Subquery | OpNode | Follow | OrderSpec | SelectItem | Where | Expr;
|
|
199
|
+
/** The closed set of node kinds. */
|
|
200
|
+
export type AstKind = AstNode["kind"];
|
|
201
|
+
/** Whether a node is a scalar `Expr` (an `op` in expression position is not: it
|
|
202
|
+
* is an expression only by position, see AST.md §1). */
|
|
203
|
+
export declare function isExpr(node: AstNode): node is Expr;
|
|
204
|
+
/** The parsed query's JSON form (`toJSON`): the tree with the language version
|
|
205
|
+
* it was produced under. */
|
|
206
|
+
export type QueryJson = Query & {
|
|
207
|
+
oqx: string;
|
|
208
|
+
};
|
|
155
209
|
//# sourceMappingURL=ast.d.ts.map
|