@omgbase/oqx 0.15.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +308 -70
- package/dist/ast.d.ts +97 -27
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +21 -6
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +43 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +58 -0
- package/dist/build.js.map +1 -0
- package/dist/context.d.ts +30 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -10
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +13 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +281 -100
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -13
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +15 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +104 -51
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts +3 -2
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +19 -18
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +2 -2
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +3 -3
- package/dist/optimize/ir.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +782 -335
- package/dist/parser.js.map +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +4 -2
- package/dist/plan.js.map +1 -1
- package/dist/print.d.ts +17 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +266 -0
- package/dist/print.js.map +1 -0
- package/dist/resolve.d.ts +23 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +333 -0
- package/dist/resolve.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/walk.d.ts +56 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +157 -0
- package/dist/walk.js.map +1 -0
- package/package.json +1 -1
- package/src/ast.ts +126 -40
- package/src/build.ts +73 -0
- package/src/context.ts +36 -12
- package/src/engine.ts +285 -110
- package/src/errors.ts +3 -2
- package/src/index.ts +40 -14
- package/src/lexer.ts +102 -51
- package/src/optimize/analysis.ts +17 -15
- package/src/optimize/ir.ts +4 -4
- package/src/parser.ts +721 -289
- package/src/plan.ts +3 -2
- package/src/print.ts +276 -0
- package/src/resolve.ts +309 -0
- package/src/version.ts +4 -0
- package/src/walk.ts +201 -0
package/src/ast.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
// OQX
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// The OQX AST — a first-class, language-level contract (spec/oqx/AST.md, since
|
|
2
|
+
// language 0.16). The parser produces it, the interpreter walks it, and tools
|
|
3
|
+
// reflect on it without re-parsing: every node carries `kind` and `span`, every
|
|
4
|
+
// optional field is materialized (`null` / `false`), and the in-memory objects
|
|
5
|
+
// ARE the JSON shape both implementations serialize (plain data, no parent
|
|
6
|
+
// pointers, no methods). There is no separate lowering/IR pass: a "relation" is
|
|
7
|
+
// just an expression evaluated against the current row.
|
|
8
|
+
//
|
|
9
|
+
// `where` keeps its surface form: a `select` alias referenced in `where` stays a
|
|
10
|
+
// bare identifier here; `resolveAliases` (resolve.ts) performs the substitution
|
|
11
|
+
// before evaluation. Nodes built by a tool rather than the parser carry the
|
|
12
|
+
// empty span `[0, 0]` (see `build.ts`).
|
|
13
|
+
|
|
14
|
+
/** A half-open source range `[start, end)` in Unicode code points over the raw
|
|
15
|
+
* source (`rawSource` for a template, where a binding occupies its `${n}`
|
|
16
|
+
* marker). The empty span `[0, 0]` marks a node that did not come from source. */
|
|
17
|
+
export type Span = [start: number, end: number];
|
|
6
18
|
|
|
7
19
|
/** Query consumers — how a (sub)query's row set is shaped. `none` is the
|
|
8
20
|
* zero-cardinality complement of `exists` (true iff the block yields no rows). */
|
|
@@ -11,50 +23,85 @@ export type Consumer = "collect" | "exists" | "none" | "count" | "first" | "sing
|
|
|
11
23
|
/** Comparison operators usable in a `count { … } <op> <int>` test. */
|
|
12
24
|
export type RelOp = "==" | "!=" | "<" | "<=" | ">" | ">=";
|
|
13
25
|
|
|
26
|
+
/** The identity operators (SEMANTICS §5b, since 0.17). */
|
|
27
|
+
export type IdentityOp = "is" | "is not";
|
|
28
|
+
|
|
29
|
+
/** The operator of a `binary` expression: a comparison, an identity test, or arithmetic. */
|
|
30
|
+
export type BinaryOp = RelOp | IdentityOp | "+" | "-" | "*" | "/" | "%";
|
|
31
|
+
|
|
32
|
+
/** The operator of a `unary` expression. */
|
|
33
|
+
export type UnaryOp = "!" | "-";
|
|
34
|
+
|
|
35
|
+
/** The operator of a `logical` expression. */
|
|
36
|
+
export type LogicalOp = "&&" | "||";
|
|
37
|
+
|
|
14
38
|
/** Scalar value/predicate expression, evaluated against a row scope + bindings. */
|
|
15
39
|
export type Expr =
|
|
16
|
-
| { kind: "lit"; value: string | number | boolean | null }
|
|
17
|
-
| { kind: "ident"; name: string } // bare property of the CURRENT row/scope only (never climbs); `$it` is the row itself
|
|
18
|
-
| { kind: "outer"; levels: number; name: string } // `^name` — read from exactly `levels` scopes out
|
|
19
|
-
| { kind: "binding"; index: number } // a ${…} interpolated host value
|
|
20
|
-
| { kind: "member"; recv: Expr; name: string } // .prop navigation on the value to its left
|
|
21
|
-
| { kind: "
|
|
22
|
-
| { kind: "
|
|
23
|
-
|
|
24
|
-
| { kind: "
|
|
25
|
-
| { kind: "
|
|
26
|
-
| { kind: "
|
|
40
|
+
| { kind: "lit"; span: Span; value: string | number | boolean | null }
|
|
41
|
+
| { kind: "ident"; span: Span; name: string } // bare property of the CURRENT row/scope only (never climbs); `$it` is the row itself
|
|
42
|
+
| { kind: "outer"; span: Span; levels: number; name: string } // `^name` — read from exactly `levels` scopes out (also what `N^name` parses to, since 0.18)
|
|
43
|
+
| { kind: "binding"; span: Span; index: number } // a ${…} interpolated host value
|
|
44
|
+
| { kind: "member"; span: Span; recv: Expr; name: string } // .prop navigation on the value to its left
|
|
45
|
+
| { kind: "call"; span: Span; recv: Expr | null; name: string; args: Expr[] } // fn / method
|
|
46
|
+
| { kind: "unary"; span: Span; op: UnaryOp; expr: Expr }
|
|
47
|
+
// `x!` — the value of `expr`, or an eval error when it is absent (since 0.17).
|
|
48
|
+
| { kind: "required"; span: Span; expr: Expr }
|
|
49
|
+
| { kind: "binary"; span: Span; op: BinaryOp; left: Expr; right: Expr } // arithmetic + comparison + identity
|
|
50
|
+
| { kind: "logical"; span: Span; op: LogicalOp; left: Expr; right: Expr }
|
|
51
|
+
| { kind: "in"; span: Span; left: Expr; right: Expr }
|
|
27
52
|
// A Ruby-style range value. `lo`/`hi` are null for the open-ended forms
|
|
28
53
|
// (`..5` / `5..`); `exclusiveEnd` distinguishes `1...5` from `1..5`. Evaluates
|
|
29
54
|
// to a runtime range value (see semantics.makeRange); primarily the RHS of `in`.
|
|
30
|
-
| { kind: "range"; lo: Expr | null; hi: Expr | null; exclusiveEnd: boolean }
|
|
55
|
+
| { kind: "range"; span: Span; lo: Expr | null; hi: Expr | null; exclusiveEnd: boolean }
|
|
56
|
+
// A tuple `(e1, e2, …)` (since 0.20): two or more expressions whose values
|
|
57
|
+
// concatenate with one level of flattening (SEMANTICS §3b) — a value and a
|
|
58
|
+
// receiver (`(a, b) { … }`, `follow (a, b) { depth 2 }`). One expression in
|
|
59
|
+
// parentheses is grouping and leaves no node.
|
|
60
|
+
| { kind: "tuple"; span: Span; items: Expr[] }
|
|
61
|
+
// A value-position directive (since 0.17): `recv first { … }.pay`, and what a
|
|
62
|
+
// bracket lookup `x[…]` desugars to. `isExpr` is false for it — an op is an
|
|
63
|
+
// expression only by position; discriminate on `kind === "op"`.
|
|
64
|
+
| OpNode;
|
|
31
65
|
|
|
66
|
+
/** One `order by` term. */
|
|
32
67
|
export interface OrderSpec {
|
|
68
|
+
kind: "order";
|
|
69
|
+
span: Span;
|
|
33
70
|
expr: Expr;
|
|
34
71
|
desc: boolean;
|
|
35
72
|
}
|
|
36
73
|
|
|
37
74
|
/** One projection item: a named scalar/navigation value, or a named nested
|
|
38
|
-
* collection consumer. `lift` marks a `^name`
|
|
75
|
+
* collection consumer. `lift` marks a `^name` lift. */
|
|
39
76
|
export type SelectItem =
|
|
40
77
|
// `lift` is the number of `^` carets: 0 = an ordinary projection, N = a lift
|
|
41
|
-
// that binds this value N scopes out (see the engine's flatten-append).
|
|
42
|
-
|
|
43
|
-
|
|
78
|
+
// that binds this value N scopes out (see the engine's flatten-append). `name`
|
|
79
|
+
// is `""` for an unaliased non-navigation item, which the parser only admits
|
|
80
|
+
// under `values`.
|
|
81
|
+
| { kind: "field"; span: Span; name: string; expr: Expr; lift: number }
|
|
82
|
+
| { kind: "collect"; span: Span; name: string; op: OpNode };
|
|
83
|
+
|
|
84
|
+
/** The `count { … } <op> <int>` test attached to an `OpNode` — an attribute of
|
|
85
|
+
* the directive, not a node of its own. */
|
|
86
|
+
export interface CountCmp {
|
|
87
|
+
op: RelOp;
|
|
88
|
+
value: number;
|
|
89
|
+
}
|
|
44
90
|
|
|
45
91
|
/** A postfix consumer directive over a receiver collection:
|
|
46
92
|
* `<receiver> <op> { <sub> }`, optionally `count { … } <relop> <int>`. */
|
|
47
93
|
export interface OpNode {
|
|
48
94
|
kind: "op";
|
|
95
|
+
span: Span;
|
|
49
96
|
receiver: Expr;
|
|
50
97
|
op: Consumer;
|
|
51
98
|
sub: Subquery;
|
|
52
|
-
countCmp
|
|
99
|
+
countCmp: CountCmp | null;
|
|
53
100
|
/** `distinct` — dedup the rows this directive consumes by their projected
|
|
54
101
|
* value (the `select`), so `count distinct { … }` counts distinct projections
|
|
55
102
|
* and `collect distinct { … }` yields distinct rows. Empty select ⇒ dedup by
|
|
56
103
|
* row identity. Spellable as `<op> distinct { … }` or `{ select distinct … }`. */
|
|
57
|
-
distinct
|
|
104
|
+
distinct: boolean;
|
|
58
105
|
}
|
|
59
106
|
|
|
60
107
|
/** One `follow` destination: a relation of the current row (an `Expr` receiver), or a
|
|
@@ -62,48 +109,61 @@ export interface OpNode {
|
|
|
62
109
|
* per frontier row, inside which `^` is that row. Discriminate on `kind === "op"`. */
|
|
63
110
|
export type FollowDestination = Expr | OpNode;
|
|
64
111
|
|
|
65
|
-
/** The recursive `follow` clause.
|
|
112
|
+
/** The recursive `follow` clause (since 0.20 just the destinations: a
|
|
113
|
+
* destination's `where`, `frontier` and `depth` live in its own block). */
|
|
66
114
|
export interface Follow {
|
|
115
|
+
kind: "follow";
|
|
116
|
+
span: Span;
|
|
67
117
|
destinations: FollowDestination[]; // ≥ 1, in source order; the walk is their union
|
|
68
118
|
distinct: boolean;
|
|
69
|
-
where: Expr | null; // successor predicate: which successors keep participating
|
|
70
|
-
frontier: Expr | null; // boundary predicate: cut a relation that could continue
|
|
71
|
-
depth: number | null; // 1..8 cap; null = the hard cap
|
|
72
|
-
by: Expr | null; // identity expression for cycle detection / dedup
|
|
73
119
|
}
|
|
74
120
|
|
|
75
|
-
/** A nested (sub)query body. */
|
|
121
|
+
/** A nested (sub)query body — the `{ … }` of a directive. */
|
|
76
122
|
export interface Subquery {
|
|
123
|
+
kind: "subquery";
|
|
124
|
+
span: Span;
|
|
77
125
|
from: Expr[]; // body-level `from E` re-projections (flatMap chain)
|
|
78
126
|
where: Where | null;
|
|
79
127
|
select: SelectItem[];
|
|
80
128
|
orderBy: OrderSpec[] | null;
|
|
81
129
|
follow: Follow | null;
|
|
130
|
+
/** `frontier P` / `depth n` (since 0.20). In a `follow` destination block
|
|
131
|
+
* `frontier` marks the occurrences reached through this destination for
|
|
132
|
+
* which `P` holds (not expanded; `P` read one level out, under the walk's
|
|
133
|
+
* enclosing scope), and `depth n` makes the destination expand a frontier
|
|
134
|
+
* row only while that row's `$depth < n` (1..8; `null` = the hard cap 8).
|
|
135
|
+
* In any other block `frontier` is legal only with a `follow` and marks the
|
|
136
|
+
* walk's SEEDS, read in the seed's scope like `where` (as on `Query`);
|
|
137
|
+
* `depth` is destination-only. Both `null` when absent. */
|
|
138
|
+
frontier: Expr | null;
|
|
139
|
+
depth: number | null;
|
|
82
140
|
/** `values` — scalar projection mode: the (single) projected expression is the
|
|
83
141
|
* row's result itself rather than being wrapped in a `{ name: value }` record,
|
|
84
142
|
* so `name values` yields `["Bob", …]` and `$it values` yields the rows. */
|
|
85
|
-
values
|
|
143
|
+
values: boolean;
|
|
86
144
|
/** `limit N` / `offset N` — bound the row set AFTER where/order/distinct and
|
|
87
145
|
* BEFORE the consumer reduces it, so `count { … limit 5 }` is at most 5 and
|
|
88
146
|
* `first { … offset 1 }` is the second row. Each is a value expression
|
|
89
147
|
* (a literal, a `${…}` binding, or an outer reference) read as part of the
|
|
90
148
|
* block — `^n` is the enclosing row's field, as everywhere inside `{ … }` —
|
|
91
149
|
* and must yield a non-negative integer. */
|
|
92
|
-
limit
|
|
93
|
-
offset
|
|
150
|
+
limit: Expr | null;
|
|
151
|
+
offset: Expr | null;
|
|
94
152
|
}
|
|
95
153
|
|
|
96
154
|
/** The where-clause boolean tree: OQX owns &&/||/!/grouping so consumer ops
|
|
97
155
|
* (invisible to the scalar evaluator) compose with scalar predicates. */
|
|
98
156
|
export type Where =
|
|
99
|
-
| { kind: "and"; parts: Where[] }
|
|
100
|
-
| { kind: "or"; parts: Where[] }
|
|
101
|
-
| { kind: "not"; expr: Where }
|
|
102
|
-
| { kind: "scalar"; expr: Expr }
|
|
157
|
+
| { kind: "and"; span: Span; parts: Where[] }
|
|
158
|
+
| { kind: "or"; span: Span; parts: Where[] }
|
|
159
|
+
| { kind: "not"; span: Span; expr: Where }
|
|
160
|
+
| { kind: "scalar"; span: Span; expr: Expr }
|
|
103
161
|
| OpNode;
|
|
104
162
|
|
|
105
163
|
/** A top-level OQX query. */
|
|
106
164
|
export interface Query {
|
|
165
|
+
kind: "query";
|
|
166
|
+
span: Span;
|
|
107
167
|
source: Expr; // the root collection (`from <source>` or the directive receiver)
|
|
108
168
|
from: Expr[]; // further top-level `from E` re-projections
|
|
109
169
|
where: Where | null;
|
|
@@ -111,11 +171,37 @@ export interface Query {
|
|
|
111
171
|
orderBy: OrderSpec[] | null;
|
|
112
172
|
consumer: Consumer;
|
|
113
173
|
follow: Follow | null;
|
|
174
|
+
/** `frontier P` (since 0.20) — legal only when the body has a `follow`: a
|
|
175
|
+
* seed for which `P` holds (read in the seed's own scope, `^` the enclosing
|
|
176
|
+
* scope, like `where`) is a frontier (`$stop == "frontier"`, not expanded).
|
|
177
|
+
* `null` without one. */
|
|
178
|
+
frontier: Expr | null;
|
|
114
179
|
/** `distinct` — dedup the result rows by their projected value (see OpNode). */
|
|
115
|
-
distinct
|
|
180
|
+
distinct: boolean;
|
|
116
181
|
/** `values` — scalar projection mode (see Subquery). */
|
|
117
|
-
values
|
|
182
|
+
values: boolean;
|
|
118
183
|
/** `limit N` / `offset N` (see Subquery); evaluated at the root scope. */
|
|
119
|
-
limit
|
|
120
|
-
offset
|
|
184
|
+
limit: Expr | null;
|
|
185
|
+
offset: Expr | null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Every node of the tree. `AstNode["kind"]` is the closed set of node kinds. */
|
|
189
|
+
export type AstNode = Query | Subquery | OpNode | Follow | OrderSpec | SelectItem | Where | Expr;
|
|
190
|
+
|
|
191
|
+
/** The closed set of node kinds. */
|
|
192
|
+
export type AstKind = AstNode["kind"];
|
|
193
|
+
|
|
194
|
+
/** The expression kinds (`Expr["kind"]`), for `isExpr`. */
|
|
195
|
+
const EXPR_KINDS: ReadonlySet<string> = new Set([
|
|
196
|
+
"lit", "ident", "outer", "binding", "member", "call", "unary", "required", "binary", "logical", "in", "range", "tuple",
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
/** Whether a node is a scalar `Expr` (an `op` in expression position is not: it
|
|
200
|
+
* is an expression only by position, see AST.md §1). */
|
|
201
|
+
export function isExpr(node: AstNode): node is Expr {
|
|
202
|
+
return EXPR_KINDS.has(node.kind);
|
|
121
203
|
}
|
|
204
|
+
|
|
205
|
+
/** The parsed query's JSON form (`toJSON`): the tree with the language version
|
|
206
|
+
* it was produced under. */
|
|
207
|
+
export type QueryJson = Query & { oqx: string };
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Builders for AST nodes a tool constructs itself (spec/oqx/AST.md §7). Every
|
|
2
|
+
// node they make carries the empty span `[0, 0]` — "not from source" — and the
|
|
3
|
+
// materialized defaults (`distinct: false`, `limit: null`, …), so a hand-built
|
|
4
|
+
// tree has the same shape as a parsed one and prints with `print`.
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
BinaryOp, Consumer, CountCmp, Expr, Follow, FollowDestination, LogicalOp, OpNode, OrderSpec, Query, SelectItem, Span,
|
|
8
|
+
Subquery, UnaryOp, Where,
|
|
9
|
+
} from "./ast.ts";
|
|
10
|
+
|
|
11
|
+
/** A fresh empty span (never shared: spans are mutable tuples). */
|
|
12
|
+
export const span = (): Span => [0, 0];
|
|
13
|
+
|
|
14
|
+
export const lit = (value: string | number | boolean | null): Expr => ({ kind: "lit", span: span(), value });
|
|
15
|
+
export const ident = (name: string): Expr => ({ kind: "ident", span: span(), name });
|
|
16
|
+
export const outer = (levels: number, name: string): Expr => ({ kind: "outer", span: span(), levels, name });
|
|
17
|
+
export const binding = (index: number): Expr => ({ kind: "binding", span: span(), index });
|
|
18
|
+
export const member = (recv: Expr, name: string): Expr => ({ kind: "member", span: span(), recv, name });
|
|
19
|
+
/** `a.b.c` from a dotted path. */
|
|
20
|
+
export const path = (first: string, ...rest: string[]): Expr => rest.reduce<Expr>((e, n) => member(e, n), ident(first));
|
|
21
|
+
export const call = (recv: Expr | null, name: string, args: Expr[] = []): Expr => ({ kind: "call", span: span(), recv, name, args });
|
|
22
|
+
export const unary = (op: UnaryOp, expr: Expr): Expr => ({ kind: "unary", span: span(), op, expr });
|
|
23
|
+
/** `expr!` — the required value (SEMANTICS §5b). */
|
|
24
|
+
export const required = (expr: Expr): Expr => ({ kind: "required", span: span(), expr });
|
|
25
|
+
export const binary = (op: BinaryOp, left: Expr, right: Expr): Expr => ({ kind: "binary", span: span(), op, left, right });
|
|
26
|
+
export const logical = (op: LogicalOp, left: Expr, right: Expr): Expr => ({ kind: "logical", span: span(), op, left, right });
|
|
27
|
+
export const inOp = (left: Expr, right: Expr): Expr => ({ kind: "in", span: span(), left, right });
|
|
28
|
+
export const range = (lo: Expr | null, hi: Expr | null, exclusiveEnd = false): Expr => ({ kind: "range", span: span(), lo, hi, exclusiveEnd });
|
|
29
|
+
/** `(e1, e2, …)` — a tuple of two or more expressions (SEMANTICS §3b). */
|
|
30
|
+
export const tuple = (items: Expr[]): Expr => ({ kind: "tuple", span: span(), items });
|
|
31
|
+
|
|
32
|
+
export const scalar = (expr: Expr): Where => ({ kind: "scalar", span: span(), expr });
|
|
33
|
+
export const and = (parts: Where[]): Where => ({ kind: "and", span: span(), parts });
|
|
34
|
+
export const or = (parts: Where[]): Where => ({ kind: "or", span: span(), parts });
|
|
35
|
+
export const not = (expr: Where): Where => ({ kind: "not", span: span(), expr });
|
|
36
|
+
|
|
37
|
+
export const field = (name: string, expr: Expr, lift = 0): SelectItem => ({ kind: "field", span: span(), name, expr, lift });
|
|
38
|
+
export const collect = (name: string, op: OpNode): SelectItem => ({ kind: "collect", span: span(), name, op });
|
|
39
|
+
export const order = (expr: Expr, desc = false): OrderSpec => ({ kind: "order", span: span(), expr, desc });
|
|
40
|
+
|
|
41
|
+
/** A block body; every clause defaults to empty/absent. */
|
|
42
|
+
export function subquery(parts: Partial<Omit<Subquery, "kind" | "span">> = {}): Subquery {
|
|
43
|
+
return {
|
|
44
|
+
kind: "subquery", span: span(),
|
|
45
|
+
from: [], where: null, select: [], orderBy: null, follow: null, frontier: null, depth: null, values: false, limit: null, offset: null,
|
|
46
|
+
...parts,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `<receiver> <op> [distinct] { sub } [<relop> N]`. */
|
|
51
|
+
export function op(
|
|
52
|
+
receiver: Expr, consumer: Consumer, sub: Subquery = subquery(),
|
|
53
|
+
opts: { distinct?: boolean; countCmp?: CountCmp | null } = {},
|
|
54
|
+
): OpNode {
|
|
55
|
+
return { kind: "op", span: span(), receiver, op: consumer, sub, countCmp: opts.countCmp ?? null, distinct: opts.distinct ?? false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `follow [distinct] <destinations>`; a destination's `where`/`frontier`/`depth`
|
|
59
|
+
* belong to its block (`op(recv, "collect", subquery({ depth: 2 }))`). */
|
|
60
|
+
export function follow(destinations: FollowDestination[], opts: { distinct?: boolean } = {}): Follow {
|
|
61
|
+
return { kind: "follow", span: span(), destinations, distinct: opts.distinct ?? false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A top-level query over `source`; every other clause defaults to empty/absent
|
|
65
|
+
* and the consumer to `collect`. */
|
|
66
|
+
export function query(source: Expr, parts: Partial<Omit<Query, "kind" | "span" | "source">> = {}): Query {
|
|
67
|
+
return {
|
|
68
|
+
kind: "query", span: span(), source,
|
|
69
|
+
from: [], where: null, select: [], orderBy: null, consumer: "collect", follow: null, frontier: null,
|
|
70
|
+
distinct: false, values: false, limit: null, offset: null,
|
|
71
|
+
...parts,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// The tier-2 seam: a DataContext binds OQX's query semantics to a concrete data
|
|
2
2
|
// model. The engine never touches host objects directly — it asks the context to
|
|
3
3
|
// resolve named roots, read properties/relations, coerce a relation result into
|
|
4
|
-
// rows
|
|
5
|
-
//
|
|
4
|
+
// rows (and say which objects are one row rather than a collection), name a
|
|
5
|
+
// row's identity (`$key`: `follow` dedup, `distinct`, `is`), and optionally
|
|
6
|
+
// supply custom scalar functions/methods. This is what lets the same query semantics run over
|
|
6
7
|
// plain objects, a lazy ORM graph, or a remote API without changing the engine.
|
|
7
8
|
//
|
|
8
9
|
// (Performant execution over a real store is the tier-3 seam — see planner.ts —
|
|
@@ -28,6 +29,14 @@ export interface CallResult {
|
|
|
28
29
|
export interface DataContext {
|
|
29
30
|
/** Resolve a named root collection (the `from <name>` / directive receiver). */
|
|
30
31
|
root(name: string): unknown;
|
|
32
|
+
/** The root scope's row — the host's root object: what `$it` is at the root
|
|
33
|
+
* scope and `^$it` from a top-level row (SEMANTICS §2, since 0.18), so
|
|
34
|
+
* `^$it.people` navigates it through `get` and `entries(^$it)` enumerates it.
|
|
35
|
+
* Bare names at the root still resolve through `root(name)`, so a host with
|
|
36
|
+
* lazy roots is unaffected. Optional: a context without it (or returning
|
|
37
|
+
* `undefined`) has no root row — `$it` at the root is absent, the pre-0.18
|
|
38
|
+
* behavior. `DefaultContext` returns its roots record. */
|
|
39
|
+
rootObject?(): unknown;
|
|
31
40
|
/** Read a property/relation off a row: a bare identifier (`field`), a
|
|
32
41
|
* `.field` segment, or a `^field` outer reference all come through here, each
|
|
33
42
|
* against exactly the row of the scope it names. An absent property is
|
|
@@ -37,6 +46,13 @@ export interface DataContext {
|
|
|
37
46
|
get(row: unknown, key: string): unknown;
|
|
38
47
|
/** Coerce a relation/source value into rows (may be lazy). */
|
|
39
48
|
toRows(value: unknown): Iterable<unknown>;
|
|
49
|
+
/** Optional: whether an object this context handed out in row position is
|
|
50
|
+
* ONE row — a record, not a collection (SEMANTICS §3, since 0.20) — so a block
|
|
51
|
+
* over it yields one result rather than a one-element list (`doc { title }`
|
|
52
|
+
* is a record; `docs { title }` a list). Asked only for non-array objects;
|
|
53
|
+
* absent, any non-iterable object is one row and any iterable (a `Set`, a
|
|
54
|
+
* lazy handle with `Symbol.iterator`) is a collection. */
|
|
55
|
+
isSingle?(value: unknown): boolean;
|
|
40
56
|
/** Optional: a value this context handed out as a stand-in for a collection
|
|
41
57
|
* it has not read yet (a lazy table handle), resolved to what it stands for.
|
|
42
58
|
* The engine calls it on every value it is about to observe AS A VALUE — an
|
|
@@ -49,8 +65,15 @@ export interface DataContext {
|
|
|
49
65
|
* seen by the language. Absent = identity; a context whose handle already
|
|
50
66
|
* behaves as the collection (a `Proxy` over an array) needs none. */
|
|
51
67
|
materialize?(value: unknown): unknown;
|
|
52
|
-
/**
|
|
53
|
-
|
|
68
|
+
/** Optional: the identity a row is known by — `$key` (SEMANTICS §16, since
|
|
69
|
+
* 0.20): what `follow` cycle detection, the per-step union, `follow distinct`,
|
|
70
|
+
* `distinct` over unprojected rows and `is` compare, and what reading `$key`
|
|
71
|
+
* on the row returns. Absent, a row's identity is its structural value (an
|
|
72
|
+
* entry's is its key either way). A store-backed context answers its row id. */
|
|
73
|
+
identityOf?(row: unknown): unknown;
|
|
74
|
+
/** @deprecated The pre-0.20 spelling of `identityOf`; read when `identityOf`
|
|
75
|
+
* is absent. Removed in the next minor. */
|
|
76
|
+
identity?(row: unknown): unknown;
|
|
54
77
|
/** Optional custom free function; return `{ handled: false }` to defer. */
|
|
55
78
|
callFunction?(name: string, args: unknown[]): CallResult;
|
|
56
79
|
/** Optional custom method; return `{ handled: false }` to defer. */
|
|
@@ -82,9 +105,11 @@ export interface DefaultContextOptions {
|
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
/** The default context: ordinary JavaScript objects. Named roots come from a
|
|
85
|
-
* plain `{ name: collection }` map
|
|
86
|
-
*
|
|
87
|
-
*
|
|
108
|
+
* plain `{ name: collection }` map, which is also the root object (`^$it` from
|
|
109
|
+
* a top-level row); properties are OWN keys only (an array exposes only its
|
|
110
|
+
* integer indices, a primitive has none); it names no identity of its own, so a
|
|
111
|
+
* row's `$key` is its structural value (SEMANTICS §16) unless a query projects
|
|
112
|
+
* one (`$key: id`). */
|
|
88
113
|
export class DefaultContext implements DataContext {
|
|
89
114
|
private roots: Record<string, unknown>;
|
|
90
115
|
readonly regexDialect: RegexDialect;
|
|
@@ -98,6 +123,10 @@ export class DefaultContext implements DataContext {
|
|
|
98
123
|
return Object.hasOwn(this.roots, name) ? this.roots[name] : undefined;
|
|
99
124
|
}
|
|
100
125
|
|
|
126
|
+
rootObject(): unknown {
|
|
127
|
+
return this.roots;
|
|
128
|
+
}
|
|
129
|
+
|
|
101
130
|
get(row: unknown, key: string): unknown {
|
|
102
131
|
if (row == null || typeof row !== "object") return undefined;
|
|
103
132
|
if (Array.isArray(row)) return isIndexKey(key) ? row[Number(key)] : undefined;
|
|
@@ -108,11 +137,6 @@ export class DefaultContext implements DataContext {
|
|
|
108
137
|
return coerceCollection(value);
|
|
109
138
|
}
|
|
110
139
|
|
|
111
|
-
identity(row: unknown): unknown {
|
|
112
|
-
if (row != null && typeof row === "object" && "id" in row) return (row as Record<string, unknown>).id;
|
|
113
|
-
return row;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
140
|
callFunction(name: string, args: unknown[]): CallResult {
|
|
117
141
|
const fn = Object.hasOwn(BUILTIN_FUNCTIONS, name) ? BUILTIN_FUNCTIONS[name] : undefined;
|
|
118
142
|
return fn ? { handled: true, value: fn(args) } : { handled: false };
|