@omgbase/oqx 0.7.0 → 0.9.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/README.md +80 -6
- package/dist/adapters/sqlite.d.ts.map +1 -1
- package/dist/adapters/sqlite.js +5 -3
- package/dist/adapters/sqlite.js.map +1 -1
- package/dist/ast.d.ts +20 -2
- package/dist/ast.d.ts.map +1 -1
- package/dist/engine.d.ts +6 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +118 -51
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +95 -21
- package/dist/parser.js.map +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +3 -1
- package/dist/plan.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/sqlite.ts +5 -3
- package/src/ast.ts +21 -3
- package/src/engine.ts +116 -44
- package/src/index.ts +3 -2
- package/src/parser.ts +87 -20
- package/src/plan.ts +3 -1
package/README.md
CHANGED
|
@@ -101,8 +101,51 @@ oqx`label: name, decade: age / 10 from ${people} where name == "Bob"`;
|
|
|
101
101
|
// [{ label: "Bob", decade: 4.1 }]
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
An item that is not a plain navigation — a call, arithmetic, a comparison — has
|
|
105
|
+
no natural key, so it must be aliased (`n: size(jobs)`), unless the projection is
|
|
106
|
+
in `values` mode (next).
|
|
107
|
+
|
|
104
108
|
The `select` keyword is optional and works in any position — `select name from …`
|
|
105
|
-
is identical to `name from
|
|
109
|
+
is identical to `name from …` (and hosts `select distinct`, §6).
|
|
110
|
+
|
|
111
|
+
**`values` — scalar projection.** Ordinarily every row projects to a record. Add
|
|
112
|
+
`values` after a projection of exactly **one** item to get the value itself:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
oqx`name values from ${people}`; // ["Bob", "Alice", "Carol"]
|
|
116
|
+
oqx`name.upper() values from ${people} where age < 30`; // ["CAROL"] (no alias needed)
|
|
117
|
+
oqx`${people} first { name values where age > 50 }`; // "Alice"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`values` is a result-shape mode, not a consumer: it works in the top-level
|
|
121
|
+
projection and inside any `collect` / `first` / `single` block, and composes with
|
|
122
|
+
`distinct` (`select distinct employer values from ${jobs}` is the distinct set of
|
|
123
|
+
employers as strings, not `{ employer }` records). An alias, if present, is
|
|
124
|
+
ignored; a lift (`^name:`) cannot be combined with it.
|
|
125
|
+
|
|
126
|
+
**`$value` — the current item.** Every scope has a current value; `$value` is
|
|
127
|
+
that exact value, whatever its type (an object row or a plain scalar). Bare names
|
|
128
|
+
still navigate it (`name` ≡ `$value.name`), so `$value` matters exactly where
|
|
129
|
+
there is nothing to navigate: collections of numbers or strings, or handing the
|
|
130
|
+
whole row somewhere. Together with `values` this makes scalar collections
|
|
131
|
+
first-class:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const scores = [10, 60, 70, 45];
|
|
135
|
+
oqx`$value values from ${scores} where $value > 50`; // [60, 70]
|
|
136
|
+
oqx`$value values from ${scores} order by $value desc`; // [70, 60, 45, 10]
|
|
137
|
+
|
|
138
|
+
const players = [{ name: "Ann", scores: [10, 60, 70] }, { name: "Ben", scores: [45] }];
|
|
139
|
+
oqx`name, big: scores collect { $value values where $value > 50 } from ${players}`;
|
|
140
|
+
// [{ name: "Ann", big: [60, 70] }, { name: "Ben", big: [] }]
|
|
141
|
+
|
|
142
|
+
oqx`employee: $value from ${people} where name == "Bob"`; // [{ employee: <Bob> }]
|
|
143
|
+
```
|
|
144
|
+
|
|
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. Plain objects are **not**
|
|
147
|
+
iterable through `$value` — a record stays a record; converting one to a
|
|
148
|
+
collection of entries is a separate, explicit step (`entries()`, planned).
|
|
106
149
|
|
|
107
150
|
### 3. Predicates (where)
|
|
108
151
|
|
|
@@ -223,12 +266,13 @@ oqx`name from ${people} where title.lower() == "director"`; // → Alice
|
|
|
223
266
|
|
|
224
267
|
### 5. Consumers
|
|
225
268
|
|
|
226
|
-
A consumer shapes a result set. There are
|
|
269
|
+
A consumer shapes a result set. There are six:
|
|
227
270
|
|
|
228
271
|
| Consumer | Returns |
|
|
229
272
|
| --------- | ------------------------------ |
|
|
230
273
|
| `collect` | an array (the default) |
|
|
231
|
-
| `exists` | a boolean
|
|
274
|
+
| `exists` | a boolean — one or more rows |
|
|
275
|
+
| `none` | a boolean — zero rows (the complement of `exists`) |
|
|
232
276
|
| `count` | a number |
|
|
233
277
|
| `first` | one record, or `null` |
|
|
234
278
|
| `single` | one record, or `null`; throws if more than one matches |
|
|
@@ -258,8 +302,14 @@ cardinality:
|
|
|
258
302
|
```js
|
|
259
303
|
oqx`name from ${people} where jobs exists { !end }`; // has a current job → Bob, Carol
|
|
260
304
|
oqx`name from ${people} where jobs count {} >= 2`; // ≥2 jobs → Bob, Alice
|
|
305
|
+
oqx`name from ${people} where jobs none { where end }`; // no past job → Carol
|
|
261
306
|
```
|
|
262
307
|
|
|
308
|
+
`none { … }` is exactly `!… exists { … }`, kept as its own word because the
|
|
309
|
+
cardinality is the point. It is also how you say "every": *all* members active is
|
|
310
|
+
`members none { where !active }` — there is deliberately no `all { … }`, whose
|
|
311
|
+
block would have to mean something different from every other consumer's.
|
|
312
|
+
|
|
263
313
|
In a projection, `collect` yields a nested array; `first` / `single` yield one
|
|
264
314
|
nested record:
|
|
265
315
|
|
|
@@ -340,6 +390,26 @@ oqx`name from ${people} where city == "NYC" order by age desc`;
|
|
|
340
390
|
// [{ name: "Bob" }, { name: "Carol" }]
|
|
341
391
|
```
|
|
342
392
|
|
|
393
|
+
### 8b. Bounding: `limit` / `offset`
|
|
394
|
+
|
|
395
|
+
`limit N` and `offset N` bound the row set **after** `where`, `order by`, and
|
|
396
|
+
`distinct`, and **before** the consumer reduces it — so they mean the same thing
|
|
397
|
+
under every consumer: `count { … limit 5 }` is at most 5, `first { … offset 1 }`
|
|
398
|
+
is the second row, `exists { offset 2 }` asks for a third. They work at the top
|
|
399
|
+
level and inside any block, and `N` may be a literal, a `${…}` binding, or an
|
|
400
|
+
outer reference — `limit ^n` reads the enclosing row's `n`, as `^` does everywhere
|
|
401
|
+
inside `{ … }`; it must be a non-negative integer.
|
|
402
|
+
|
|
403
|
+
```js
|
|
404
|
+
oqx`name values from ${people} order by age desc limit 2`; // ["Alice", "Bob"]
|
|
405
|
+
oqx`name values from ${people} order by age desc offset 1 limit 1`; // ["Bob"]
|
|
406
|
+
oqx`name, latest: jobs collect { employer values order by start desc limit 1 } from ${people}`;
|
|
407
|
+
oqx`name from ${people} where jobs exists { offset 1 }`; // has a second job
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
A storage adapter that pushes the whole query may translate them to SQL
|
|
411
|
+
`LIMIT`/`OFFSET`; the shipped `SqliteTable` leaves them to the residual.
|
|
412
|
+
|
|
343
413
|
### 9. Recursion: `follow`
|
|
344
414
|
|
|
345
415
|
`follow <relation>` turns a query into a bounded recursive traversal: the `where`
|
|
@@ -392,15 +462,19 @@ your relation returns fresh objects rather than shared references.
|
|
|
392
462
|
name, alias: expr, nested: rel collect { … } projection (select optional)
|
|
393
463
|
from ${source} source collection
|
|
394
464
|
where a == b && rel exists { … } || !c predicate tree + nested ops
|
|
465
|
+
where rel none { … } zero rows (≡ !rel exists { … }; "all" = none over the complement)
|
|
395
466
|
where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl. / open-ended)
|
|
396
467
|
where x in range(field) coerce a string field to a range, then test coverage
|
|
397
468
|
name the CURRENT row's field only (never climbs)
|
|
398
|
-
|
|
469
|
+
$value the current item itself (a scalar row, or the whole object)
|
|
470
|
+
<expr> values scalar projection: the value, not a { name: value } record
|
|
471
|
+
^name / ^^name read an enclosing row's field (exactly N scopes out); ^$value = the enclosing row
|
|
399
472
|
^name: expr / ^^name: expr lift/export a value N scopes out (flatten-append)
|
|
400
473
|
^rel collect { … } / ^^root exists { … } nested consumer over an enclosing row's relation / a named root
|
|
401
474
|
order by expr desc, expr2 ordering
|
|
475
|
+
limit n / offset n bound the row set (after where/order/distinct, before the consumer)
|
|
402
476
|
follow rel { where … frontier … depth n by … } recursion ($depth/$stop/$leaf/$frontier)
|
|
403
|
-
${source} <collect|exists|count|first|single> { … } whole-query consumer
|
|
477
|
+
${source} <collect|exists|none|count|first|single> { … } whole-query consumer
|
|
404
478
|
```
|
|
405
479
|
|
|
406
480
|
## Data context: string queries and named roots
|
|
@@ -482,7 +556,7 @@ new PlannedEngine(planner).run(parse('name from emp where dept == "eng" && level
|
|
|
482
556
|
|
|
483
557
|
This is the seam an omgbase adapter uses: its existing OQX→SQL compiler (docs/
|
|
484
558
|
blocks/nodes, the relations table, `$` intrinsics, `WITH RECURSIVE` for `follow`)
|
|
485
|
-
becomes a `QueryPlanner`, while oqx
|
|
559
|
+
becomes a `QueryPlanner`, while oqx contributes the parser, IR, semantics
|
|
486
560
|
contract, and residual executor.
|
|
487
561
|
|
|
488
562
|
## Requirements
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/adapters/sqlite.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAQ,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGxD,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,+EAA+E;IAC/E,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;CACjD;AAOD,qBAAa,WAAY,YAAW,YAAY;IAC9C,OAAO,CAAC,EAAE,CAAe;IACzB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,IAAI,CAAqB;gBAErB,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB;IAOrE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/adapters/sqlite.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAQ,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGxD,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,+EAA+E;IAC/E,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;CACjD;AAOD,qBAAa,WAAY,YAAW,YAAY;IAC9C,OAAO,CAAC,EAAE,CAAe;IACzB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,IAAI,CAAqB;gBAErB,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB;IAOrE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,GAAG,IAAI;IAwB3D,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,MAAM;CAWf"}
|
package/dist/adapters/sqlite.js
CHANGED
|
@@ -36,10 +36,12 @@ export class SqliteTable {
|
|
|
36
36
|
const { pushed, residual } = partitionPushable(query.where, (e) => this.translatable(e));
|
|
37
37
|
const sqlParams = [];
|
|
38
38
|
const whereSql = pushed.map((e) => this.translate(e, params, sqlParams)).join(" AND ");
|
|
39
|
-
// A LIMIT is only safe when nothing is left to filter in-memory
|
|
40
|
-
//
|
|
39
|
+
// A LIMIT is only safe when nothing is left to filter in-memory, the result
|
|
40
|
+
// is unordered (first/single are "some row" without an order by), and the
|
|
41
|
+
// query carries no limit/offset of its own (the residual applies those, so
|
|
42
|
+
// a SQL LIMIT underneath would starve them).
|
|
41
43
|
let tail = "";
|
|
42
|
-
if (!residual && !query.orderBy) {
|
|
44
|
+
if (!residual && !query.orderBy && !query.limit && !query.offset) {
|
|
43
45
|
if (query.consumer === "first")
|
|
44
46
|
tail = " LIMIT 1";
|
|
45
47
|
else if (query.consumer === "single")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/adapters/sqlite.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,gFAAgF;AAChF,iFAAiF;AACjF,8BAA8B;AAC9B,EAAE;AACF,gFAAgF;AAChF,6DAA6D;AAC7D,EAAE;AACF,+EAA+E;AAC/E,4EAA4E;AAC5E,6EAA6E;AAE7E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAW9D,MAAM,SAAS,GAA2B;IACxC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI;CAClE,CAAC;AACF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAErD,MAAM,OAAO,WAAW;IACd,EAAE,CAAe;IACjB,KAAK,CAAS;IACd,OAAO,CAAc;IACrB,IAAI,CAAqB;IAEjC,YAAY,EAAgB,EAAE,KAAa,EAAE,IAAwB;QACnE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,IAAI,CAAC,KAAY,EAAE,MAA0B;QAC3C,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACnF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEvD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,iBAAiB,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QACzF,MAAM,SAAS,GAAc,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAEvF,
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/adapters/sqlite.ts"],"names":[],"mappings":"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"}
|
package/dist/ast.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/** Query consumers — how a (sub)query's row set is shaped.
|
|
2
|
-
|
|
1
|
+
/** Query consumers — how a (sub)query's row set is shaped. `none` is the
|
|
2
|
+
* zero-cardinality complement of `exists` (true iff the block yields no rows). */
|
|
3
|
+
export type Consumer = "collect" | "exists" | "none" | "count" | "first" | "single";
|
|
3
4
|
/** Comparison operators usable in a `count { … } <op> <int>` test. */
|
|
4
5
|
export type RelOp = "==" | "!=" | "<" | "<=" | ">" | ">=";
|
|
5
6
|
/** Scalar value/predicate expression, evaluated against a row scope + bindings. */
|
|
@@ -102,6 +103,18 @@ export interface Subquery {
|
|
|
102
103
|
select: SelectItem[];
|
|
103
104
|
orderBy: OrderSpec[] | null;
|
|
104
105
|
follow: Follow | null;
|
|
106
|
+
/** `values` — scalar projection mode: the (single) projected expression is the
|
|
107
|
+
* row's result itself rather than being wrapped in a `{ name: value }` record,
|
|
108
|
+
* so `name values` yields `["Bob", …]` and `$value values` yields the rows. */
|
|
109
|
+
values?: boolean;
|
|
110
|
+
/** `limit N` / `offset N` — bound the row set AFTER where/order/distinct and
|
|
111
|
+
* BEFORE the consumer reduces it, so `count { … limit 5 }` is at most 5 and
|
|
112
|
+
* `first { … offset 1 }` is the second row. Each is a value expression
|
|
113
|
+
* (a literal, a `${…}` binding, or an outer reference) read as part of the
|
|
114
|
+
* block — `^n` is the enclosing row's field, as everywhere inside `{ … }` —
|
|
115
|
+
* and must yield a non-negative integer. */
|
|
116
|
+
limit?: Expr;
|
|
117
|
+
offset?: Expr;
|
|
105
118
|
}
|
|
106
119
|
/** The where-clause boolean tree: OQX owns &&/||/!/grouping so consumer ops
|
|
107
120
|
* (invisible to the scalar evaluator) compose with scalar predicates. */
|
|
@@ -129,5 +142,10 @@ export interface Query {
|
|
|
129
142
|
follow: Follow | null;
|
|
130
143
|
/** `distinct` — dedup the result rows by their projected value (see OpNode). */
|
|
131
144
|
distinct?: boolean;
|
|
145
|
+
/** `values` — scalar projection mode (see Subquery). */
|
|
146
|
+
values?: boolean;
|
|
147
|
+
/** `limit N` / `offset N` (see Subquery); evaluated at the root scope. */
|
|
148
|
+
limit?: Expr;
|
|
149
|
+
offset?: Expr;
|
|
132
150
|
}
|
|
133
151
|
//# sourceMappingURL=ast.d.ts.map
|
package/dist/ast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAMA
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export type OqxResult = {
|
|
|
7
7
|
} | {
|
|
8
8
|
consumer: "exists";
|
|
9
9
|
exists: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
consumer: "none";
|
|
12
|
+
none: boolean;
|
|
10
13
|
} | {
|
|
11
14
|
consumer: "count";
|
|
12
15
|
count: number;
|
|
@@ -25,6 +28,7 @@ export declare class InMemoryEngine implements Engine {
|
|
|
25
28
|
private ctx;
|
|
26
29
|
constructor(context?: DataContext);
|
|
27
30
|
run(query: Query, bindings?: readonly unknown[]): OqxResult;
|
|
31
|
+
private boundOf;
|
|
28
32
|
private matches;
|
|
29
33
|
private rowsOf;
|
|
30
34
|
private child;
|
|
@@ -33,8 +37,10 @@ export declare class InMemoryEngine implements Engine {
|
|
|
33
37
|
private followWalk;
|
|
34
38
|
private shape;
|
|
35
39
|
private projectRow;
|
|
40
|
+
private itemValue;
|
|
36
41
|
private evalWhere;
|
|
37
42
|
private evalWhereOp;
|
|
43
|
+
private opRows;
|
|
38
44
|
private anyMatch;
|
|
39
45
|
private iterMatchRows;
|
|
40
46
|
private matchRows;
|
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,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;AA0BD,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;IAMb,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;IAuDhB,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
|
@@ -16,6 +16,7 @@ import { OqxError } from "./errors.js";
|
|
|
16
16
|
import { equals, relate, arith, membership, truthy, toNumber, compareForSort, makeRange, } from "./semantics.js";
|
|
17
17
|
const RECUR = new Set(["$depth", "$stop", "$leaf", "$frontier", "$ordinal"]);
|
|
18
18
|
const HARD_DEPTH_CAP = 8;
|
|
19
|
+
const UNBOUNDED = { offset: 0, limit: null };
|
|
19
20
|
export class InMemoryEngine {
|
|
20
21
|
ctx;
|
|
21
22
|
constructor(context = new DefaultContext()) {
|
|
@@ -27,26 +28,36 @@ export class InMemoryEngine {
|
|
|
27
28
|
for (const proj of query.from) {
|
|
28
29
|
rows = rows.flatMap((r) => this.rowsOf(this.evalExpr(proj, this.child(r, root))));
|
|
29
30
|
}
|
|
31
|
+
const bound = this.boundOf(query, root);
|
|
30
32
|
if (query.follow)
|
|
31
|
-
return this.runFollow(query, rows, root);
|
|
33
|
+
return this.runFollow(query, rows, root, bound);
|
|
32
34
|
// Consumer-directed short-circuits (skipped under `distinct`, which must
|
|
33
|
-
// materialize + dedup by projection before reducing).
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
// materialize + dedup by projection before reducing). Counting never
|
|
36
|
+
// materializes rows; exists/none stop as soon as the bound is known to be
|
|
37
|
+
// non-empty (the (offset+1)th match — or the first, when unbounded).
|
|
38
|
+
if (!query.distinct && (query.consumer === "exists" || query.consumer === "none" || query.consumer === "count")) {
|
|
39
|
+
const need = query.consumer === "count" ? Infinity : bound.offset + 1;
|
|
40
|
+
let n = 0;
|
|
41
|
+
for (const r of rows) {
|
|
42
|
+
if (this.matches(query.where, r, root)) {
|
|
43
|
+
n++;
|
|
44
|
+
if (n >= need)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const m = boundedCount(n, bound);
|
|
49
|
+
if (query.consumer === "count")
|
|
50
|
+
return { consumer: "count", count: m };
|
|
51
|
+
if (query.consumer === "exists")
|
|
52
|
+
return { consumer: "exists", exists: m > 0 };
|
|
53
|
+
return { consumer: "none", none: m === 0 };
|
|
46
54
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
// first/single over an unordered, non-distinct set need only the rows up to
|
|
56
|
+
// the bound: offset + 1 (first) / offset + 2 (single, to detect a second).
|
|
57
|
+
const want = query.consumer === "first" ? 1 : query.consumer === "single" ? 2 : Infinity;
|
|
58
|
+
const cap = !query.orderBy && !query.distinct && want !== Infinity
|
|
59
|
+
? bound.offset + Math.min(want, bound.limit ?? want)
|
|
60
|
+
: Infinity;
|
|
50
61
|
let kept = [];
|
|
51
62
|
for (const r of rows) {
|
|
52
63
|
const s = { row: r, parent: root, bindings, lifts: {} };
|
|
@@ -58,8 +69,29 @@ export class InMemoryEngine {
|
|
|
58
69
|
}
|
|
59
70
|
this.sortScopes(kept, query.orderBy);
|
|
60
71
|
if (query.distinct)
|
|
61
|
-
kept = this.dedupByProjection(kept, query
|
|
62
|
-
|
|
72
|
+
kept = this.dedupByProjection(kept, query);
|
|
73
|
+
kept = sliceBound(kept, bound);
|
|
74
|
+
return this.shape(query.consumer, kept, query);
|
|
75
|
+
}
|
|
76
|
+
// Evaluate a block's `limit`/`offset`. The bound is part of the block, so it
|
|
77
|
+
// is read in a row-less scope INSIDE it: a bare name is absent (there is no
|
|
78
|
+
// current item yet), `^name` is the enclosing row — exactly as in the block's
|
|
79
|
+
// body — and literals/bindings are themselves. For a top-level query `scope`
|
|
80
|
+
// is the root. Each must be a non-negative integer.
|
|
81
|
+
boundOf(b, enclosing) {
|
|
82
|
+
if (!b.limit && !b.offset)
|
|
83
|
+
return UNBOUNDED;
|
|
84
|
+
const scope = enclosing.parent === null ? enclosing : { row: undefined, parent: enclosing, bindings: enclosing.bindings };
|
|
85
|
+
const read = (e, word) => {
|
|
86
|
+
if (!e)
|
|
87
|
+
return null;
|
|
88
|
+
const v = this.evalExpr(e, scope);
|
|
89
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
|
|
90
|
+
throw new OqxError(`${word} must be a non-negative integer (got ${JSON.stringify(v ?? null)})`, "eval");
|
|
91
|
+
}
|
|
92
|
+
return v;
|
|
93
|
+
};
|
|
94
|
+
return { offset: read(b.offset, "offset") ?? 0, limit: read(b.limit, "limit") };
|
|
63
95
|
}
|
|
64
96
|
// A where match that needs no lift capture (exists/count fast paths).
|
|
65
97
|
matches(where, row, parent) {
|
|
@@ -74,7 +106,7 @@ export class InMemoryEngine {
|
|
|
74
106
|
return { row, parent, bindings: parent.bindings };
|
|
75
107
|
}
|
|
76
108
|
// ---- follow ---------------------------------------------------------------
|
|
77
|
-
runFollow(query, rows, root) {
|
|
109
|
+
runFollow(query, rows, root, bound) {
|
|
78
110
|
const { seed, post } = query.where ? partitionRecur(query.where) : { seed: null, post: null };
|
|
79
111
|
const seeds = seed ? rows.filter((r) => this.matches(seed, r, root)) : rows;
|
|
80
112
|
const occ = this.followWalk(seeds, query.follow, root);
|
|
@@ -83,19 +115,20 @@ export class InMemoryEngine {
|
|
|
83
115
|
scopes = scopes.filter((s) => this.evalWhere(post, s));
|
|
84
116
|
this.sortScopes(scopes, query.orderBy);
|
|
85
117
|
if (query.distinct)
|
|
86
|
-
scopes = this.dedupByProjection(scopes, query
|
|
87
|
-
|
|
118
|
+
scopes = this.dedupByProjection(scopes, query);
|
|
119
|
+
scopes = sliceBound(scopes, bound);
|
|
120
|
+
return this.shape(query.consumer, scopes, query);
|
|
88
121
|
}
|
|
89
122
|
// Dedup scopes by their PROJECTED value (`distinct`): keep the first scope per
|
|
90
123
|
// distinct projection, preserving order. An empty projection dedups by row
|
|
91
124
|
// identity (so `count distinct { }` counts distinct rows).
|
|
92
|
-
dedupByProjection(scopes,
|
|
125
|
+
dedupByProjection(scopes, proj) {
|
|
93
126
|
const seen = new Set();
|
|
94
127
|
const out = [];
|
|
95
128
|
for (const s of scopes) {
|
|
96
|
-
const key = select.length === 0
|
|
129
|
+
const key = proj.select.length === 0
|
|
97
130
|
? `i:${String(this.ctx.identity(s.row))}`
|
|
98
|
-
: `p:${stableStringify(this.projectRow(
|
|
131
|
+
: `p:${stableStringify(this.projectRow(proj, s))}`;
|
|
99
132
|
if (seen.has(key))
|
|
100
133
|
continue;
|
|
101
134
|
seen.add(key);
|
|
@@ -176,27 +209,35 @@ export class InMemoryEngine {
|
|
|
176
209
|
}));
|
|
177
210
|
}
|
|
178
211
|
// ---- consumer shaping -----------------------------------------------------
|
|
179
|
-
shape(consumer, scopes,
|
|
212
|
+
shape(consumer, scopes, proj) {
|
|
180
213
|
switch (consumer) {
|
|
181
214
|
case "exists": return { consumer, exists: scopes.length > 0 };
|
|
215
|
+
case "none": return { consumer, none: scopes.length === 0 };
|
|
182
216
|
case "count": return { consumer, count: scopes.length };
|
|
183
|
-
case "collect": return { consumer, rows: scopes.map((s) => this.projectRow(
|
|
184
|
-
case "first": return { consumer, row: scopes.length > 0 ? this.projectRow(
|
|
217
|
+
case "collect": return { consumer, rows: scopes.map((s) => this.projectRow(proj, s)) };
|
|
218
|
+
case "first": return { consumer, row: scopes.length > 0 ? this.projectRow(proj, scopes[0]) : null };
|
|
185
219
|
case "single":
|
|
186
220
|
if (scopes.length > 1)
|
|
187
221
|
throw new OqxError(`single { … } matched ${scopes.length} rows; use first { … } for zero-or-one`, "eval");
|
|
188
|
-
return { consumer, row: scopes.length > 0 ? this.projectRow(
|
|
222
|
+
return { consumer, row: scopes.length > 0 ? this.projectRow(proj, scopes[0]) : null };
|
|
189
223
|
}
|
|
190
224
|
}
|
|
191
|
-
|
|
225
|
+
// The per-row result: the raw row (empty projection), the single item's value
|
|
226
|
+
// itself (`values` mode), or a `{ name: value }` record.
|
|
227
|
+
projectRow(proj, scope) {
|
|
228
|
+
const { select } = proj;
|
|
192
229
|
if (select.length === 0)
|
|
193
230
|
return scope.row;
|
|
231
|
+
if (proj.values)
|
|
232
|
+
return this.itemValue(select[0], scope);
|
|
194
233
|
const out = {};
|
|
195
|
-
for (const item of select)
|
|
196
|
-
out[item.name] =
|
|
197
|
-
}
|
|
234
|
+
for (const item of select)
|
|
235
|
+
out[item.name] = this.itemValue(item, scope);
|
|
198
236
|
return out;
|
|
199
237
|
}
|
|
238
|
+
itemValue(item, scope) {
|
|
239
|
+
return item.kind === "field" ? this.evalExpr(item.expr, scope) : this.evalCollectValue(item.op, scope);
|
|
240
|
+
}
|
|
200
241
|
// ---- where evaluation -----------------------------------------------------
|
|
201
242
|
evalWhere(w, scope) {
|
|
202
243
|
switch (w.kind) {
|
|
@@ -216,10 +257,15 @@ export class InMemoryEngine {
|
|
|
216
257
|
evalWhereOp(op, scope) {
|
|
217
258
|
if (op.sub.follow)
|
|
218
259
|
throw new OqxError("`follow` is only valid on a select-position collect { … }, not a where op", "eval");
|
|
219
|
-
|
|
220
|
-
|
|
260
|
+
const bound = this.boundOf(op.sub, scope);
|
|
261
|
+
if (op.op === "exists" || op.op === "none") {
|
|
262
|
+
// Unbounded: stop at the first match (dedup cannot change emptiness).
|
|
263
|
+
// Bounded: the offset/limit decide emptiness, so materialize the set.
|
|
264
|
+
const any = bound === UNBOUNDED ? this.anyMatch(op, scope) : this.opRows(op, scope, bound).length > 0;
|
|
265
|
+
return op.op === "exists" ? any : !any;
|
|
266
|
+
}
|
|
221
267
|
if (op.op === "collect") {
|
|
222
|
-
const matched = this.
|
|
268
|
+
const matched = this.opRows(op, scope, bound);
|
|
223
269
|
for (const item of op.sub.select) {
|
|
224
270
|
if (item.kind !== "field")
|
|
225
271
|
continue;
|
|
@@ -237,13 +283,18 @@ export class InMemoryEngine {
|
|
|
237
283
|
return matched.length > 0;
|
|
238
284
|
}
|
|
239
285
|
if (op.op === "count") {
|
|
240
|
-
|
|
241
|
-
if (op.distinct)
|
|
242
|
-
rows = this.dedupByProjection(rows, op.sub.select);
|
|
243
|
-
const n = rows.length;
|
|
286
|
+
const n = this.opRows(op, scope, bound).length;
|
|
244
287
|
return op.countCmp ? compareCount(n, op.countCmp) : n > 0;
|
|
245
288
|
}
|
|
246
|
-
return this.
|
|
289
|
+
return this.opRows(op, scope, bound).length > 0;
|
|
290
|
+
}
|
|
291
|
+
// The rows a consumer op reduces: matched → ordered → distinct → bounded.
|
|
292
|
+
opRows(op, scope, bound) {
|
|
293
|
+
let scopes = this.matchRows(op, scope);
|
|
294
|
+
this.sortScopes(scopes, op.sub.orderBy);
|
|
295
|
+
if (op.distinct)
|
|
296
|
+
scopes = this.dedupByProjection(scopes, op.sub);
|
|
297
|
+
return sliceBound(scopes, bound);
|
|
247
298
|
}
|
|
248
299
|
// Short-circuiting existence check over a consumer receiver.
|
|
249
300
|
anyMatch(op, scope) {
|
|
@@ -269,6 +320,7 @@ export class InMemoryEngine {
|
|
|
269
320
|
// A select-position collect/first/single, optionally recursive via `follow`.
|
|
270
321
|
evalCollectValue(op, scope) {
|
|
271
322
|
const sub = op.sub;
|
|
323
|
+
const bound = this.boundOf(sub, scope);
|
|
272
324
|
let scopes;
|
|
273
325
|
if (sub.follow) {
|
|
274
326
|
let rows = this.rowsOf(this.evalExpr(op.receiver, scope));
|
|
@@ -276,20 +328,21 @@ export class InMemoryEngine {
|
|
|
276
328
|
rows = rows.flatMap((r) => this.rowsOf(this.evalExpr(proj, this.child(r, scope))));
|
|
277
329
|
const seeds = sub.where ? rows.filter((r) => this.evalWhere(sub.where, { row: r, parent: scope, bindings: scope.bindings, lifts: {} })) : rows;
|
|
278
330
|
scopes = this.followWalk(seeds, sub.follow, scope).map((o) => ({ row: o.row, parent: scope, bindings: scope.bindings, meta: o.meta }));
|
|
331
|
+
this.sortScopes(scopes, sub.orderBy);
|
|
332
|
+
if (op.distinct)
|
|
333
|
+
scopes = this.dedupByProjection(scopes, sub);
|
|
334
|
+
scopes = sliceBound(scopes, bound);
|
|
279
335
|
}
|
|
280
336
|
else {
|
|
281
|
-
scopes = this.
|
|
337
|
+
scopes = this.opRows(op, scope, bound);
|
|
282
338
|
}
|
|
283
|
-
this.sortScopes(scopes, sub.orderBy);
|
|
284
|
-
if (op.distinct)
|
|
285
|
-
scopes = this.dedupByProjection(scopes, sub.select);
|
|
286
339
|
switch (op.op) {
|
|
287
|
-
case "collect": return scopes.map((s) => this.projectRow(sub
|
|
288
|
-
case "first": return scopes.length > 0 ? this.projectRow(sub
|
|
340
|
+
case "collect": return scopes.map((s) => this.projectRow(sub, s));
|
|
341
|
+
case "first": return scopes.length > 0 ? this.projectRow(sub, scopes[0]) : null;
|
|
289
342
|
case "single":
|
|
290
343
|
if (scopes.length > 1)
|
|
291
344
|
throw new OqxError(`single { … } for '${describeReceiver(op.receiver)}' matched ${scopes.length} rows`, "eval");
|
|
292
|
-
return scopes.length > 0 ? this.projectRow(sub
|
|
345
|
+
return scopes.length > 0 ? this.projectRow(sub, scopes[0]) : null;
|
|
293
346
|
default: throw new OqxError(`${op.op} { … } is not valid in select position`, "eval");
|
|
294
347
|
}
|
|
295
348
|
}
|
|
@@ -362,9 +415,11 @@ export class InMemoryEngine {
|
|
|
362
415
|
}
|
|
363
416
|
}
|
|
364
417
|
// Resolve a name against ONE scope — never its ancestors. A scope provides,
|
|
365
|
-
// in order: the
|
|
366
|
-
//
|
|
367
|
-
//
|
|
418
|
+
// in order: `$value` (the scope's row itself — the current item, whatever its
|
|
419
|
+
// type, so scalar collections are queryable; absent at the root, which has no
|
|
420
|
+
// row); the recursion intrinsics (`$depth`, …) when it is a follow occurrence;
|
|
421
|
+
// values lifted into it by `^name:` items; then either the row's own property
|
|
422
|
+
// or, for the root scope (no row), the context's named roots.
|
|
368
423
|
//
|
|
369
424
|
// A name the scope lacks is simply absent (undefined). It does NOT fall
|
|
370
425
|
// through to an enclosing scope, so a query's meaning never depends on which
|
|
@@ -373,6 +428,8 @@ export class InMemoryEngine {
|
|
|
373
428
|
// always spelled explicitly as `^name`. Present-but-falsy values (null, false,
|
|
374
429
|
// 0, "") need no special case — there is no "absent, so look outward" rule.
|
|
375
430
|
resolveIn(name, scope) {
|
|
431
|
+
if (name === "$value")
|
|
432
|
+
return scope.parent === null ? undefined : scope.row;
|
|
376
433
|
if (RECUR.has(name))
|
|
377
434
|
return scope.meta ? scope.meta[name] : undefined;
|
|
378
435
|
if (scope.lifts && name in scope.lifts)
|
|
@@ -417,6 +474,16 @@ function stableStringify(v) {
|
|
|
417
474
|
function compareCount(n, cmp) {
|
|
418
475
|
return relate(cmp.op, n, cmp.value);
|
|
419
476
|
}
|
|
477
|
+
// Apply a bound to an ordered row set / to a match count.
|
|
478
|
+
function sliceBound(rows, b) {
|
|
479
|
+
if (b === UNBOUNDED)
|
|
480
|
+
return rows;
|
|
481
|
+
return rows.slice(b.offset, b.limit == null ? undefined : b.offset + b.limit);
|
|
482
|
+
}
|
|
483
|
+
function boundedCount(n, b) {
|
|
484
|
+
const rest = Math.max(0, n - b.offset);
|
|
485
|
+
return b.limit == null ? rest : Math.min(rest, b.limit);
|
|
486
|
+
}
|
|
420
487
|
// Order `&&` conjuncts so cheap scalar leaves run before consumer-op leaves.
|
|
421
488
|
function orderByCost(parts) {
|
|
422
489
|
return [...parts].sort((a, b) => whereCost(a) - whereCost(b));
|