@omgbase/oqx 0.9.0 → 0.10.1
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 +214 -0
- package/LICENSE +21 -0
- package/README.md +214 -8
- package/dist/engine.d.ts +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +32 -13
- package/dist/engine.js.map +1 -1
- package/dist/parser.js +6 -2
- package/dist/parser.js.map +1 -1
- package/dist/semantics.d.ts +15 -0
- package/dist/semantics.d.ts.map +1 -1
- package/dist/semantics.js +34 -0
- package/dist/semantics.js.map +1 -1
- package/package.json +27 -6
- package/src/engine.ts +34 -15
- package/src/parser.ts +5 -2
- package/src/semantics.ts +42 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@omgbase/oqx` are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/) (pre-1.0: a minor bump may break).
|
|
6
|
+
|
|
7
|
+
## [0.10.1] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
Metadata and documentation only; no code changes.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `LICENSE` file (MIT) — `package.json` previously said ISC with no license text.
|
|
13
|
+
- Package metadata: `author`, `repository`, `homepage`, `bugs`, `keywords`,
|
|
14
|
+
`sideEffects: false`, and a single consistent `description`.
|
|
15
|
+
- This changelog, shipped in the package.
|
|
16
|
+
- GitHub Actions CI (typecheck, test, build on Node 22 and 24).
|
|
17
|
+
- README: an "Exports" reference for every public export, the
|
|
18
|
+
`DataContext.callFunction` / `callMethod` extension seam (and the
|
|
19
|
+
"unknown function" pitfall of a context that does not delegate to the
|
|
20
|
+
builtins), `OqxError` and its `stage` discriminator, an `IndexedCollection`
|
|
21
|
+
example, `SqliteTableOptions` fields, string-literal escapes, `has(x)` and
|
|
22
|
+
array `contains`, and a License section.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `engines.node` is now `>=22.13.0` — the floor at which `node:sqlite` (used
|
|
26
|
+
only by the `./sqlite` subpath) is available without a flag. The main entry is
|
|
27
|
+
plain ES2022 ESM and runs on older Node in practice.
|
|
28
|
+
- README "Requirements" states the real floors (see above) and that the package
|
|
29
|
+
is ESM-only.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- README referred to `oqx.semantics`; the scalar contract is the named export
|
|
33
|
+
`semantics`.
|
|
34
|
+
|
|
35
|
+
## [0.10.0] - 2026-09-22
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- `entries(x)` builtin: the explicit bridge from a record to a collection of
|
|
39
|
+
`{ key, value }` entries. Objects yield own enumerable pairs in insertion
|
|
40
|
+
order, arrays yield `(index, element)`, `Map`s yield their entries; absence,
|
|
41
|
+
scalars, and ranges yield nothing. A plain object is still **not** iterable —
|
|
42
|
+
`from ${obj}` is one row.
|
|
43
|
+
- `$key` intrinsic: inside an entry scope, the entry's key. The scope's row is
|
|
44
|
+
the property's *value*, so `$value` and bare names read the value.
|
|
45
|
+
`$key` exists only in entry scopes — an array element has no implicit index.
|
|
46
|
+
- A free-function call may be a consumer receiver
|
|
47
|
+
(`entries(prefs) exists { … }`) and may be navigated further.
|
|
48
|
+
- `semantics`: `makeEntry`, `isEntry`, `entriesOf` (the entry tag is a
|
|
49
|
+
non-enumerable symbol, so `entries(x)` as a plain value reads as an array of
|
|
50
|
+
`{ key, value }` records).
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
- Engine: one `enter(row, parent, extra)` now creates every scope (top level,
|
|
54
|
+
nested blocks, `from` re-projections, follow seeds/occurrences, fast paths).
|
|
55
|
+
|
|
56
|
+
## [0.9.0] - 2026-09-21
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- `none { … }` consumer — true iff the block yields zero rows; the explicit
|
|
60
|
+
complement of `exists`, and how universal quantification is spelled
|
|
61
|
+
(`members none { where !active }`). Works in `where` position and as a
|
|
62
|
+
whole-query directive (`{ consumer: "none", none }`). Not comparable and not a
|
|
63
|
+
projection.
|
|
64
|
+
- `limit N` / `offset N` — bound the row set **after** `where` / `order by` /
|
|
65
|
+
`distinct` and **before** the consumer reduces it, at the top level and in any
|
|
66
|
+
block. `N` is a literal, a `${…}` binding, or an outer reference (`limit ^n`);
|
|
67
|
+
it must be a non-negative integer (eval error otherwise). `limit`/`offset` are
|
|
68
|
+
clause words only when followed by a number, binding, or `^`, so a field with
|
|
69
|
+
that name still reads as a field.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
- Engine: `exists` / `none` / `count` fast paths stop at the (offset+1)th match;
|
|
73
|
+
`first` / `single` caps grow by the offset; consumer ops share one `opRows`
|
|
74
|
+
pipeline (matched → ordered → distinct → bounded), which also makes
|
|
75
|
+
`collect distinct { ^x: … }` lifts dedup as documented.
|
|
76
|
+
- SQLite adapter: no SQL `LIMIT` under `first` / `single` when the query carries
|
|
77
|
+
its own `limit` / `offset` (the residual applies them).
|
|
78
|
+
|
|
79
|
+
## [0.8.0] - 2026-09-21
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
- `$value` intrinsic — the current scope's own row, whatever its type. Absent at
|
|
83
|
+
the root scope. Inside a nested block `$value` is the inner item and `^$value`
|
|
84
|
+
the enclosing row. Never resolved through the `DataContext`.
|
|
85
|
+
- `values` projection mode — after a projection of exactly one item, the row's
|
|
86
|
+
result is that value rather than a `{ name: value }` record. Works at the top
|
|
87
|
+
level and inside any `collect` / `first` / `single` block; composes with
|
|
88
|
+
`distinct` (dedup by the bare value). Cannot be combined with a lift.
|
|
89
|
+
- An unaliased projection item may be any value expression: a bare or dotted
|
|
90
|
+
navigation keys by its last segment; a call / arithmetic / comparison is legal
|
|
91
|
+
only under `values`, otherwise a clear "needs an alias" parse error.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
- `{ has(x) }` used to misparse as select `has` + where `(x)`; it is now the
|
|
95
|
+
alias error above (write `where has(x)`).
|
|
96
|
+
|
|
97
|
+
## [0.7.0] - 2026-09-19
|
|
98
|
+
|
|
99
|
+
### Changed
|
|
100
|
+
- **BREAKING:** bare names resolve **locally only**. A bare identifier reads the
|
|
101
|
+
current row/scope; an absent local name stays absent and never falls through
|
|
102
|
+
to an enclosing scope. Outer correlation is always the explicit `^name`
|
|
103
|
+
(exactly one scope out per caret; past the root is absent).
|
|
104
|
+
- **BREAKING:** `DataContext.has` removed — it existed only to stop the scope
|
|
105
|
+
climb; a computed relation now needs only `get`.
|
|
106
|
+
- A consumer receiver may start with `^` (`^people collect { … }`), since a
|
|
107
|
+
named root is reachable from inside a row only as `^root`.
|
|
108
|
+
- Planner helpers / SQLite adapter: a bare identifier in a top-level `where` is
|
|
109
|
+
now unambiguously a column of the scanned rows and is safe to push.
|
|
110
|
+
|
|
111
|
+
## [0.6.0] - 2026-09-17
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
- `range(s)` builtin: parse a string (`"1..5"`, `"1...5"`, `"..5"`,
|
|
115
|
+
`"2026-01-01..2026-01-31"`, …) into the range value a `lo..hi` literal
|
|
116
|
+
produces; a range passes through unchanged; anything else yields an absent
|
|
117
|
+
range, so `x in range(bad)` is simply false. Bounds must share a scalar domain
|
|
118
|
+
(numeric or ISO-8601).
|
|
119
|
+
- `semantics.parseRangeString`.
|
|
120
|
+
|
|
121
|
+
## [0.5.0] - 2026-09-16
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
- Ruby-style range literals: `lo..hi` (inclusive), `lo...hi` (exclusive high
|
|
125
|
+
end), and open-ended `..hi` / `lo..`. Used mainly as the right side of `in`
|
|
126
|
+
for interval membership; a range is a general value expression. Bounds
|
|
127
|
+
compare via the ordering rules, so ranges work over numbers and ISO-8601
|
|
128
|
+
date/time strings; an absent value is never covered.
|
|
129
|
+
- `semantics`: `makeRange`, `isRange`, `rangeCovers`; a `membership` branch for
|
|
130
|
+
ranges.
|
|
131
|
+
|
|
132
|
+
### Fixed
|
|
133
|
+
- Lexer: a decimal point now binds only when a digit follows, so `1..5` no
|
|
134
|
+
longer mis-lexes as `1.` + `.5`.
|
|
135
|
+
|
|
136
|
+
## [0.4.0] - 2026-09-16
|
|
137
|
+
|
|
138
|
+
### Added
|
|
139
|
+
- `distinct` modifier on consumers (top-level and nested): dedup the rows a
|
|
140
|
+
consumer reduces by their **projected** value, keeping the first per distinct
|
|
141
|
+
projection in order; an empty projection dedups by row identity. Spelled
|
|
142
|
+
`<op> distinct { … }` or `select distinct …` inside the block.
|
|
143
|
+
|
|
144
|
+
### Fixed
|
|
145
|
+
- Parser: `looksLikePredicate` skips nested `{ … }` blocks when scanning for a
|
|
146
|
+
top-level operator and recognizes a consumer directive
|
|
147
|
+
(`… exists { }`, `… count distinct { } == N`) as a where-position predicate.
|
|
148
|
+
|
|
149
|
+
## [0.3.0] - 2026-09-16
|
|
150
|
+
|
|
151
|
+
### Changed
|
|
152
|
+
- `follow` is now a **per-path** walk: a node reached by N distinct paths yields
|
|
153
|
+
N occurrences (previously collapsed by global identity dedup).
|
|
154
|
+
- Cycles are admitted **once** as `$stop == "cycle"` and never re-expanded
|
|
155
|
+
(previously a revisit produced no occurrence).
|
|
156
|
+
- `$stop` categories are `interior | leaf | frontier | depth | cycle`
|
|
157
|
+
(precedence cycle > frontier > depth > leaf > interior); only `interior` rows
|
|
158
|
+
expand. The old `"continue"` is renamed `"interior"`.
|
|
159
|
+
- `$frontier` covers both frontier and depth cutoffs; `$leaf` stays "no
|
|
160
|
+
successors".
|
|
161
|
+
|
|
162
|
+
### Added
|
|
163
|
+
- `$ordinal` intrinsic: a deterministic 1..N rank over `(depth, path)`.
|
|
164
|
+
|
|
165
|
+
### Fixed
|
|
166
|
+
- `follow distinct` now works: collapse occurrences to the minimal
|
|
167
|
+
`(depth, path)` per identity (`by` key, else `ctx.identity`).
|
|
168
|
+
|
|
169
|
+
## [0.2.0] - 2026-09-16
|
|
170
|
+
|
|
171
|
+
### Changed
|
|
172
|
+
- Ship a compiled `dist/` (JS + `.d.ts` + source maps) instead of raw `.ts`.
|
|
173
|
+
0.1.0 pointed `main` / `types` / `exports` at `src/*.ts`, which `tsc` rejects
|
|
174
|
+
under `module: NodeNext` and Node refuses to type-strip under `node_modules`.
|
|
175
|
+
- Added `tsconfig.build.json` (`rewriteRelativeImportExtensions`) and
|
|
176
|
+
`scripts/fix-dts-extensions.mjs` to rewrite `.ts` specifiers left in emitted
|
|
177
|
+
declarations; `build`, `clean`, and `prepublishOnly` scripts.
|
|
178
|
+
|
|
179
|
+
### Fixed
|
|
180
|
+
- `order by <expr> desc` hoisted rows missing the sort key to the top. Absent
|
|
181
|
+
now sorts last regardless of direction.
|
|
182
|
+
|
|
183
|
+
## [0.1.0] - 2026-09-14
|
|
184
|
+
|
|
185
|
+
First public release as `@omgbase/oqx`.
|
|
186
|
+
|
|
187
|
+
### Added
|
|
188
|
+
- The OQX language kernel: lexer, parser, AST; the `oqx` tagged template with
|
|
189
|
+
typed value bindings (prepared-statement semantics); `parse`, `execute`, `run`.
|
|
190
|
+
- Projection (optional `select`, aliases, dotted navigation), `where` predicates,
|
|
191
|
+
`order by`, consumers `collect` / `exists` / `count` / `first` / `single`,
|
|
192
|
+
nested consumer directives over relations, lifts (`^name:`) including
|
|
193
|
+
multi-level `^^` / `^^^` with flatten-append accumulation, `^name` outer
|
|
194
|
+
references, and bounded recursion with `follow` and its intrinsics.
|
|
195
|
+
- `semantics.ts` — the normative scalar contract every backend must obey
|
|
196
|
+
(typed/strict equality, absent-operand comparisons false, CEL-style `in`,
|
|
197
|
+
absent-last sort), backed by a conformance suite.
|
|
198
|
+
- `DataContext` / `DefaultContext` (tier 2), `Engine` / `InMemoryEngine`,
|
|
199
|
+
`QueryPlanner` / `Plan` / `PlannedEngine` (tier 3) with the pushdown helpers
|
|
200
|
+
in `plan.ts`, and two adapters: `IndexedCollection` (hash-index probe) and
|
|
201
|
+
`@omgbase/oqx/sqlite` (`SqliteTable`, real OQX→SQL pushdown over
|
|
202
|
+
`node:sqlite`).
|
|
203
|
+
|
|
204
|
+
[0.10.1]: https://github.com/omgbase/oqx/compare/v0.10.0...HEAD
|
|
205
|
+
[0.10.0]: https://github.com/omgbase/oqx/compare/v0.9.0...v0.10.0
|
|
206
|
+
[0.9.0]: https://github.com/omgbase/oqx/compare/v0.8.0...v0.9.0
|
|
207
|
+
[0.8.0]: https://github.com/omgbase/oqx/compare/v0.7.0...v0.8.0
|
|
208
|
+
[0.7.0]: https://github.com/omgbase/oqx/compare/v0.6.0...v0.7.0
|
|
209
|
+
[0.6.0]: https://github.com/omgbase/oqx/compare/v0.5.0...v0.6.0
|
|
210
|
+
[0.5.0]: https://github.com/omgbase/oqx/compare/v0.4.0...v0.5.0
|
|
211
|
+
[0.4.0]: https://github.com/omgbase/oqx/commits/v0.4.0
|
|
212
|
+
[0.3.0]: https://github.com/omgbase/oqx/commit/aca739b
|
|
213
|
+
[0.2.0]: https://github.com/omgbase/oqx/commit/82fb99e
|
|
214
|
+
[0.1.0]: https://github.com/omgbase/oqx/commit/4552bb5
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brendan Baldwin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -143,9 +143,32 @@ oqx`employee: $value from ${people} where name == "Bob"`; // [{ employee
|
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
Inside a nested block `$value` is the inner item; the enclosing row is `^$value`
|
|
146
|
-
(§3). At the root scope (before any row) it is absent.
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
(§3). At the root scope (before any row) it is absent.
|
|
147
|
+
|
|
148
|
+
**`entries(x)` and `$key` — records to collections, explicitly.** A plain object
|
|
149
|
+
is **not** iterable: `from ${obj}` is one row (the object). `entries(obj)`
|
|
150
|
+
converts it into a collection of entries, and inside such a scope the current
|
|
151
|
+
item is the property's **value** — `$value` and bare names read it — while
|
|
152
|
+
**`$key`** is the property's key:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
const settings = { theme: "dark", fontSize: 14, autosave: true };
|
|
156
|
+
oqx`key: $key, value: $value from entries(${settings})`;
|
|
157
|
+
// [{ key: "theme", value: "dark" }, { key: "fontSize", value: 14 }, { key: "autosave", value: true }]
|
|
158
|
+
oqx`$key values from entries(${settings}) where $value != "dark"`; // ["fontSize", "autosave"]
|
|
159
|
+
|
|
160
|
+
const flags = { beta: { on: true }, legacy: { on: false } };
|
|
161
|
+
oqx`$key values from entries(${flags}) where on`; // ["beta"] (bare `on` reads the value)
|
|
162
|
+
|
|
163
|
+
oqx`name, on: entries(prefs) collect { $key values where $value } from ${users}`; // as a nested receiver
|
|
164
|
+
oqx`name from ${users} where entries(prefs) exists { where $key == "dark" && $value }`;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`entries(array)` yields numeric index keys, a `Map` yields its entries, and
|
|
168
|
+
absence/scalars yield nothing. `$key` exists **only** in an entry scope — an
|
|
169
|
+
ordinary row or array element has no implicit index; `entries(arr)` is how you
|
|
170
|
+
ask for one. As a plain value (not a source) `entries(x)` is an array of
|
|
171
|
+
`{ key, value }` records.
|
|
149
172
|
|
|
150
173
|
### 3. Predicates (where)
|
|
151
174
|
|
|
@@ -164,6 +187,14 @@ oqx`name from ${people} where !active`; // → Carol
|
|
|
164
187
|
Equality is **typed and strict** (`5 == "5"` is false); a comparison against an
|
|
165
188
|
absent (`null`/`undefined`) field is simply false rather than an error.
|
|
166
189
|
|
|
190
|
+
**String literals** may be double- or single-quoted (`"NYC"` / `'NYC'`) and
|
|
191
|
+
support backslash escapes: `\n`, `\t`, `\r`, `\0`, and `\<any other char>`
|
|
192
|
+
for that character itself (`\"`, `\'`, `\\`). An unterminated string is a lex
|
|
193
|
+
error. Note that inside the `oqx` tagged template JavaScript resolves its own
|
|
194
|
+
escapes first (the template's cooked strings are what OQX lexes), so `\\n` in
|
|
195
|
+
your source reaches OQX as `\n`. Usually you don't need any of this —
|
|
196
|
+
interpolate the host value instead (`where name == ${name}`).
|
|
197
|
+
|
|
167
198
|
**Ranges.** A Ruby-style range `lo..hi` (inclusive) or `lo...hi` (exclusive high
|
|
168
199
|
end) is a value, used most often as the right side of `in`. Either bound may be
|
|
169
200
|
omitted for an open-ended range (`..hi`, `lo..`). Bounds compare with the same
|
|
@@ -257,13 +288,28 @@ like a row property.
|
|
|
257
288
|
|
|
258
289
|
Methods on a value: `contains`, `startsWith`, `endsWith`, `matches` (regex),
|
|
259
290
|
`size`, `lower`, `upper`. Free functions: `list(x)` (coerce to an array), `size(x)`,
|
|
260
|
-
`has(x)
|
|
291
|
+
`has(x)`, `range(s)` (§3), `entries(x)` (§2).
|
|
261
292
|
|
|
262
293
|
```js
|
|
263
294
|
oqx`name from ${people} where title.startsWith("Eng")`; // → Bob
|
|
264
295
|
oqx`name from ${people} where title.lower() == "director"`; // → Alice
|
|
296
|
+
oqx`name from ${people} where has(age) && !has(nickname)`; // present vs absent
|
|
297
|
+
oqx`name from ${people} where tags.contains("admin")`; // array membership
|
|
265
298
|
```
|
|
266
299
|
|
|
300
|
+
- `has(x)` is true when `x` is **present** — anything other than `null` /
|
|
301
|
+
`undefined`. `has(0)`, `has("")`, and `has(false)` are all true; use bare
|
|
302
|
+
truthiness (`where active`) when you mean truthy.
|
|
303
|
+
- `.contains(v)` works on strings (substring) **and arrays** (an element equal
|
|
304
|
+
to `v` under OQX's strict equality); on anything else it is false.
|
|
305
|
+
- `.size()` / `size(x)` is the length of a string or array, the key count of
|
|
306
|
+
an object, and 0 for absent; `.matches(re)` compiles its argument as a
|
|
307
|
+
JavaScript `RegExp`.
|
|
308
|
+
|
|
309
|
+
Anything not in these tables is an **eval error** (`unknown function 'f(…)'` /
|
|
310
|
+
`unknown method '.m(…)'`) — see [custom functions](#custom-functions-and-methods)
|
|
311
|
+
for adding your own.
|
|
312
|
+
|
|
267
313
|
### 5. Consumers
|
|
268
314
|
|
|
269
315
|
A consumer shapes a result set. There are six:
|
|
@@ -467,10 +513,12 @@ where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl.
|
|
|
467
513
|
where x in range(field) coerce a string field to a range, then test coverage
|
|
468
514
|
name the CURRENT row's field only (never climbs)
|
|
469
515
|
$value the current item itself (a scalar row, or the whole object)
|
|
516
|
+
from entries(obj) … $key / $value a record's properties as a collection (key + value; bare names read the value)
|
|
470
517
|
<expr> values scalar projection: the value, not a { name: value } record
|
|
471
518
|
^name / ^^name read an enclosing row's field (exactly N scopes out); ^$value = the enclosing row
|
|
472
519
|
^name: expr / ^^name: expr lift/export a value N scopes out (flatten-append)
|
|
473
520
|
^rel collect { … } / ^^root exists { … } nested consumer over an enclosing row's relation / a named root
|
|
521
|
+
entries(rel) exists { … } a free-function call may be a receiver
|
|
474
522
|
order by expr desc, expr2 ordering
|
|
475
523
|
limit n / offset n bound the row set (after where/order/distinct, before the consumer)
|
|
476
524
|
follow rel { where … frontier … depth n by … } recursion ($depth/$stop/$leaf/$frontier)
|
|
@@ -503,7 +551,7 @@ Query AST ─┬─ InMemoryEngine(DataContext) tier 1/2 — drive any data
|
|
|
503
551
|
└─ finishes the residual on the in-memory engine
|
|
504
552
|
```
|
|
505
553
|
|
|
506
|
-
**Everything obeys one scalar-semantics contract** (`
|
|
554
|
+
**Everything obeys one scalar-semantics contract** (the `semantics` export): typed/strict
|
|
507
555
|
equality (`5 == "5"` is false), absent operands make ordering comparisons false,
|
|
508
556
|
CEL-style `in`, absent-last sort order. Any backend that can't reproduce a rule
|
|
509
557
|
in its native language must leave that fragment as an in-memory *residual* rather
|
|
@@ -531,6 +579,41 @@ const graph = {
|
|
|
531
579
|
run(parse("id, depth: $depth from tree follow children"), { context: graph });
|
|
532
580
|
```
|
|
533
581
|
|
|
582
|
+
#### Custom functions and methods
|
|
583
|
+
|
|
584
|
+
`DataContext` has two optional hooks, `callFunction(name, args)` for free
|
|
585
|
+
functions (`f(x)`) and `callMethod(name, recv, args)` for methods (`x.m()`).
|
|
586
|
+
Each returns a `CallResult`: `{ handled: true, value }` to answer, or
|
|
587
|
+
`{ handled: false }` to decline. **The engine does not consult the builtin
|
|
588
|
+
table itself** — `DefaultContext` is what does that. So a context that omits
|
|
589
|
+
these hooks, or handles only its own names without deferring, loses
|
|
590
|
+
`entries()`, `size()`, `has()`, `range()`, `.contains()`, and the rest: the
|
|
591
|
+
engine throws `OqxError("unknown function 'size(…)'", "eval")`. (The `graph`
|
|
592
|
+
context above has exactly this limitation.) Delegate whatever you don't
|
|
593
|
+
recognize:
|
|
594
|
+
|
|
595
|
+
```js
|
|
596
|
+
import { DefaultContext, parse, run } from "@omgbase/oqx";
|
|
597
|
+
|
|
598
|
+
const builtins = new DefaultContext(); // or: semantics.BUILTIN_FUNCTIONS[name]
|
|
599
|
+
const ctx = {
|
|
600
|
+
...graph,
|
|
601
|
+
callFunction(name, args) {
|
|
602
|
+
if (name === "age") return { handled: true, value: yearsSince(args[0]) };
|
|
603
|
+
return builtins.callFunction(name, args); // entries/size/has/range keep working
|
|
604
|
+
},
|
|
605
|
+
callMethod(name, recv, args) {
|
|
606
|
+
if (name === "slug") return { handled: true, value: slugify(recv) };
|
|
607
|
+
return builtins.callMethod(name, recv, args);
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
run(parse("id from tree where age(born) > 18 && title.slug() == 'x'"), { context: ctx });
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
The simplest route is to `extends DefaultContext` and `super.callFunction(...)`
|
|
614
|
+
in the fallthrough. Custom calls are never pushed down by the shipped planners;
|
|
615
|
+
they always run in the residual.
|
|
616
|
+
|
|
534
617
|
### Tier 3 — a `QueryPlanner` (pushdown + planning)
|
|
535
618
|
|
|
536
619
|
A planner translates as much of a query as it can into its store's native query
|
|
@@ -559,11 +642,130 @@ blocks/nodes, the relations table, `$` intrinsics, `WITH RECURSIVE` for `follow`
|
|
|
559
642
|
becomes a `QueryPlanner`, while oqx contributes the parser, IR, semantics
|
|
560
643
|
contract, and residual executor.
|
|
561
644
|
|
|
645
|
+
## Exports
|
|
646
|
+
|
|
647
|
+
Everything below is exported from `@omgbase/oqx` (`src/index.ts`); the SQLite
|
|
648
|
+
adapter lives on the `@omgbase/oqx/sqlite` subpath.
|
|
649
|
+
|
|
650
|
+
**Running queries**
|
|
651
|
+
|
|
652
|
+
- `oqx` (default export **and** named) — the tagged template. Returns the
|
|
653
|
+
consumer-shaped value: an array for `collect`, a boolean for `exists` / `none`,
|
|
654
|
+
a number for `count`, a record or `null` for `first` / `single`.
|
|
655
|
+
- `parse(source)` — a query string → reusable `Query` AST (an `OqxError` with
|
|
656
|
+
`stage: "lex" | "parse"` on bad input).
|
|
657
|
+
- `execute(source, roots?)` — parse and run a string against named roots
|
|
658
|
+
(`{ people }`), returning the consumer-shaped value.
|
|
659
|
+
- `run(query, opts?)` — run a parsed `Query`, returning the full `OqxResult`.
|
|
660
|
+
`opts.values` are the positional `${…}` bindings; then exactly one of
|
|
661
|
+
`opts.engine` (any `Engine`, e.g. a `PlannedEngine`), `opts.context` (a
|
|
662
|
+
`DataContext`, run on the in-memory engine), or `opts.roots` (plain-object
|
|
663
|
+
named roots → `DefaultContext`). Default: an empty `DefaultContext`.
|
|
664
|
+
- `runQuery(query, values, roots)` — the lower-level call `oqx` / `execute` use:
|
|
665
|
+
in-memory over a `DefaultContext(roots)` (`roots` may be `undefined`).
|
|
666
|
+
- `OqxResult` — the discriminated result `run` returns:
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
type OqxResult =
|
|
670
|
+
| { consumer: "collect"; rows: unknown[] }
|
|
671
|
+
| { consumer: "exists"; exists: boolean }
|
|
672
|
+
| { consumer: "none"; none: boolean }
|
|
673
|
+
| { consumer: "count"; count: number }
|
|
674
|
+
| { consumer: "first"; row: unknown | null }
|
|
675
|
+
| { consumer: "single"; row: unknown | null };
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
**Errors**
|
|
679
|
+
|
|
680
|
+
- `OqxError` — the one error class for every failure. `error.stage` is
|
|
681
|
+
`"lex" | "parse" | "eval"` so you can branch without matching messages;
|
|
682
|
+
`error.message` carries the detail (position for lex/parse errors, the
|
|
683
|
+
offending name or count for eval errors). Eval errors include: unknown
|
|
684
|
+
function/method, a non-integer or negative `limit` / `offset`, and
|
|
685
|
+
`single { … }` matching **more than one** row (zero rows is `null`, not an
|
|
686
|
+
error — use `first` when zero-or-one is expected and you don't care to assert).
|
|
687
|
+
|
|
688
|
+
**Engines and contexts**
|
|
689
|
+
|
|
690
|
+
- `Engine` — `{ run(query, bindings): OqxResult }`; what `run({ engine })` accepts.
|
|
691
|
+
- `InMemoryEngine` — `new InMemoryEngine(context?)`; the reference engine over a
|
|
692
|
+
`DataContext` (tier 1/2).
|
|
693
|
+
- `DataContext` — the tier-2 interface: `root`, `get`, `toRows`, `identity`,
|
|
694
|
+
plus optional `callFunction` / `callMethod` (above).
|
|
695
|
+
- `CallResult` — `{ handled: boolean; value?: unknown }`, returned by those hooks.
|
|
696
|
+
- `DefaultContext` — `new DefaultContext(roots?)`; plain-object access, `.id`
|
|
697
|
+
identity, and the builtin function/method tables.
|
|
698
|
+
- `semantics` — the scalar-contract module (`equals`, `relate`, `arith`,
|
|
699
|
+
`membership`, `truthy`, `compareForSort`, `sizeOf`, `toList`,
|
|
700
|
+
`coerceCollection`, ranges: `makeRange` / `isRange` / `rangeCovers` /
|
|
701
|
+
`parseRangeString`, entries: `makeEntry` / `isEntry` / `entriesOf`, and the
|
|
702
|
+
`BUILTIN_FUNCTIONS` / `BUILTIN_METHODS` tables). A backend reproducing a rule
|
|
703
|
+
natively must match these.
|
|
704
|
+
|
|
705
|
+
**Planning (tier 3)**
|
|
706
|
+
|
|
707
|
+
- `QueryPlanner` — `{ plan(query, params): Plan | null }`. Return `null` to
|
|
708
|
+
decline a query entirely (full in-memory fallback).
|
|
709
|
+
- `Plan` — `{ rows(): Iterable; residual: Query; context?: DataContext }`: the
|
|
710
|
+
rows the store produced, the query to finish over them, and optionally a
|
|
711
|
+
context for navigating those rows' relations.
|
|
712
|
+
- `PlannedEngine` — `new PlannedEngine(planner, fallbackContext?)`; an `Engine`
|
|
713
|
+
that runs the planner and finishes `plan.residual` on the in-memory engine
|
|
714
|
+
over `plan.rows()` (exposed as the `ROWS_ROOT` root).
|
|
715
|
+
- Planner helpers, used together inside `plan()`: `partitionPushable(where,
|
|
716
|
+
canPush)` splits the top-level `where` conjunction into `pushed` expressions
|
|
717
|
+
(those your `canPush` accepts) and a `residual` where-tree; `asEquality(expr)`
|
|
718
|
+
recognizes `field == const` (either order) as `{ field, value }`;
|
|
719
|
+
`isConst(expr)` / `constValue(expr, params)` tell a literal-or-binding from a
|
|
720
|
+
row-dependent expression and evaluate it against the query bindings; and
|
|
721
|
+
`residualQuery(query, residualWhere)` rebuilds the query to scan `ROWS_ROOT`
|
|
722
|
+
with the pushed predicates dropped and projection / order / consumer / bounds
|
|
723
|
+
intact. A planner typically: checks `query.source` names its table (and
|
|
724
|
+
declines `from` / `follow`), partitions the `where`, translates `pushed` into
|
|
725
|
+
its native query using `constValue` for parameters, then returns
|
|
726
|
+
`{ rows, residual: residualQuery(query, residual) }`.
|
|
727
|
+
- `IndexedCollection` — `new IndexedCollection(rootName, rows, indexFields)`; a
|
|
728
|
+
`QueryPlanner` that hash-indexes `rows` on `indexFields` and answers
|
|
729
|
+
`field == value` predicates on them from the index, leaving the rest residual:
|
|
730
|
+
|
|
731
|
+
```js
|
|
732
|
+
import { parse, run, IndexedCollection, PlannedEngine } from "@omgbase/oqx";
|
|
733
|
+
const planner = new IndexedCollection("people", people, ["city", "title"]);
|
|
734
|
+
const engine = new PlannedEngine(planner);
|
|
735
|
+
run(parse('name from people where city == "NYC" && age > 30'), { engine });
|
|
736
|
+
// city probe from the index; `age > 30` finished in-memory over the candidates
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
- `SqliteTable` (from `@omgbase/oqx/sqlite`) —
|
|
740
|
+
`new SqliteTable(db, tableName, options)` over a `node:sqlite` `DatabaseSync`.
|
|
741
|
+
`SqliteTableOptions`:
|
|
742
|
+
- `columns: string[]` — the columns that map to bare OQX fields; only these
|
|
743
|
+
are pushable (any other identifier stays residual).
|
|
744
|
+
- `jsonColumns?: string[]` — columns whose stored text is `JSON.parse`d back
|
|
745
|
+
into the row (for nested relations kept as JSON).
|
|
746
|
+
- `map?: (raw) => row` — a custom raw-SQL-row → query-row mapper (overrides
|
|
747
|
+
`jsonColumns`).
|
|
748
|
+
|
|
749
|
+
**Types**
|
|
750
|
+
|
|
751
|
+
- Every AST node type from `src/ast.ts` is re-exported (`Query`, `Subquery`,
|
|
752
|
+
`Where`, `Expr`, `OpNode`, `SelectItem`, `OrderSpec`, `Follow`, …) for
|
|
753
|
+
planners that walk the IR.
|
|
754
|
+
|
|
562
755
|
## Requirements
|
|
563
756
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
757
|
+
No runtime dependencies. The package is **ESM-only** (there is no `require`
|
|
758
|
+
condition in `exports`; Node 22.12+ can `require()` an ES module natively).
|
|
759
|
+
|
|
760
|
+
- **Main entry** (`@omgbase/oqx`): compiled ES2022 ESM. `engines.node` says
|
|
761
|
+
`>=22.13.0`, but nothing in the main entry needs it — Node 18+ works in
|
|
762
|
+
practice.
|
|
763
|
+
- **`@omgbase/oqx/sqlite`**: imports `node:sqlite`, which is available without
|
|
764
|
+
a flag from Node 22.13 (behind `--experimental-sqlite` in 22.5–22.12). This is
|
|
765
|
+
the reason for the `engines` floor.
|
|
766
|
+
- **Developing the repo**: `npm test` runs the `.ts` suite directly through
|
|
767
|
+
Node's type stripping, unflagged from Node 22.18 (and all of 24). `npm run
|
|
768
|
+
typecheck` typechecks; `npm run build` emits `dist/`.
|
|
567
769
|
|
|
568
770
|
## Relationship to omgbase
|
|
569
771
|
|
|
@@ -571,3 +773,7 @@ This is tier 1 (the in-memory object/collection interpreter) of the OQX
|
|
|
571
773
|
implementation tiers. The language kernel here is host-agnostic; richer hosts
|
|
572
774
|
(e.g. omgbase's docs/blocks/nodes with index pushdown) layer data-model
|
|
573
775
|
vocabulary and execution capabilities on top of the same surface syntax.
|
|
776
|
+
|
|
777
|
+
## License
|
|
778
|
+
|
|
779
|
+
[MIT](./LICENSE). Release history is in the [CHANGELOG](./CHANGELOG.md).
|
package/dist/engine.d.ts
CHANGED
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,KAAK,EACN,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAOhD,yEAAyE;AACzE,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,EAAE,CAAA;CAAE,GACxC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACvC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GACnC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,GAC1C;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CAAC;AAEhD,6EAA6E;AAC7E,MAAM,WAAW,MAAM;IACrB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,CAAC;CAC5D;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,KAAK,EACN,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAOhD,yEAAyE;AACzE,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,EAAE,CAAA;CAAE,GACxC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACvC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GACnC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,GAC1C;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CAAC;AAEhD,6EAA6E;AAC7E,MAAM,WAAW,MAAM;IACrB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,CAAC;CAC5D;AA4BD,qBAAa,cAAe,YAAW,MAAM;IAC3C,OAAO,CAAC,GAAG,CAAc;gBAEb,OAAO,GAAE,WAAkC;IAIvD,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,GAAE,SAAS,OAAO,EAAO,GAAG,SAAS;IAmD/D,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,iBAAiB;IA2BzB,OAAO,CAAC,UAAU;IA6DlB,OAAO,CAAC,KAAK;IAeb,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,WAAW;IAiCnB,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAE,aAAa;IAStB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,gBAAgB;IA2BxB,OAAO,CAAC,UAAU;IAwBlB,OAAO,CAAC,QAAQ;IAwDhB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,QAAQ;CAYjB;AAyFD,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,CAG9F"}
|
package/dist/engine.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
// fall-through from an inner scope to an outer one — see `resolveIn`.
|
|
14
14
|
import { DefaultContext } from "./context.js";
|
|
15
15
|
import { OqxError } from "./errors.js";
|
|
16
|
-
import { equals, relate, arith, membership, truthy, toNumber, compareForSort, makeRange, } from "./semantics.js";
|
|
16
|
+
import { equals, relate, arith, membership, truthy, toNumber, compareForSort, makeRange, isEntry, } from "./semantics.js";
|
|
17
17
|
const RECUR = new Set(["$depth", "$stop", "$leaf", "$frontier", "$ordinal"]);
|
|
18
|
+
const KEY = "$key";
|
|
18
19
|
const HARD_DEPTH_CAP = 8;
|
|
19
20
|
const UNBOUNDED = { offset: 0, limit: null };
|
|
20
21
|
export class InMemoryEngine {
|
|
@@ -60,7 +61,7 @@ export class InMemoryEngine {
|
|
|
60
61
|
: Infinity;
|
|
61
62
|
let kept = [];
|
|
62
63
|
for (const r of rows) {
|
|
63
|
-
const s =
|
|
64
|
+
const s = this.enter(r, root, { lifts: {} });
|
|
64
65
|
if (!query.where || this.evalWhere(query.where, s)) {
|
|
65
66
|
kept.push(s);
|
|
66
67
|
if (kept.length >= cap)
|
|
@@ -97,20 +98,37 @@ export class InMemoryEngine {
|
|
|
97
98
|
matches(where, row, parent) {
|
|
98
99
|
if (!where)
|
|
99
100
|
return true;
|
|
100
|
-
return this.evalWhere(where,
|
|
101
|
+
return this.evalWhere(where, this.enter(row, parent));
|
|
101
102
|
}
|
|
102
103
|
rowsOf(v) {
|
|
103
104
|
return Array.from(this.ctx.toRows(v));
|
|
104
105
|
}
|
|
105
106
|
child(row, parent) {
|
|
106
|
-
return
|
|
107
|
+
return this.enter(row, parent);
|
|
108
|
+
}
|
|
109
|
+
// Make the scope for a row. An `entries()` entry is unwrapped here: the scope's
|
|
110
|
+
// row is the property's VALUE (so `$value` and bare names read it) and the key
|
|
111
|
+
// becomes the `$key` intrinsic in `meta`. Every place a row becomes a scope
|
|
112
|
+
// goes through this, so entries behave the same at the top level, in nested
|
|
113
|
+
// blocks, as `from` re-projections, and as follow seeds.
|
|
114
|
+
enter(row, parent, extra = {}) {
|
|
115
|
+
const s = { row, parent, bindings: parent.bindings };
|
|
116
|
+
if (extra.lifts)
|
|
117
|
+
s.lifts = extra.lifts;
|
|
118
|
+
if (extra.meta)
|
|
119
|
+
s.meta = extra.meta;
|
|
120
|
+
if (isEntry(row)) {
|
|
121
|
+
s.row = row.value;
|
|
122
|
+
s.meta = { ...(s.meta ?? {}), [KEY]: row.key };
|
|
123
|
+
}
|
|
124
|
+
return s;
|
|
107
125
|
}
|
|
108
126
|
// ---- follow ---------------------------------------------------------------
|
|
109
127
|
runFollow(query, rows, root, bound) {
|
|
110
128
|
const { seed, post } = query.where ? partitionRecur(query.where) : { seed: null, post: null };
|
|
111
129
|
const seeds = seed ? rows.filter((r) => this.matches(seed, r, root)) : rows;
|
|
112
130
|
const occ = this.followWalk(seeds, query.follow, root);
|
|
113
|
-
let scopes = occ.map((o) => (
|
|
131
|
+
let scopes = occ.map((o) => this.enter(o.row, root, { lifts: {}, meta: o.meta }));
|
|
114
132
|
if (post)
|
|
115
133
|
scopes = scopes.filter((s) => this.evalWhere(post, s));
|
|
116
134
|
this.sortScopes(scopes, query.orderBy);
|
|
@@ -151,7 +169,7 @@ export class InMemoryEngine {
|
|
|
151
169
|
// `ctx.identity(row)`.
|
|
152
170
|
followWalk(seedRows, follow, parent) {
|
|
153
171
|
const cap = follow.depth ?? HARD_DEPTH_CAP;
|
|
154
|
-
const scopeFor = (row) => (
|
|
172
|
+
const scopeFor = (row) => this.enter(row, parent);
|
|
155
173
|
const keyOf = (row) => String(follow.by ? this.evalExpr(follow.by, scopeFor(row)) : this.ctx.identity(row));
|
|
156
174
|
const succOf = (row) => {
|
|
157
175
|
const raw = this.rowsOf(this.evalExpr(follow.receiver, scopeFor(row)));
|
|
@@ -309,7 +327,7 @@ export class InMemoryEngine {
|
|
|
309
327
|
for (const proj of op.sub.from)
|
|
310
328
|
rows = rows.flatMap((r) => this.rowsOf(this.evalExpr(proj, this.child(r, scope))));
|
|
311
329
|
for (const r of rows) {
|
|
312
|
-
const s =
|
|
330
|
+
const s = this.enter(r, scope, { lifts: {} });
|
|
313
331
|
if (!op.sub.where || this.evalWhere(op.sub.where, s))
|
|
314
332
|
yield s;
|
|
315
333
|
}
|
|
@@ -326,8 +344,8 @@ export class InMemoryEngine {
|
|
|
326
344
|
let rows = this.rowsOf(this.evalExpr(op.receiver, scope));
|
|
327
345
|
for (const proj of sub.from)
|
|
328
346
|
rows = rows.flatMap((r) => this.rowsOf(this.evalExpr(proj, this.child(r, scope))));
|
|
329
|
-
const seeds = sub.where ? rows.filter((r) => this.evalWhere(sub.where,
|
|
330
|
-
scopes = this.followWalk(seeds, sub.follow, scope).map((o) => (
|
|
347
|
+
const seeds = sub.where ? rows.filter((r) => this.evalWhere(sub.where, this.enter(r, scope, { lifts: {} }))) : rows;
|
|
348
|
+
scopes = this.followWalk(seeds, sub.follow, scope).map((o) => this.enter(o.row, scope, { meta: o.meta }));
|
|
331
349
|
this.sortScopes(scopes, sub.orderBy);
|
|
332
350
|
if (op.distinct)
|
|
333
351
|
scopes = this.dedupByProjection(scopes, sub);
|
|
@@ -417,9 +435,10 @@ export class InMemoryEngine {
|
|
|
417
435
|
// Resolve a name against ONE scope — never its ancestors. A scope provides,
|
|
418
436
|
// in order: `$value` (the scope's row itself — the current item, whatever its
|
|
419
437
|
// type, so scalar collections are queryable; absent at the root, which has no
|
|
420
|
-
// row); the
|
|
421
|
-
//
|
|
422
|
-
//
|
|
438
|
+
// row); `$key` (the property key, for an entry scope only — see `enter`); the
|
|
439
|
+
// recursion intrinsics (`$depth`, …) when it is a follow occurrence; values
|
|
440
|
+
// lifted into it by `^name:` items; then either the row's own property or,
|
|
441
|
+
// for the root scope (no row), the context's named roots.
|
|
423
442
|
//
|
|
424
443
|
// A name the scope lacks is simply absent (undefined). It does NOT fall
|
|
425
444
|
// through to an enclosing scope, so a query's meaning never depends on which
|
|
@@ -430,7 +449,7 @@ export class InMemoryEngine {
|
|
|
430
449
|
resolveIn(name, scope) {
|
|
431
450
|
if (name === "$value")
|
|
432
451
|
return scope.parent === null ? undefined : scope.row;
|
|
433
|
-
if (RECUR.has(name))
|
|
452
|
+
if (name === KEY || RECUR.has(name))
|
|
434
453
|
return scope.meta ? scope.meta[name] : undefined;
|
|
435
454
|
if (scope.lifts && name in scope.lifts)
|
|
436
455
|
return scope.lifts[name];
|