@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/plan.ts
CHANGED
|
@@ -34,14 +34,15 @@ export function partitionPushable(
|
|
|
34
34
|
if (p.kind === "scalar" && canPush(p.expr)) pushed.push(p.expr);
|
|
35
35
|
else rest.push(p);
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
if (pushed.length === 0) return { pushed, residual: where }; // nothing pushed: the where itself, span and all
|
|
38
|
+
const residual: Where | null = rest.length === 0 ? null : rest.length === 1 ? rest[0]! : { kind: "and", span: [0, 0], parts: rest };
|
|
38
39
|
return { pushed, residual };
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/** Rebuild a query to run in-memory over a plan's produced rows: scan the rows
|
|
42
43
|
* root, drop pushed top-level `from`/predicates, keep projection/order/consumer. */
|
|
43
44
|
export function residualQuery(query: Query, residualWhere: Where | null): Query {
|
|
44
|
-
return { ...query, source: { kind: "ident", name: ROWS_ROOT }, from: [], where: residualWhere };
|
|
45
|
+
return { ...query, source: { kind: "ident", span: [0, 0], name: ROWS_ROOT }, from: [], where: residualWhere };
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/** A literal or binding — a value known without a row context. */
|
package/src/print.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// The canonical printer (spec/oqx/AST.md §6): the one source text of a tree.
|
|
2
|
+
// Single spaces, double-quoted strings with GRAMMAR §1 escapes, the minimal
|
|
3
|
+
// parentheses the precedence table (GRAMMAR §4) needs, `select` written at the
|
|
4
|
+
// top level, the connectives as the words `and` / `or` (since 0.18; `!` stays
|
|
5
|
+
// `!`), every clause in its canonical position (a destination block's
|
|
6
|
+
// `frontier` and `depth` after its `where`, since 0.20). The law both spec runners
|
|
7
|
+
// enforce over every fixture query:
|
|
8
|
+
//
|
|
9
|
+
// strip(parse(print(parse(q)))) ≡ strip(parse(q))
|
|
10
|
+
//
|
|
11
|
+
// `print(parse(q)) == q` is NOT a law — the printer normalizes spelling. A
|
|
12
|
+
// binding is a value, never source text: `print` throws on one; `printTemplate`
|
|
13
|
+
// emits template fragments around each binding instead.
|
|
14
|
+
|
|
15
|
+
import type { AstNode, Expr, Follow, OpNode, OrderSpec, Query, SelectItem, Subquery, Where } from "./ast.ts";
|
|
16
|
+
import { OqxError } from "./errors.ts";
|
|
17
|
+
|
|
18
|
+
/** The template form of a tree: `strings` has `count + 1` fragments; the i-th gap
|
|
19
|
+
* stands for the binding `indices[i]` of the printed tree, so a caller re-runs it
|
|
20
|
+
* as `parseTemplate(strings, count)` with the values permuted by `indices`. In a
|
|
21
|
+
* tree straight from `parseTemplate`, `indices` is `0..count-1` unless the
|
|
22
|
+
* canonical clause order moved a binding past another (`${0} collect { x: ${1} }`
|
|
23
|
+
* prints as `select x: ${1} from ${0}`). */
|
|
24
|
+
export interface Template {
|
|
25
|
+
strings: string[];
|
|
26
|
+
count: number;
|
|
27
|
+
indices: number[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The canonical source of a node. Throws (`OqxError`, stage `print`) on a binding. */
|
|
31
|
+
export function print(node: AstNode): string {
|
|
32
|
+
let text = "";
|
|
33
|
+
for (const p of render(node)) {
|
|
34
|
+
if (typeof p === "number") {
|
|
35
|
+
throw new OqxError(`cannot print binding \${${p}}: a binding is a value, not source text — use printTemplate`, "print");
|
|
36
|
+
}
|
|
37
|
+
text += p;
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The canonical source of a node as template fragments, one gap per binding. */
|
|
43
|
+
export function printTemplate(node: AstNode): Template {
|
|
44
|
+
const strings: string[] = [];
|
|
45
|
+
const indices: number[] = [];
|
|
46
|
+
let current = "";
|
|
47
|
+
for (const p of render(node)) {
|
|
48
|
+
if (typeof p === "number") { strings.push(current); current = ""; indices.push(p); }
|
|
49
|
+
else current += p;
|
|
50
|
+
}
|
|
51
|
+
strings.push(current);
|
|
52
|
+
return { strings, count: indices.length, indices };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Output is a list of pieces: source text, or a binding index standing where the
|
|
56
|
+
// binding goes (so a template can be split exactly there).
|
|
57
|
+
type Out = (string | number)[];
|
|
58
|
+
|
|
59
|
+
const cat = (...xs: (string | Out)[]): Out => xs.flatMap((x) => (typeof x === "string" ? [x] : x));
|
|
60
|
+
const join = (parts: Out[], sep: string): Out => parts.flatMap((p, i) => (i === 0 ? p : [sep, ...p]));
|
|
61
|
+
const paren = (o: Out, yes: boolean): Out => (yes ? cat("(", o, ")") : o);
|
|
62
|
+
|
|
63
|
+
// Precedence levels of GRAMMAR §4, loosest to tightest. A child is parenthesized
|
|
64
|
+
// when its level is below what its position requires.
|
|
65
|
+
const OR = 1, AND = 2, CMP = 3, RANGE = 4, ADD = 5, MUL = 6, UNARY = 7, POSTFIX = 8, PRIMARY = 9;
|
|
66
|
+
|
|
67
|
+
function prec(e: Expr): number {
|
|
68
|
+
switch (e.kind) {
|
|
69
|
+
case "lit": case "ident": case "outer": case "binding": case "tuple": return PRIMARY;
|
|
70
|
+
case "member": case "call": case "required": case "op": return POSTFIX;
|
|
71
|
+
case "unary": return UNARY;
|
|
72
|
+
case "binary": return e.op === "+" || e.op === "-" ? ADD : e.op === "*" || e.op === "/" || e.op === "%" ? MUL : CMP;
|
|
73
|
+
case "in": return CMP;
|
|
74
|
+
case "range": return RANGE;
|
|
75
|
+
case "logical": return e.op === "&&" ? AND : OR;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function render(n: AstNode): Out {
|
|
80
|
+
switch (n.kind) {
|
|
81
|
+
case "query": return query(n);
|
|
82
|
+
case "subquery": return block(n);
|
|
83
|
+
case "op": return op(n);
|
|
84
|
+
case "follow": return follow(n);
|
|
85
|
+
case "order": return order(n);
|
|
86
|
+
case "field": case "collect": return item(n);
|
|
87
|
+
case "and": case "or": case "not": case "scalar": return where(n);
|
|
88
|
+
default: return expr(n);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- query / bodies ---------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
function query(q: Query): Out {
|
|
95
|
+
// Body form for `collect` — unless the query carries body-level `from`
|
|
96
|
+
// re-projections, which only a block can hold (a second top-level `from` is a
|
|
97
|
+
// duplicate clause): then the directive form `source collect { … from E }`.
|
|
98
|
+
if (q.consumer === "collect" && q.from.length === 0) {
|
|
99
|
+
// `select` is always written — except for a leading bare item literally
|
|
100
|
+
// named `distinct`, which the keyword would swallow as the modifier.
|
|
101
|
+
const first = q.select[0];
|
|
102
|
+
const bareDistinct = !q.distinct && first?.kind === "field" && first.name === "distinct"
|
|
103
|
+
&& first.expr.kind === "ident" && first.expr.name === "distinct";
|
|
104
|
+
const clauses: Out[] = [];
|
|
105
|
+
if (q.select.length > 0) {
|
|
106
|
+
const items = projection(q.select, q.values);
|
|
107
|
+
clauses.push(bareDistinct ? items : cat(`select ${q.distinct ? "distinct " : ""}`, items));
|
|
108
|
+
}
|
|
109
|
+
clauses.push(cat("from ", expr(q.source)));
|
|
110
|
+
clauses.push(...tail(q));
|
|
111
|
+
return join(clauses, " ");
|
|
112
|
+
}
|
|
113
|
+
// Directive form: `<source> <consumer> [distinct] { body }`.
|
|
114
|
+
return cat(expr(q.source), ` ${q.consumer}${q.distinct ? " distinct" : ""} `, braces(body(q)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type Body = Pick<Subquery, "select" | "values" | "from" | "where" | "follow" | "orderBy" | "limit" | "offset">
|
|
118
|
+
& Partial<Pick<Subquery, "frontier" | "depth">>;
|
|
119
|
+
|
|
120
|
+
// The clauses after `from` (a query) or after the projection (a block): the
|
|
121
|
+
// fixed clause order, each present clause once. `frontier`/`depth` exist only
|
|
122
|
+
// on a follow destination block's body (a `Query` has neither).
|
|
123
|
+
function tail(b: Omit<Body, "select" | "values">): Out[] {
|
|
124
|
+
const out: Out[] = [];
|
|
125
|
+
for (const e of b.from) out.push(cat("from ", expr(e)));
|
|
126
|
+
if (b.where) out.push(cat("where ", where(b.where)));
|
|
127
|
+
if (b.frontier) out.push(cat("frontier ", expr(b.frontier)));
|
|
128
|
+
if (b.depth != null) out.push([`depth ${number(b.depth)}`]);
|
|
129
|
+
if (b.follow) out.push(follow(b.follow));
|
|
130
|
+
if (b.orderBy && b.orderBy.length > 0) out.push(cat("order by ", join(b.orderBy.map(order), ", ")));
|
|
131
|
+
if (b.limit) out.push(cat("limit ", expr(b.limit)));
|
|
132
|
+
if (b.offset) out.push(cat("offset ", expr(b.offset)));
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A block body: the projection leads without its keyword (the body's first
|
|
137
|
+
// clause is always the projection, so the keyword adds nothing there).
|
|
138
|
+
function body(b: Body): Out[] {
|
|
139
|
+
const clauses: Out[] = [];
|
|
140
|
+
if (b.select.length > 0) clauses.push(projection(b.select, b.values));
|
|
141
|
+
clauses.push(...tail(b));
|
|
142
|
+
return clauses;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function block(sub: Subquery): Out {
|
|
146
|
+
return braces(body(sub));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function braces(clauses: Out[]): Out {
|
|
150
|
+
return clauses.length === 0 ? ["{ }"] : cat("{ ", join(clauses, " "), " }");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function projection(items: SelectItem[], values: boolean): Out {
|
|
154
|
+
return cat(join(items.map(item), ", "), values ? " values" : "");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function item(it: SelectItem): Out {
|
|
158
|
+
// An unnamed directive item exists only under `values` (`jobs[0] values`).
|
|
159
|
+
if (it.kind === "collect") return it.name === "" ? op(it.op) : cat(`${it.name}: `, op(it.op));
|
|
160
|
+
return cat("^".repeat(it.lift), it.name === "" ? "" : `${it.name}: `, expr(it.expr));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function order(o: OrderSpec): Out {
|
|
164
|
+
return cat(expr(o.expr), o.desc ? " desc" : "");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function op(o: OpNode): Out {
|
|
168
|
+
return cat(
|
|
169
|
+
receiver(o.receiver),
|
|
170
|
+
` ${o.op}${o.distinct ? " distinct" : ""} `,
|
|
171
|
+
block(o.sub),
|
|
172
|
+
o.countCmp ? ` ${o.countCmp.op} ${number(o.countCmp.value)}` : "",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function follow(f: Follow): Out {
|
|
177
|
+
const dests = join(f.destinations.map((d) => (d.kind === "op" ? op(d) : expr(d))), ", ");
|
|
178
|
+
return cat(`follow${f.distinct ? " distinct" : ""} `, dests);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- where ------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
function where(w: Where): Out {
|
|
184
|
+
switch (w.kind) {
|
|
185
|
+
// The connectives print as the words (`and` / `or`, since 0.18); the symbols
|
|
186
|
+
// parse to the same nodes.
|
|
187
|
+
case "and": return join(w.parts.map((p) => paren(where(p), p.kind === "or" || p.kind === "and")), " and ");
|
|
188
|
+
case "or": return join(w.parts.map((p) => paren(where(p), p.kind === "or")), " or ");
|
|
189
|
+
case "not": {
|
|
190
|
+
const inner = w.expr;
|
|
191
|
+
// The operand of `!` is a consumer test, a group, or a scalar primary —
|
|
192
|
+
// a comparison or anything looser must be grouped (`!(a == b)`).
|
|
193
|
+
const grouped = inner.kind === "and" || inner.kind === "or" || (inner.kind === "scalar" && prec(inner.expr) < UNARY);
|
|
194
|
+
return cat("!", paren(where(inner), grouped));
|
|
195
|
+
}
|
|
196
|
+
case "scalar": return expr(w.expr);
|
|
197
|
+
case "op": return op(w);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- expressions ------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
function expr(e: Expr): Out {
|
|
204
|
+
switch (e.kind) {
|
|
205
|
+
case "lit": return [literal(e.value)];
|
|
206
|
+
case "ident": return [e.name];
|
|
207
|
+
case "outer": return ["^".repeat(e.levels) + e.name];
|
|
208
|
+
case "binding": return [e.index];
|
|
209
|
+
case "member": return cat(receiver(e.recv), `.${e.name}`);
|
|
210
|
+
case "call": {
|
|
211
|
+
const args = cat("(", join(e.args.map(expr), ", "), ")");
|
|
212
|
+
return e.recv === null ? cat(e.name, args) : cat(receiver(e.recv), `.${e.name}`, args);
|
|
213
|
+
}
|
|
214
|
+
case "unary": return cat(e.op, operand(e.expr, UNARY));
|
|
215
|
+
// Postfix, tightest: anything below postfix level is grouped (`(a + 1)!`).
|
|
216
|
+
case "required": return cat(operand(e.expr, POSTFIX), "!");
|
|
217
|
+
case "op": return op(e);
|
|
218
|
+
case "binary": {
|
|
219
|
+
const p = prec(e);
|
|
220
|
+
// Left-associative arithmetic keeps an equal-level left operand bare and
|
|
221
|
+
// groups an equal-level right one; a comparison is non-associative, so a
|
|
222
|
+
// nested comparison is grouped on either side.
|
|
223
|
+
return cat(operand(e.left, p === CMP ? p + 1 : p), ` ${e.op} `, operand(e.right, p + 1));
|
|
224
|
+
}
|
|
225
|
+
case "logical": {
|
|
226
|
+
const p = prec(e);
|
|
227
|
+
return cat(operand(e.left, p), e.op === "&&" ? " and " : " or ", operand(e.right, p + 1));
|
|
228
|
+
}
|
|
229
|
+
case "in": return cat(operand(e.left, CMP + 1), " in ", operand(e.right, CMP + 1));
|
|
230
|
+
case "range":
|
|
231
|
+
return cat(e.lo ? operand(e.lo, ADD) : "", e.exclusiveEnd ? "..." : "..", e.hi ? operand(e.hi, ADD) : "");
|
|
232
|
+
// A tuple is self-delimiting; its elements are full expressions.
|
|
233
|
+
case "tuple": return cat("(", join(e.items.map(expr), ", "), ")");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// A child expression, parenthesized when its level is below `min`.
|
|
238
|
+
function operand(e: Expr, min: number): Out {
|
|
239
|
+
return paren(expr(e), prec(e) < min);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// The value a `.name` navigates: postfix level, and a number literal is grouped
|
|
243
|
+
// too (`1.size()` would lex as a malformed number).
|
|
244
|
+
function receiver(e: Expr): Out {
|
|
245
|
+
return paren(expr(e), prec(e) < POSTFIX || (e.kind === "lit" && typeof e.value === "number"));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function literal(v: string | number | boolean | null): string {
|
|
249
|
+
if (v === null) return "null";
|
|
250
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
251
|
+
if (typeof v === "number") return number(v);
|
|
252
|
+
return quote(v);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** A number in the shortest round-trip form (`String(n)`); the lexer reads every
|
|
256
|
+
* spelling JavaScript produces for a finite number, including `1e+21`. */
|
|
257
|
+
function number(n: number): string {
|
|
258
|
+
return String(n);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A double-quoted string literal with the GRAMMAR §1 escapes. */
|
|
262
|
+
function quote(s: string): string {
|
|
263
|
+
let out = '"';
|
|
264
|
+
for (const ch of s) {
|
|
265
|
+
switch (ch) {
|
|
266
|
+
case '"': out += '\\"'; break;
|
|
267
|
+
case "\\": out += "\\\\"; break;
|
|
268
|
+
case "\n": out += "\\n"; break;
|
|
269
|
+
case "\t": out += "\\t"; break;
|
|
270
|
+
case "\r": out += "\\r"; break;
|
|
271
|
+
case "\0": out += "\\0"; break;
|
|
272
|
+
default: out += ch;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out + '"';
|
|
276
|
+
}
|
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Select-alias resolution (SEMANTICS §14). `where` may reference the same body's
|
|
2
|
+
// `select` aliases, and since 0.17 a `select` item may reference the items to
|
|
3
|
+
// its left; the parser keeps the surface form (so the tree can be printed back
|
|
4
|
+
// and reflected on) and only VALIDATES the references, and this module performs
|
|
5
|
+
// the substitution — a pure rewrite the entry points (`run`, `execute`, `oqx`,
|
|
6
|
+
// `runQuery`) apply once before evaluation, so the engine and any pushdown
|
|
7
|
+
// planner see an ordinary `where` and `select` over row fields. Rules:
|
|
8
|
+
// • an alias shadows a same-named row field inside `where` and in the items
|
|
9
|
+
// after it;
|
|
10
|
+
// • an alias's own name inside its own expression is the row field
|
|
11
|
+
// (`name: name.upper()` is not recursive), but a chain of aliases that
|
|
12
|
+
// comes back to one being resolved (`a: b, b: a`) is a cycle → error;
|
|
13
|
+
// • a `select` item may name only the items to its LEFT; a reference to an
|
|
14
|
+
// item to its right is an error (it is a cycle when that item refers back);
|
|
15
|
+
// • an alias whose value is a `collect`/`first`/`single { … }` block may
|
|
16
|
+
// stand alone as a where leaf (a collection in predicate position means
|
|
17
|
+
// non-empty) but not appear inside a `where` expression; in a later `select`
|
|
18
|
+
// item it inlines as a value-position directive (`boss.name`);
|
|
19
|
+
// • nested blocks (consumer bodies, follow blocks, bracket lookups) are their
|
|
20
|
+
// own scopes and are not rewritten against this body's select — each body
|
|
21
|
+
// rewrites against its own.
|
|
22
|
+
//
|
|
23
|
+
// Resolution is NOT idempotent (`name: name.upper() … where name` resolves to
|
|
24
|
+
// `name.upper()`, whose `name` would be substituted again), so it must run
|
|
25
|
+
// exactly once: the entry points do it, `Engine.run` evaluates the query as
|
|
26
|
+
// given. Untouched nodes are returned by reference, so a cached query keeps the
|
|
27
|
+
// identity of every block the rewrite did not reach.
|
|
28
|
+
|
|
29
|
+
import type { Expr, Follow, FollowDestination, OpNode, Query, SelectItem, Subquery, Where } from "./ast.ts";
|
|
30
|
+
import { OqxError } from "./errors.ts";
|
|
31
|
+
|
|
32
|
+
/** How a resolution failure is reported: the parser raises its positioned parse
|
|
33
|
+
* error, the standalone entry point a plain one. */
|
|
34
|
+
export type Fail = (message: string) => never;
|
|
35
|
+
|
|
36
|
+
const failPlain: Fail = (message) => {
|
|
37
|
+
throw new OqxError(message, "parse");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Resolve every `where` and `select` of a query (its own and every nested
|
|
41
|
+
* block's) against the `select` of the same body. Pure: returns a new tree
|
|
42
|
+
* sharing every node the rewrite did not touch. Throws an `OqxError` (stage
|
|
43
|
+
* `parse`) for an alias cycle, a forward reference, or a block alias used
|
|
44
|
+
* inside a `where` expression — the same conditions the parser rejects, so a
|
|
45
|
+
* parsed query never throws here. */
|
|
46
|
+
export function resolveAliases<T extends Query | Subquery>(node: T): T {
|
|
47
|
+
return resolveBody(node, failPlain);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveBody<T extends Query | Subquery>(node: T, fail: Fail): T {
|
|
51
|
+
// `where` is validated and substituted against the raw select first: its
|
|
52
|
+
// cycle report follows the where's own path (`c → a → b → c`), as pinned.
|
|
53
|
+
const where = node.where === null ? null : resolveWhere(node.select, node.where, fail);
|
|
54
|
+
const where2 = where === null ? null : resolveNestedWhere(where, fail);
|
|
55
|
+
const select = resolveSelect(node.select, fail);
|
|
56
|
+
const follow = node.follow === null ? null : resolveFollow(node.follow, fail);
|
|
57
|
+
const from = mapSame(node.from, (e) => resolveExpr(e, fail));
|
|
58
|
+
const orderBy = node.orderBy === null ? null : mapSame(node.orderBy, (o) => {
|
|
59
|
+
const expr = resolveExpr(o.expr, fail);
|
|
60
|
+
return expr === o.expr ? o : { ...o, expr };
|
|
61
|
+
});
|
|
62
|
+
const limit = node.limit === null ? null : resolveExpr(node.limit, fail);
|
|
63
|
+
const offset = node.offset === null ? null : resolveExpr(node.offset, fail);
|
|
64
|
+
// `frontier` (0.20) is not rewritten against the select: a destination
|
|
65
|
+
// block's reads the occurrence, a body's reads the seed row — not a projected
|
|
66
|
+
// record (like `order by`).
|
|
67
|
+
const frontier = node.frontier === null ? null : resolveExpr(node.frontier, fail);
|
|
68
|
+
const source = node.kind === "query" ? resolveExpr(node.source, fail) : null;
|
|
69
|
+
const same = where2 === node.where && select === node.select && follow === node.follow && from === node.from
|
|
70
|
+
&& orderBy === node.orderBy && limit === node.limit && offset === node.offset && frontier === node.frontier
|
|
71
|
+
&& (node.kind !== "query" || source === node.source);
|
|
72
|
+
if (same) return node;
|
|
73
|
+
const out = { ...node, where: where2, select, follow, frontier, from, orderBy, limit, offset };
|
|
74
|
+
if (node.kind === "query") (out as Query).source = source!;
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveOp(op: OpNode, fail: Fail): OpNode {
|
|
79
|
+
const receiver = resolveExpr(op.receiver, fail);
|
|
80
|
+
const sub = resolveBody(op.sub, fail);
|
|
81
|
+
return sub === op.sub && receiver === op.receiver ? op : { ...op, receiver, sub };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveFollow(f: Follow, fail: Fail): Follow {
|
|
85
|
+
const destinations = mapSame(f.destinations, (d: FollowDestination) => (d.kind === "op" ? resolveOp(d, fail) : resolveExpr(d, fail)));
|
|
86
|
+
return destinations === f.destinations ? f : { ...f, destinations };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Nested blocks inside a where tree are their own scopes: resolve each against
|
|
90
|
+
// its own select (a scalar leaf may hold a value-position directive).
|
|
91
|
+
function resolveNestedWhere(w: Where, fail: Fail): Where {
|
|
92
|
+
switch (w.kind) {
|
|
93
|
+
case "and": case "or": {
|
|
94
|
+
const parts = mapSame(w.parts, (p) => resolveNestedWhere(p, fail));
|
|
95
|
+
return parts === w.parts ? w : { ...w, parts };
|
|
96
|
+
}
|
|
97
|
+
case "not": {
|
|
98
|
+
const expr = resolveNestedWhere(w.expr, fail);
|
|
99
|
+
return expr === w.expr ? w : { ...w, expr };
|
|
100
|
+
}
|
|
101
|
+
case "scalar": {
|
|
102
|
+
const expr = resolveExpr(w.expr, fail);
|
|
103
|
+
return expr === w.expr ? w : { ...w, expr };
|
|
104
|
+
}
|
|
105
|
+
case "op": return resolveOp(w, fail);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Resolve the nested blocks (value-position directives) inside an expression,
|
|
110
|
+
* each against its own select; the expression's own idents are left alone. */
|
|
111
|
+
function resolveExpr(e: Expr, fail: Fail): Expr {
|
|
112
|
+
return mapExpr(e, (x) => (x.kind === "op" ? resolveOp(x, fail) : null));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Rebuild `e` bottom-up, letting `f` replace a node (return `null` to keep it;
|
|
116
|
+
* a replaced node's children are not visited — `f` owns them). Untouched nodes
|
|
117
|
+
* are returned by reference. */
|
|
118
|
+
function mapExpr(e: Expr, f: (e: Expr) => Expr | null): Expr {
|
|
119
|
+
const hit = f(e);
|
|
120
|
+
if (hit !== null) return hit;
|
|
121
|
+
const go = (x: Expr): Expr => mapExpr(x, f);
|
|
122
|
+
switch (e.kind) {
|
|
123
|
+
case "member": { const recv = go(e.recv); return recv === e.recv ? e : { ...e, recv }; }
|
|
124
|
+
case "call": {
|
|
125
|
+
const recv = e.recv ? go(e.recv) : null;
|
|
126
|
+
const args = mapSame(e.args, go);
|
|
127
|
+
return recv === e.recv && args === e.args ? e : { ...e, recv, args };
|
|
128
|
+
}
|
|
129
|
+
case "unary": case "required": { const expr = go(e.expr); return expr === e.expr ? e : { ...e, expr }; }
|
|
130
|
+
case "binary": case "logical": case "in": {
|
|
131
|
+
const left = go(e.left);
|
|
132
|
+
const right = go(e.right);
|
|
133
|
+
return left === e.left && right === e.right ? e : { ...e, left, right };
|
|
134
|
+
}
|
|
135
|
+
case "range": {
|
|
136
|
+
const lo = e.lo ? go(e.lo) : null;
|
|
137
|
+
const hi = e.hi ? go(e.hi) : null;
|
|
138
|
+
return lo === e.lo && hi === e.hi ? e : { ...e, lo, hi };
|
|
139
|
+
}
|
|
140
|
+
case "tuple": {
|
|
141
|
+
const items = mapSame(e.items, go);
|
|
142
|
+
return items === e.items ? e : { ...e, items };
|
|
143
|
+
}
|
|
144
|
+
case "op": { // the receiver is read in this scope; the block is its own scope
|
|
145
|
+
const receiver = go(e.receiver);
|
|
146
|
+
return receiver === e.receiver ? e : { ...e, receiver };
|
|
147
|
+
}
|
|
148
|
+
default: return e; // lit, ident, binding, outer
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// An item other items may name: a `collect` item, or a named non-lift field.
|
|
153
|
+
function aliasName(it: SelectItem): string | null {
|
|
154
|
+
if (it.kind === "collect") return it.name === "" ? null : it.name;
|
|
155
|
+
return it.lift === 0 && it.name !== "" ? it.name : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The alias names an item's own expression (the receiver, for a block) mentions
|
|
159
|
+
// as bare identifiers, excluding its own name (the row field).
|
|
160
|
+
function aliasRefs(it: SelectItem, index: ReadonlyMap<string, number>): string[] {
|
|
161
|
+
const own = aliasName(it);
|
|
162
|
+
const out: string[] = [];
|
|
163
|
+
const visit = (e: Expr): void => {
|
|
164
|
+
mapExpr(e, (x) => {
|
|
165
|
+
if (x.kind === "ident" && x.name !== own && index.has(x.name) && !out.includes(x.name)) out.push(x.name);
|
|
166
|
+
return null;
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
visit(it.kind === "collect" ? it.op.receiver : it.expr);
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Resolve one body's `select` items left to right (this body only; each item's
|
|
174
|
+
* nested blocks are resolved against their own select). An item's bare
|
|
175
|
+
* identifier that names an item to its left is replaced by that item's resolved
|
|
176
|
+
* value — a block alias inlines as a value-position directive — and the item's
|
|
177
|
+
* own name is the row field. A reference to an item to its right fails: as a
|
|
178
|
+
* cycle when that item refers back, else as a forward reference. Exported for
|
|
179
|
+
* the parser's validation pass. */
|
|
180
|
+
export function resolveSelect(select: readonly SelectItem[], fail: Fail = failPlain): SelectItem[] {
|
|
181
|
+
const index = new Map<string, number>();
|
|
182
|
+
select.forEach((it, i) => { const n = aliasName(it); if (n !== null) index.set(n, i); });
|
|
183
|
+
if (index.size === 0) {
|
|
184
|
+
return mapSame(select as SelectItem[], (it) => resolveItemBlocks(it, fail));
|
|
185
|
+
}
|
|
186
|
+
const resolved = new Map<string, Expr>();
|
|
187
|
+
let out: SelectItem[] | null = null;
|
|
188
|
+
for (let i = 0; i < select.length; i++) {
|
|
189
|
+
const it = select[i]!;
|
|
190
|
+
const own = aliasName(it);
|
|
191
|
+
const subst = (e: Expr): Expr => mapExpr(e, (x) => {
|
|
192
|
+
if (x.kind !== "ident") return null;
|
|
193
|
+
if (x.name === own) return x;
|
|
194
|
+
const r = resolved.get(x.name);
|
|
195
|
+
if (r) return r;
|
|
196
|
+
const j = index.get(x.name);
|
|
197
|
+
if (j === undefined || j <= i) return null;
|
|
198
|
+
const back = own === null ? null : cyclePath(select, index, j, own);
|
|
199
|
+
if (back) fail(`select aliases form a cycle: ${[own, ...back].join(" → ")} — an alias cannot depend on itself`);
|
|
200
|
+
fail(`select alias '${x.name}' is used before it is defined — a select item may reference only the items to its left`);
|
|
201
|
+
});
|
|
202
|
+
let r: SelectItem;
|
|
203
|
+
if (it.kind === "collect") {
|
|
204
|
+
const receiver = subst(it.op.receiver);
|
|
205
|
+
const op = resolveOp(receiver === it.op.receiver ? it.op : { ...it.op, receiver }, fail);
|
|
206
|
+
r = op === it.op ? it : { ...it, op };
|
|
207
|
+
if (own !== null) resolved.set(own, op);
|
|
208
|
+
} else {
|
|
209
|
+
const expr = resolveExpr(subst(it.expr), fail);
|
|
210
|
+
r = expr === it.expr ? it : expr.kind === "op"
|
|
211
|
+
? { kind: "collect", span: it.span, name: it.name, op: expr }
|
|
212
|
+
: { ...it, expr };
|
|
213
|
+
if (own !== null) resolved.set(own, expr);
|
|
214
|
+
}
|
|
215
|
+
if (r !== it) { out ??= select.slice(); out[i] = r; }
|
|
216
|
+
}
|
|
217
|
+
return out ?? (select as SelectItem[]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The nested blocks of an item, resolved against their own select.
|
|
221
|
+
function resolveItemBlocks(it: SelectItem, fail: Fail): SelectItem {
|
|
222
|
+
if (it.kind === "collect") {
|
|
223
|
+
const op = resolveOp(it.op, fail);
|
|
224
|
+
return op === it.op ? it : { ...it, op };
|
|
225
|
+
}
|
|
226
|
+
const expr = resolveExpr(it.expr, fail);
|
|
227
|
+
return expr === it.expr ? it : { ...it, expr };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The alias path from item `from` back to the alias `to`, if the raw reference
|
|
231
|
+
// graph has one (`[b, a]` for `a: b, b: a` from `b` back to `a`).
|
|
232
|
+
function cyclePath(select: readonly SelectItem[], index: ReadonlyMap<string, number>, from: number, to: string): string[] | null {
|
|
233
|
+
const seen = new Set<number>();
|
|
234
|
+
const go = (i: number, path: string[]): string[] | null => {
|
|
235
|
+
if (seen.has(i)) return null;
|
|
236
|
+
seen.add(i);
|
|
237
|
+
const it = select[i]!;
|
|
238
|
+
const name = aliasName(it)!;
|
|
239
|
+
for (const ref of aliasRefs(it, index)) {
|
|
240
|
+
if (ref === to) return [...path, name, to];
|
|
241
|
+
const hit = go(index.get(ref)!, [...path, name]);
|
|
242
|
+
if (hit) return hit;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
};
|
|
246
|
+
return go(from, []);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Resolve one body's `where` against its `select` (this body only; nested
|
|
250
|
+
* blocks are left as they are). Exported for the parser's validation pass. */
|
|
251
|
+
export function resolveWhere(select: readonly SelectItem[], where: Where, fail: Fail = failPlain): Where {
|
|
252
|
+
if (select.length === 0) return where;
|
|
253
|
+
const aliases = new Map<string, SelectItem>();
|
|
254
|
+
for (const it of select) {
|
|
255
|
+
const n = aliasName(it);
|
|
256
|
+
if (n !== null) aliases.set(n, it);
|
|
257
|
+
}
|
|
258
|
+
if (aliases.size === 0) return where;
|
|
259
|
+
const resolving: string[] = [];
|
|
260
|
+
const subst = (e: Expr): Expr => mapExpr(e, (x) => {
|
|
261
|
+
if (x.kind !== "ident") return null;
|
|
262
|
+
const a = aliases.get(x.name);
|
|
263
|
+
if (!a) return null;
|
|
264
|
+
if (resolving[resolving.length - 1] === x.name) return x; // its own name inside its own expression: the row field
|
|
265
|
+
if (resolving.includes(x.name)) {
|
|
266
|
+
const cycle = [...resolving.slice(resolving.indexOf(x.name)), x.name].join(" → ");
|
|
267
|
+
fail(`select aliases form a cycle: ${cycle} — an alias used in \`where\` cannot depend on itself`);
|
|
268
|
+
}
|
|
269
|
+
if (a.kind === "collect") {
|
|
270
|
+
fail(`select alias '${x.name}' is a ${a.op.op} { … } block — in \`where\` it can only stand alone as a non-empty test, not inside an expression`);
|
|
271
|
+
}
|
|
272
|
+
resolving.push(x.name);
|
|
273
|
+
const out = subst(a.expr);
|
|
274
|
+
resolving.pop();
|
|
275
|
+
return out;
|
|
276
|
+
});
|
|
277
|
+
const walk = (w: Where): Where => {
|
|
278
|
+
switch (w.kind) {
|
|
279
|
+
case "and": case "or": { const parts = mapSame(w.parts, walk); return parts === w.parts ? w : { ...w, parts }; }
|
|
280
|
+
case "not": { const expr = walk(w.expr); return expr === w.expr ? w : { ...w, expr }; }
|
|
281
|
+
case "scalar": {
|
|
282
|
+
if (w.expr.kind === "ident") {
|
|
283
|
+
const a = aliases.get(w.expr.name);
|
|
284
|
+
if (a?.kind === "collect") return a.op; // a collection in predicate position: non-empty
|
|
285
|
+
}
|
|
286
|
+
const expr = subst(w.expr);
|
|
287
|
+
return expr === w.expr ? w : { ...w, expr };
|
|
288
|
+
}
|
|
289
|
+
case "op": { // the receiver is read in this scope; the block is its own scope
|
|
290
|
+
const receiver = subst(w.receiver);
|
|
291
|
+
return receiver === w.receiver ? w : { ...w, receiver };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
return walk(where);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// `map` that returns the input array itself when no element changed.
|
|
299
|
+
function mapSame<T>(xs: readonly T[], f: (x: T) => T): T[] {
|
|
300
|
+
let out: T[] | null = null;
|
|
301
|
+
for (let i = 0; i < xs.length; i++) {
|
|
302
|
+
const y = f(xs[i]!);
|
|
303
|
+
if (y !== xs[i]) {
|
|
304
|
+
out ??= xs.slice();
|
|
305
|
+
out[i] = y;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return out ?? (xs as T[]);
|
|
309
|
+
}
|
package/src/version.ts
ADDED