@omgbase/oqx 0.8.0 → 0.10.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 +61 -7
- 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 +14 -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 -43
- 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 +48 -9
- 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 +1 -1
- package/src/adapters/sqlite.ts +5 -3
- package/src/ast.ts +14 -2
- package/src/engine.ts +115 -37
- package/src/index.ts +3 -2
- package/src/parser.ts +42 -9
- package/src/semantics.ts +42 -0
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
|
|
|
@@ -257,7 +280,7 @@ like a row property.
|
|
|
257
280
|
|
|
258
281
|
Methods on a value: `contains`, `startsWith`, `endsWith`, `matches` (regex),
|
|
259
282
|
`size`, `lower`, `upper`. Free functions: `list(x)` (coerce to an array), `size(x)`,
|
|
260
|
-
`has(x)
|
|
283
|
+
`has(x)`, `range(s)` (§3), `entries(x)` (§2).
|
|
261
284
|
|
|
262
285
|
```js
|
|
263
286
|
oqx`name from ${people} where title.startsWith("Eng")`; // → Bob
|
|
@@ -266,12 +289,13 @@ oqx`name from ${people} where title.lower() == "director"`; // → Alice
|
|
|
266
289
|
|
|
267
290
|
### 5. Consumers
|
|
268
291
|
|
|
269
|
-
A consumer shapes a result set. There are
|
|
292
|
+
A consumer shapes a result set. There are six:
|
|
270
293
|
|
|
271
294
|
| Consumer | Returns |
|
|
272
295
|
| --------- | ------------------------------ |
|
|
273
296
|
| `collect` | an array (the default) |
|
|
274
|
-
| `exists` | a boolean
|
|
297
|
+
| `exists` | a boolean — one or more rows |
|
|
298
|
+
| `none` | a boolean — zero rows (the complement of `exists`) |
|
|
275
299
|
| `count` | a number |
|
|
276
300
|
| `first` | one record, or `null` |
|
|
277
301
|
| `single` | one record, or `null`; throws if more than one matches |
|
|
@@ -301,8 +325,14 @@ cardinality:
|
|
|
301
325
|
```js
|
|
302
326
|
oqx`name from ${people} where jobs exists { !end }`; // has a current job → Bob, Carol
|
|
303
327
|
oqx`name from ${people} where jobs count {} >= 2`; // ≥2 jobs → Bob, Alice
|
|
328
|
+
oqx`name from ${people} where jobs none { where end }`; // no past job → Carol
|
|
304
329
|
```
|
|
305
330
|
|
|
331
|
+
`none { … }` is exactly `!… exists { … }`, kept as its own word because the
|
|
332
|
+
cardinality is the point. It is also how you say "every": *all* members active is
|
|
333
|
+
`members none { where !active }` — there is deliberately no `all { … }`, whose
|
|
334
|
+
block would have to mean something different from every other consumer's.
|
|
335
|
+
|
|
306
336
|
In a projection, `collect` yields a nested array; `first` / `single` yield one
|
|
307
337
|
nested record:
|
|
308
338
|
|
|
@@ -383,6 +413,26 @@ oqx`name from ${people} where city == "NYC" order by age desc`;
|
|
|
383
413
|
// [{ name: "Bob" }, { name: "Carol" }]
|
|
384
414
|
```
|
|
385
415
|
|
|
416
|
+
### 8b. Bounding: `limit` / `offset`
|
|
417
|
+
|
|
418
|
+
`limit N` and `offset N` bound the row set **after** `where`, `order by`, and
|
|
419
|
+
`distinct`, and **before** the consumer reduces it — so they mean the same thing
|
|
420
|
+
under every consumer: `count { … limit 5 }` is at most 5, `first { … offset 1 }`
|
|
421
|
+
is the second row, `exists { offset 2 }` asks for a third. They work at the top
|
|
422
|
+
level and inside any block, and `N` may be a literal, a `${…}` binding, or an
|
|
423
|
+
outer reference — `limit ^n` reads the enclosing row's `n`, as `^` does everywhere
|
|
424
|
+
inside `{ … }`; it must be a non-negative integer.
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
oqx`name values from ${people} order by age desc limit 2`; // ["Alice", "Bob"]
|
|
428
|
+
oqx`name values from ${people} order by age desc offset 1 limit 1`; // ["Bob"]
|
|
429
|
+
oqx`name, latest: jobs collect { employer values order by start desc limit 1 } from ${people}`;
|
|
430
|
+
oqx`name from ${people} where jobs exists { offset 1 }`; // has a second job
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
A storage adapter that pushes the whole query may translate them to SQL
|
|
434
|
+
`LIMIT`/`OFFSET`; the shipped `SqliteTable` leaves them to the residual.
|
|
435
|
+
|
|
386
436
|
### 9. Recursion: `follow`
|
|
387
437
|
|
|
388
438
|
`follow <relation>` turns a query into a bounded recursive traversal: the `where`
|
|
@@ -435,17 +485,21 @@ your relation returns fresh objects rather than shared references.
|
|
|
435
485
|
name, alias: expr, nested: rel collect { … } projection (select optional)
|
|
436
486
|
from ${source} source collection
|
|
437
487
|
where a == b && rel exists { … } || !c predicate tree + nested ops
|
|
488
|
+
where rel none { … } zero rows (≡ !rel exists { … }; "all" = none over the complement)
|
|
438
489
|
where x in lo..hi / lo...hi / ..hi / lo.. range membership (incl. / excl. / open-ended)
|
|
439
490
|
where x in range(field) coerce a string field to a range, then test coverage
|
|
440
491
|
name the CURRENT row's field only (never climbs)
|
|
441
492
|
$value the current item itself (a scalar row, or the whole object)
|
|
493
|
+
from entries(obj) … $key / $value a record's properties as a collection (key + value; bare names read the value)
|
|
442
494
|
<expr> values scalar projection: the value, not a { name: value } record
|
|
443
495
|
^name / ^^name read an enclosing row's field (exactly N scopes out); ^$value = the enclosing row
|
|
444
496
|
^name: expr / ^^name: expr lift/export a value N scopes out (flatten-append)
|
|
445
497
|
^rel collect { … } / ^^root exists { … } nested consumer over an enclosing row's relation / a named root
|
|
498
|
+
entries(rel) exists { … } a free-function call may be a receiver
|
|
446
499
|
order by expr desc, expr2 ordering
|
|
500
|
+
limit n / offset n bound the row set (after where/order/distinct, before the consumer)
|
|
447
501
|
follow rel { where … frontier … depth n by … } recursion ($depth/$stop/$leaf/$frontier)
|
|
448
|
-
${source} <collect|exists|count|first|single> { … } whole-query consumer
|
|
502
|
+
${source} <collect|exists|none|count|first|single> { … } whole-query consumer
|
|
449
503
|
```
|
|
450
504
|
|
|
451
505
|
## Data context: string queries and named roots
|
|
@@ -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. */
|
|
@@ -106,6 +107,14 @@ export interface Subquery {
|
|
|
106
107
|
* row's result itself rather than being wrapped in a `{ name: value }` record,
|
|
107
108
|
* so `name values` yields `["Bob", …]` and `$value values` yields the rows. */
|
|
108
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;
|
|
109
118
|
}
|
|
110
119
|
/** The where-clause boolean tree: OQX owns &&/||/!/grouping so consumer ops
|
|
111
120
|
* (invisible to the scalar evaluator) compose with scalar predicates. */
|
|
@@ -135,5 +144,8 @@ export interface Query {
|
|
|
135
144
|
distinct?: boolean;
|
|
136
145
|
/** `values` — scalar projection mode (see Subquery). */
|
|
137
146
|
values?: boolean;
|
|
147
|
+
/** `limit N` / `offset N` (see Subquery); evaluated at the root scope. */
|
|
148
|
+
limit?: Expr;
|
|
149
|
+
offset?: Expr;
|
|
138
150
|
}
|
|
139
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,9 +28,11 @@ 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;
|
|
35
|
+
private enter;
|
|
31
36
|
private runFollow;
|
|
32
37
|
private dedupByProjection;
|
|
33
38
|
private followWalk;
|
|
@@ -36,6 +41,7 @@ export declare class InMemoryEngine implements Engine {
|
|
|
36
41
|
private itemValue;
|
|
37
42
|
private evalWhere;
|
|
38
43
|
private evalWhereOp;
|
|
44
|
+
private opRows;
|
|
39
45
|
private anyMatch;
|
|
40
46
|
private iterMatchRows;
|
|
41
47
|
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;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,9 +13,11 @@
|
|
|
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;
|
|
20
|
+
const UNBOUNDED = { offset: 0, limit: null };
|
|
19
21
|
export class InMemoryEngine {
|
|
20
22
|
ctx;
|
|
21
23
|
constructor(context = new DefaultContext()) {
|
|
@@ -27,29 +29,39 @@ export class InMemoryEngine {
|
|
|
27
29
|
for (const proj of query.from) {
|
|
28
30
|
rows = rows.flatMap((r) => this.rowsOf(this.evalExpr(proj, this.child(r, root))));
|
|
29
31
|
}
|
|
32
|
+
const bound = this.boundOf(query, root);
|
|
30
33
|
if (query.follow)
|
|
31
|
-
return this.runFollow(query, rows, root);
|
|
34
|
+
return this.runFollow(query, rows, root, bound);
|
|
32
35
|
// 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
|
-
|
|
36
|
+
// materialize + dedup by projection before reducing). Counting never
|
|
37
|
+
// materializes rows; exists/none stop as soon as the bound is known to be
|
|
38
|
+
// non-empty (the (offset+1)th match — or the first, when unbounded).
|
|
39
|
+
if (!query.distinct && (query.consumer === "exists" || query.consumer === "none" || query.consumer === "count")) {
|
|
40
|
+
const need = query.consumer === "count" ? Infinity : bound.offset + 1;
|
|
41
|
+
let n = 0;
|
|
42
|
+
for (const r of rows) {
|
|
43
|
+
if (this.matches(query.where, r, root)) {
|
|
44
|
+
n++;
|
|
45
|
+
if (n >= need)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const m = boundedCount(n, bound);
|
|
50
|
+
if (query.consumer === "count")
|
|
51
|
+
return { consumer: "count", count: m };
|
|
52
|
+
if (query.consumer === "exists")
|
|
53
|
+
return { consumer: "exists", exists: m > 0 };
|
|
54
|
+
return { consumer: "none", none: m === 0 };
|
|
46
55
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
// first/single over an unordered, non-distinct set need only the rows up to
|
|
57
|
+
// the bound: offset + 1 (first) / offset + 2 (single, to detect a second).
|
|
58
|
+
const want = query.consumer === "first" ? 1 : query.consumer === "single" ? 2 : Infinity;
|
|
59
|
+
const cap = !query.orderBy && !query.distinct && want !== Infinity
|
|
60
|
+
? bound.offset + Math.min(want, bound.limit ?? want)
|
|
61
|
+
: Infinity;
|
|
50
62
|
let kept = [];
|
|
51
63
|
for (const r of rows) {
|
|
52
|
-
const s =
|
|
64
|
+
const s = this.enter(r, root, { lifts: {} });
|
|
53
65
|
if (!query.where || this.evalWhere(query.where, s)) {
|
|
54
66
|
kept.push(s);
|
|
55
67
|
if (kept.length >= cap)
|
|
@@ -59,31 +71,70 @@ export class InMemoryEngine {
|
|
|
59
71
|
this.sortScopes(kept, query.orderBy);
|
|
60
72
|
if (query.distinct)
|
|
61
73
|
kept = this.dedupByProjection(kept, query);
|
|
74
|
+
kept = sliceBound(kept, bound);
|
|
62
75
|
return this.shape(query.consumer, kept, query);
|
|
63
76
|
}
|
|
77
|
+
// Evaluate a block's `limit`/`offset`. The bound is part of the block, so it
|
|
78
|
+
// is read in a row-less scope INSIDE it: a bare name is absent (there is no
|
|
79
|
+
// current item yet), `^name` is the enclosing row — exactly as in the block's
|
|
80
|
+
// body — and literals/bindings are themselves. For a top-level query `scope`
|
|
81
|
+
// is the root. Each must be a non-negative integer.
|
|
82
|
+
boundOf(b, enclosing) {
|
|
83
|
+
if (!b.limit && !b.offset)
|
|
84
|
+
return UNBOUNDED;
|
|
85
|
+
const scope = enclosing.parent === null ? enclosing : { row: undefined, parent: enclosing, bindings: enclosing.bindings };
|
|
86
|
+
const read = (e, word) => {
|
|
87
|
+
if (!e)
|
|
88
|
+
return null;
|
|
89
|
+
const v = this.evalExpr(e, scope);
|
|
90
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
|
|
91
|
+
throw new OqxError(`${word} must be a non-negative integer (got ${JSON.stringify(v ?? null)})`, "eval");
|
|
92
|
+
}
|
|
93
|
+
return v;
|
|
94
|
+
};
|
|
95
|
+
return { offset: read(b.offset, "offset") ?? 0, limit: read(b.limit, "limit") };
|
|
96
|
+
}
|
|
64
97
|
// A where match that needs no lift capture (exists/count fast paths).
|
|
65
98
|
matches(where, row, parent) {
|
|
66
99
|
if (!where)
|
|
67
100
|
return true;
|
|
68
|
-
return this.evalWhere(where,
|
|
101
|
+
return this.evalWhere(where, this.enter(row, parent));
|
|
69
102
|
}
|
|
70
103
|
rowsOf(v) {
|
|
71
104
|
return Array.from(this.ctx.toRows(v));
|
|
72
105
|
}
|
|
73
106
|
child(row, parent) {
|
|
74
|
-
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;
|
|
75
125
|
}
|
|
76
126
|
// ---- follow ---------------------------------------------------------------
|
|
77
|
-
runFollow(query, rows, root) {
|
|
127
|
+
runFollow(query, rows, root, bound) {
|
|
78
128
|
const { seed, post } = query.where ? partitionRecur(query.where) : { seed: null, post: null };
|
|
79
129
|
const seeds = seed ? rows.filter((r) => this.matches(seed, r, root)) : rows;
|
|
80
130
|
const occ = this.followWalk(seeds, query.follow, root);
|
|
81
|
-
let scopes = occ.map((o) => (
|
|
131
|
+
let scopes = occ.map((o) => this.enter(o.row, root, { lifts: {}, meta: o.meta }));
|
|
82
132
|
if (post)
|
|
83
133
|
scopes = scopes.filter((s) => this.evalWhere(post, s));
|
|
84
134
|
this.sortScopes(scopes, query.orderBy);
|
|
85
135
|
if (query.distinct)
|
|
86
136
|
scopes = this.dedupByProjection(scopes, query);
|
|
137
|
+
scopes = sliceBound(scopes, bound);
|
|
87
138
|
return this.shape(query.consumer, scopes, query);
|
|
88
139
|
}
|
|
89
140
|
// Dedup scopes by their PROJECTED value (`distinct`): keep the first scope per
|
|
@@ -118,7 +169,7 @@ export class InMemoryEngine {
|
|
|
118
169
|
// `ctx.identity(row)`.
|
|
119
170
|
followWalk(seedRows, follow, parent) {
|
|
120
171
|
const cap = follow.depth ?? HARD_DEPTH_CAP;
|
|
121
|
-
const scopeFor = (row) => (
|
|
172
|
+
const scopeFor = (row) => this.enter(row, parent);
|
|
122
173
|
const keyOf = (row) => String(follow.by ? this.evalExpr(follow.by, scopeFor(row)) : this.ctx.identity(row));
|
|
123
174
|
const succOf = (row) => {
|
|
124
175
|
const raw = this.rowsOf(this.evalExpr(follow.receiver, scopeFor(row)));
|
|
@@ -179,6 +230,7 @@ export class InMemoryEngine {
|
|
|
179
230
|
shape(consumer, scopes, proj) {
|
|
180
231
|
switch (consumer) {
|
|
181
232
|
case "exists": return { consumer, exists: scopes.length > 0 };
|
|
233
|
+
case "none": return { consumer, none: scopes.length === 0 };
|
|
182
234
|
case "count": return { consumer, count: scopes.length };
|
|
183
235
|
case "collect": return { consumer, rows: scopes.map((s) => this.projectRow(proj, s)) };
|
|
184
236
|
case "first": return { consumer, row: scopes.length > 0 ? this.projectRow(proj, scopes[0]) : null };
|
|
@@ -223,10 +275,15 @@ export class InMemoryEngine {
|
|
|
223
275
|
evalWhereOp(op, scope) {
|
|
224
276
|
if (op.sub.follow)
|
|
225
277
|
throw new OqxError("`follow` is only valid on a select-position collect { … }, not a where op", "eval");
|
|
226
|
-
|
|
227
|
-
|
|
278
|
+
const bound = this.boundOf(op.sub, scope);
|
|
279
|
+
if (op.op === "exists" || op.op === "none") {
|
|
280
|
+
// Unbounded: stop at the first match (dedup cannot change emptiness).
|
|
281
|
+
// Bounded: the offset/limit decide emptiness, so materialize the set.
|
|
282
|
+
const any = bound === UNBOUNDED ? this.anyMatch(op, scope) : this.opRows(op, scope, bound).length > 0;
|
|
283
|
+
return op.op === "exists" ? any : !any;
|
|
284
|
+
}
|
|
228
285
|
if (op.op === "collect") {
|
|
229
|
-
const matched = this.
|
|
286
|
+
const matched = this.opRows(op, scope, bound);
|
|
230
287
|
for (const item of op.sub.select) {
|
|
231
288
|
if (item.kind !== "field")
|
|
232
289
|
continue;
|
|
@@ -244,13 +301,18 @@ export class InMemoryEngine {
|
|
|
244
301
|
return matched.length > 0;
|
|
245
302
|
}
|
|
246
303
|
if (op.op === "count") {
|
|
247
|
-
|
|
248
|
-
if (op.distinct)
|
|
249
|
-
rows = this.dedupByProjection(rows, op.sub);
|
|
250
|
-
const n = rows.length;
|
|
304
|
+
const n = this.opRows(op, scope, bound).length;
|
|
251
305
|
return op.countCmp ? compareCount(n, op.countCmp) : n > 0;
|
|
252
306
|
}
|
|
253
|
-
return this.
|
|
307
|
+
return this.opRows(op, scope, bound).length > 0;
|
|
308
|
+
}
|
|
309
|
+
// The rows a consumer op reduces: matched → ordered → distinct → bounded.
|
|
310
|
+
opRows(op, scope, bound) {
|
|
311
|
+
let scopes = this.matchRows(op, scope);
|
|
312
|
+
this.sortScopes(scopes, op.sub.orderBy);
|
|
313
|
+
if (op.distinct)
|
|
314
|
+
scopes = this.dedupByProjection(scopes, op.sub);
|
|
315
|
+
return sliceBound(scopes, bound);
|
|
254
316
|
}
|
|
255
317
|
// Short-circuiting existence check over a consumer receiver.
|
|
256
318
|
anyMatch(op, scope) {
|
|
@@ -265,7 +327,7 @@ export class InMemoryEngine {
|
|
|
265
327
|
for (const proj of op.sub.from)
|
|
266
328
|
rows = rows.flatMap((r) => this.rowsOf(this.evalExpr(proj, this.child(r, scope))));
|
|
267
329
|
for (const r of rows) {
|
|
268
|
-
const s =
|
|
330
|
+
const s = this.enter(r, scope, { lifts: {} });
|
|
269
331
|
if (!op.sub.where || this.evalWhere(op.sub.where, s))
|
|
270
332
|
yield s;
|
|
271
333
|
}
|
|
@@ -276,20 +338,22 @@ export class InMemoryEngine {
|
|
|
276
338
|
// A select-position collect/first/single, optionally recursive via `follow`.
|
|
277
339
|
evalCollectValue(op, scope) {
|
|
278
340
|
const sub = op.sub;
|
|
341
|
+
const bound = this.boundOf(sub, scope);
|
|
279
342
|
let scopes;
|
|
280
343
|
if (sub.follow) {
|
|
281
344
|
let rows = this.rowsOf(this.evalExpr(op.receiver, scope));
|
|
282
345
|
for (const proj of sub.from)
|
|
283
346
|
rows = rows.flatMap((r) => this.rowsOf(this.evalExpr(proj, this.child(r, scope))));
|
|
284
|
-
const seeds = sub.where ? rows.filter((r) => this.evalWhere(sub.where,
|
|
285
|
-
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 }));
|
|
349
|
+
this.sortScopes(scopes, sub.orderBy);
|
|
350
|
+
if (op.distinct)
|
|
351
|
+
scopes = this.dedupByProjection(scopes, sub);
|
|
352
|
+
scopes = sliceBound(scopes, bound);
|
|
286
353
|
}
|
|
287
354
|
else {
|
|
288
|
-
scopes = this.
|
|
355
|
+
scopes = this.opRows(op, scope, bound);
|
|
289
356
|
}
|
|
290
|
-
this.sortScopes(scopes, sub.orderBy);
|
|
291
|
-
if (op.distinct)
|
|
292
|
-
scopes = this.dedupByProjection(scopes, sub);
|
|
293
357
|
switch (op.op) {
|
|
294
358
|
case "collect": return scopes.map((s) => this.projectRow(sub, s));
|
|
295
359
|
case "first": return scopes.length > 0 ? this.projectRow(sub, scopes[0]) : null;
|
|
@@ -371,9 +435,10 @@ export class InMemoryEngine {
|
|
|
371
435
|
// Resolve a name against ONE scope — never its ancestors. A scope provides,
|
|
372
436
|
// in order: `$value` (the scope's row itself — the current item, whatever its
|
|
373
437
|
// type, so scalar collections are queryable; absent at the root, which has no
|
|
374
|
-
// row); the
|
|
375
|
-
//
|
|
376
|
-
//
|
|
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.
|
|
377
442
|
//
|
|
378
443
|
// A name the scope lacks is simply absent (undefined). It does NOT fall
|
|
379
444
|
// through to an enclosing scope, so a query's meaning never depends on which
|
|
@@ -384,7 +449,7 @@ export class InMemoryEngine {
|
|
|
384
449
|
resolveIn(name, scope) {
|
|
385
450
|
if (name === "$value")
|
|
386
451
|
return scope.parent === null ? undefined : scope.row;
|
|
387
|
-
if (RECUR.has(name))
|
|
452
|
+
if (name === KEY || RECUR.has(name))
|
|
388
453
|
return scope.meta ? scope.meta[name] : undefined;
|
|
389
454
|
if (scope.lifts && name in scope.lifts)
|
|
390
455
|
return scope.lifts[name];
|
|
@@ -428,6 +493,16 @@ function stableStringify(v) {
|
|
|
428
493
|
function compareCount(n, cmp) {
|
|
429
494
|
return relate(cmp.op, n, cmp.value);
|
|
430
495
|
}
|
|
496
|
+
// Apply a bound to an ordered row set / to a match count.
|
|
497
|
+
function sliceBound(rows, b) {
|
|
498
|
+
if (b === UNBOUNDED)
|
|
499
|
+
return rows;
|
|
500
|
+
return rows.slice(b.offset, b.limit == null ? undefined : b.offset + b.limit);
|
|
501
|
+
}
|
|
502
|
+
function boundedCount(n, b) {
|
|
503
|
+
const rest = Math.max(0, n - b.offset);
|
|
504
|
+
return b.limit == null ? rest : Math.min(rest, b.limit);
|
|
505
|
+
}
|
|
431
506
|
// Order `&&` conjuncts so cheap scalar leaves run before consumer-op leaves.
|
|
432
507
|
function orderByCost(parts) {
|
|
433
508
|
return [...parts].sort((a, b) => whereCost(a) - whereCost(b));
|