@omgbase/oqx 0.18.0 → 0.21.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 +30 -0
- package/README.md +177 -64
- package/dist/ast.d.ts +28 -5
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +11 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -3
- package/dist/build.js.map +1 -1
- package/dist/context.d.ts +19 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -9
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +12 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +263 -84
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +3 -3
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +79 -2
- 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 +18 -15
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +250 -118
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +47 -29
- package/dist/print.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +15 -8
- package/dist/resolve.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +5 -3
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +34 -11
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +33 -6
- package/src/build.ts +12 -8
- package/src/context.ts +22 -11
- package/src/engine.ts +258 -91
- package/src/index.ts +2 -1
- package/src/lexer.ts +76 -6
- package/src/optimize/analysis.ts +17 -12
- package/src/parser.ts +227 -101
- package/src/print.ts +44 -16
- package/src/resolve.ts +15 -7
- package/src/version.ts +1 -1
- package/src/walk.ts +39 -14
package/src/print.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Single spaces, double-quoted strings with GRAMMAR §1 escapes, the minimal
|
|
3
3
|
// parentheses the precedence table (GRAMMAR §4) needs, `select` written at the
|
|
4
4
|
// top level, the connectives as the words `and` / `or` (since 0.18; `!` stays
|
|
5
|
-
// `!`), every
|
|
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
|
|
6
7
|
// enforce over every fixture query:
|
|
7
8
|
//
|
|
8
9
|
// strip(parse(print(parse(q)))) ≡ strip(parse(q))
|
|
@@ -65,7 +66,7 @@ const OR = 1, AND = 2, CMP = 3, RANGE = 4, ADD = 5, MUL = 6, UNARY = 7, POSTFIX
|
|
|
65
66
|
|
|
66
67
|
function prec(e: Expr): number {
|
|
67
68
|
switch (e.kind) {
|
|
68
|
-
case "lit": case "ident": case "outer": case "binding": return PRIMARY;
|
|
69
|
+
case "lit": case "ident": case "outer": case "binding": case "tuple": case "template": return PRIMARY;
|
|
69
70
|
case "member": case "call": case "required": case "op": return POSTFIX;
|
|
70
71
|
case "unary": return UNARY;
|
|
71
72
|
case "binary": return e.op === "+" || e.op === "-" ? ADD : e.op === "*" || e.op === "/" || e.op === "%" ? MUL : CMP;
|
|
@@ -113,14 +114,18 @@ function query(q: Query): Out {
|
|
|
113
114
|
return cat(expr(q.source), ` ${q.consumer}${q.distinct ? " distinct" : ""} `, braces(body(q)));
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
type Body = Pick<Subquery, "select" | "values" | "from" | "where" | "follow" | "orderBy" | "limit" | "offset"
|
|
117
|
+
type Body = Pick<Subquery, "select" | "values" | "from" | "where" | "follow" | "orderBy" | "limit" | "offset">
|
|
118
|
+
& Partial<Pick<Subquery, "frontier" | "depth">>;
|
|
117
119
|
|
|
118
120
|
// The clauses after `from` (a query) or after the projection (a block): the
|
|
119
|
-
// fixed clause order, each present clause once.
|
|
121
|
+
// fixed clause order, each present clause once. `frontier`/`depth` exist only
|
|
122
|
+
// on a follow destination block's body (a `Query` has neither).
|
|
120
123
|
function tail(b: Omit<Body, "select" | "values">): Out[] {
|
|
121
124
|
const out: Out[] = [];
|
|
122
125
|
for (const e of b.from) out.push(cat("from ", expr(e)));
|
|
123
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)}`]);
|
|
124
129
|
if (b.follow) out.push(follow(b.follow));
|
|
125
130
|
if (b.orderBy && b.orderBy.length > 0) out.push(cat("order by ", join(b.orderBy.map(order), ", ")));
|
|
126
131
|
if (b.limit) out.push(cat("limit ", expr(b.limit)));
|
|
@@ -170,12 +175,7 @@ function op(o: OpNode): Out {
|
|
|
170
175
|
|
|
171
176
|
function follow(f: Follow): Out {
|
|
172
177
|
const dests = join(f.destinations.map((d) => (d.kind === "op" ? op(d) : expr(d))), ", ");
|
|
173
|
-
|
|
174
|
-
if (f.where) opts.push(cat("where ", expr(f.where)));
|
|
175
|
-
if (f.frontier) opts.push(cat("frontier ", expr(f.frontier)));
|
|
176
|
-
if (f.depth !== null) opts.push([`depth ${f.depth}`]);
|
|
177
|
-
if (f.by) opts.push(cat("by ", expr(f.by)));
|
|
178
|
-
return cat(`follow${f.distinct ? " distinct" : ""} `, dests, opts.length > 0 ? cat(" { ", join(opts, " "), " }") : "");
|
|
178
|
+
return cat(`follow${f.distinct ? " distinct" : ""} `, dests);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// ---- where ------------------------------------------------------------------
|
|
@@ -229,6 +229,12 @@ function expr(e: Expr): Out {
|
|
|
229
229
|
case "in": return cat(operand(e.left, CMP + 1), " in ", operand(e.right, CMP + 1));
|
|
230
230
|
case "range":
|
|
231
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
|
+
// A template literal prints as one: its text with the GRAMMAR §1 escapes
|
|
235
|
+
// (`` \` ``, `\\`, `\$` before a `{`, and the control characters), each
|
|
236
|
+
// interpolation as `${…}` around the full expression.
|
|
237
|
+
case "template": return cat("`", ...e.parts.map((p) => (typeof p === "string" ? templateText(p) : cat("${", expr(p), "}"))), "`");
|
|
232
238
|
}
|
|
233
239
|
}
|
|
234
240
|
|
|
@@ -262,13 +268,35 @@ function quote(s: string): string {
|
|
|
262
268
|
for (const ch of s) {
|
|
263
269
|
switch (ch) {
|
|
264
270
|
case '"': out += '\\"'; break;
|
|
265
|
-
|
|
266
|
-
case "\n": out += "\\n"; break;
|
|
267
|
-
case "\t": out += "\\t"; break;
|
|
268
|
-
case "\r": out += "\\r"; break;
|
|
269
|
-
case "\0": out += "\\0"; break;
|
|
270
|
-
default: out += ch;
|
|
271
|
+
default: out += escapeChar(ch);
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
return out + '"';
|
|
274
275
|
}
|
|
276
|
+
|
|
277
|
+
/** The text of a template literal part, escaped so it re-lexes to itself: the
|
|
278
|
+
* backtick and the backslash, the control characters as in a string, and a `$`
|
|
279
|
+
* that a `{` follows (which would otherwise open an interpolation). */
|
|
280
|
+
function templateText(s: string): string {
|
|
281
|
+
const cps = Array.from(s);
|
|
282
|
+
let out = "";
|
|
283
|
+
for (let i = 0; i < cps.length; i++) {
|
|
284
|
+
const ch = cps[i]!;
|
|
285
|
+
if (ch === "`") out += "\\`";
|
|
286
|
+
else if (ch === "$" && cps[i + 1] === "{") out += "\\$";
|
|
287
|
+
else out += escapeChar(ch);
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The escapes a string literal and a template literal share (GRAMMAR §1). */
|
|
293
|
+
function escapeChar(ch: string): string {
|
|
294
|
+
switch (ch) {
|
|
295
|
+
case "\\": return "\\\\";
|
|
296
|
+
case "\n": return "\\n";
|
|
297
|
+
case "\t": return "\\t";
|
|
298
|
+
case "\r": return "\\r";
|
|
299
|
+
case "\0": return "\\0";
|
|
300
|
+
default: return ch;
|
|
301
|
+
}
|
|
302
|
+
}
|
package/src/resolve.ts
CHANGED
|
@@ -61,12 +61,16 @@ function resolveBody<T extends Query | Subquery>(node: T, fail: Fail): T {
|
|
|
61
61
|
});
|
|
62
62
|
const limit = node.limit === null ? null : resolveExpr(node.limit, fail);
|
|
63
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);
|
|
64
68
|
const source = node.kind === "query" ? resolveExpr(node.source, fail) : null;
|
|
65
69
|
const same = where2 === node.where && select === node.select && follow === node.follow && from === node.from
|
|
66
|
-
&& orderBy === node.orderBy && limit === node.limit && offset === node.offset
|
|
70
|
+
&& orderBy === node.orderBy && limit === node.limit && offset === node.offset && frontier === node.frontier
|
|
67
71
|
&& (node.kind !== "query" || source === node.source);
|
|
68
72
|
if (same) return node;
|
|
69
|
-
const out = { ...node, where: where2, select, follow, from, orderBy, limit, offset };
|
|
73
|
+
const out = { ...node, where: where2, select, follow, frontier, from, orderBy, limit, offset };
|
|
70
74
|
if (node.kind === "query") (out as Query).source = source!;
|
|
71
75
|
return out;
|
|
72
76
|
}
|
|
@@ -79,11 +83,7 @@ function resolveOp(op: OpNode, fail: Fail): OpNode {
|
|
|
79
83
|
|
|
80
84
|
function resolveFollow(f: Follow, fail: Fail): Follow {
|
|
81
85
|
const destinations = mapSame(f.destinations, (d: FollowDestination) => (d.kind === "op" ? resolveOp(d, fail) : resolveExpr(d, fail)));
|
|
82
|
-
|
|
83
|
-
const frontier = f.frontier === null ? null : resolveExpr(f.frontier, fail);
|
|
84
|
-
const by = f.by === null ? null : resolveExpr(f.by, fail);
|
|
85
|
-
if (destinations === f.destinations && where === f.where && frontier === f.frontier && by === f.by) return f;
|
|
86
|
-
return { ...f, destinations, where, frontier, by };
|
|
86
|
+
return destinations === f.destinations ? f : { ...f, destinations };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Nested blocks inside a where tree are their own scopes: resolve each against
|
|
@@ -137,6 +137,14 @@ function mapExpr(e: Expr, f: (e: Expr) => Expr | null): Expr {
|
|
|
137
137
|
const hi = e.hi ? go(e.hi) : null;
|
|
138
138
|
return lo === e.lo && hi === e.hi ? e : { ...e, lo, hi };
|
|
139
139
|
}
|
|
140
|
+
case "tuple": {
|
|
141
|
+
const items = mapSame(e.items, go);
|
|
142
|
+
return items === e.items ? e : { ...e, items };
|
|
143
|
+
}
|
|
144
|
+
case "template": { // the text parts are strings; only the interpolations are expressions
|
|
145
|
+
const parts = mapSame(e.parts, (p) => (typeof p === "string" ? p : go(p)));
|
|
146
|
+
return parts === e.parts ? e : { ...e, parts };
|
|
147
|
+
}
|
|
140
148
|
case "op": { // the receiver is read in this scope; the block is its own scope
|
|
141
149
|
const receiver = go(e.receiver);
|
|
142
150
|
return receiver === e.receiver ? e : { ...e, receiver };
|
package/src/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** The OQX language version (`spec/oqx/VERSION`, `major.minor`) this implementation
|
|
2
2
|
* conforms to — distinct from the package's own semver, whose `major.minor` it
|
|
3
3
|
* equals. Stamped on every `toJSON` document. */
|
|
4
|
-
export const LANGUAGE_VERSION = "0.
|
|
4
|
+
export const LANGUAGE_VERSION = "0.21";
|
package/src/walk.ts
CHANGED
|
@@ -14,13 +14,15 @@ import { LANGUAGE_VERSION } from "./version.ts";
|
|
|
14
14
|
* The root node has no clause (`null`). */
|
|
15
15
|
export type Clause =
|
|
16
16
|
| "source" | "from" | "where" | "select" | "orderBy" | "limit" | "offset"
|
|
17
|
-
| "follow" | "follow.destination" | "follow.
|
|
17
|
+
| "follow" | "follow.destination" | "follow.frontier";
|
|
18
18
|
|
|
19
19
|
/** Where a visited node sits. `depth` is the scope depth at which the node is
|
|
20
20
|
* evaluated (0 = the root scope: a top-level `source`, `limit`, `offset`; 1 =
|
|
21
21
|
* a top-level row: its `where`, `select`, `order by`, `from` re-projections and
|
|
22
|
-
* `follow`; a block's rows are one deeper than the block's receiver
|
|
23
|
-
* `where`
|
|
22
|
+
* `follow`; a block's rows are one deeper than the block's receiver — a follow
|
|
23
|
+
* destination block's `where` therefore one deeper than the frontier row —
|
|
24
|
+
* and a destination block's `frontier` one level out of its rows, under the
|
|
25
|
+
* walk's enclosing scope). */
|
|
24
26
|
export interface VisitContext {
|
|
25
27
|
/** The ancestors of the node, root first (empty for the root). */
|
|
26
28
|
readonly path: readonly AstNode[];
|
|
@@ -52,6 +54,8 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
52
54
|
{ key: "source", clause: "source" },
|
|
53
55
|
{ key: "from", clause: "from", depth: 1 },
|
|
54
56
|
{ key: "where", clause: "where", depth: 1 },
|
|
57
|
+
// The body's `frontier` (0.20) reads each seed in the row scope, like `where`.
|
|
58
|
+
{ key: "frontier", clause: "follow.frontier", depth: 1 },
|
|
55
59
|
{ key: "follow", clause: "follow", depth: 1 },
|
|
56
60
|
{ key: "orderBy", clause: "orderBy", depth: 1 },
|
|
57
61
|
{ key: "limit", clause: "limit" },
|
|
@@ -61,18 +65,18 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
61
65
|
{ key: "select", clause: "select" },
|
|
62
66
|
{ key: "from", clause: "from" },
|
|
63
67
|
{ key: "where", clause: "where" },
|
|
68
|
+
// A destination block's `frontier` reads the occurrence under the walk's
|
|
69
|
+
// enclosing scope — one level OUT of the block's rows (SEMANTICS §20); any
|
|
70
|
+
// other block's reads its seeds in the row scope, like `where`
|
|
71
|
+
// (`childDepth` tells the two apart by the block's position).
|
|
72
|
+
{ key: "frontier", clause: "follow.frontier", depth: -1 },
|
|
64
73
|
{ key: "follow", clause: "follow" },
|
|
65
74
|
{ key: "orderBy", clause: "orderBy" },
|
|
66
75
|
{ key: "limit", clause: "limit" },
|
|
67
76
|
{ key: "offset", clause: "offset" },
|
|
68
77
|
],
|
|
69
78
|
op: [{ key: "receiver" }, { key: "sub", depth: 1 }],
|
|
70
|
-
follow: [
|
|
71
|
-
{ key: "destinations", clause: "follow.destination" },
|
|
72
|
-
{ key: "where", clause: "follow.where", depth: 1 },
|
|
73
|
-
{ key: "frontier", clause: "follow.frontier" },
|
|
74
|
-
{ key: "by", clause: "follow.by" },
|
|
75
|
-
],
|
|
79
|
+
follow: [{ key: "destinations", clause: "follow.destination" }],
|
|
76
80
|
order: [{ key: "expr" }],
|
|
77
81
|
field: [{ key: "expr" }],
|
|
78
82
|
collect: [{ key: "op" }],
|
|
@@ -92,14 +96,35 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
92
96
|
logical: [{ key: "left" }, { key: "right" }],
|
|
93
97
|
in: [{ key: "left" }, { key: "right" }],
|
|
94
98
|
range: [{ key: "lo" }, { key: "hi" }],
|
|
99
|
+
tuple: [{ key: "items" }],
|
|
100
|
+
// The interpolations; the text parts between them are strings, not nodes.
|
|
101
|
+
template: [{ key: "parts" }],
|
|
95
102
|
};
|
|
96
103
|
|
|
97
|
-
|
|
104
|
+
/** A child slot's value: a node, a list of nodes (a template's `parts` also
|
|
105
|
+
* holds its text strings, which the walks skip), or `null`. */
|
|
106
|
+
type Slot = AstNode | (AstNode | string)[] | null;
|
|
98
107
|
|
|
99
108
|
function slot(node: AstNode, key: string): Slot {
|
|
100
109
|
return (node as unknown as Record<string, Slot>)[key] ?? null;
|
|
101
110
|
}
|
|
102
111
|
|
|
112
|
+
/** A block is a follow destination block when its `op` is a direct destination
|
|
113
|
+
* of a `follow` node. `path` is the ancestors with the block itself last. */
|
|
114
|
+
function isDestinationBlock(path: readonly AstNode[]): boolean {
|
|
115
|
+
const op = path[path.length - 2];
|
|
116
|
+
const owner = path[path.length - 3];
|
|
117
|
+
return op?.kind === "op" && owner?.kind === "follow";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** How much deeper a child's scope is: the table's value, except a block's
|
|
121
|
+
* `frontier`, which is one level OUT only for a destination block (the walk's
|
|
122
|
+
* enclosing scope) and the row scope itself for a body with its own `follow`. */
|
|
123
|
+
function childDepth(node: AstNode, child: ChildKey, path: readonly AstNode[]): number {
|
|
124
|
+
if (node.kind === "subquery" && child.key === "frontier" && !isDestinationBlock(path)) return 0;
|
|
125
|
+
return child.depth ?? 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
/** Walk a tree depth-first in canonical source order. */
|
|
104
129
|
export function visit(root: AstNode, visitor: Visitor): void {
|
|
105
130
|
const path: AstNode[] = [];
|
|
@@ -112,8 +137,8 @@ export function visit(root: AstNode, visitor: Visitor): void {
|
|
|
112
137
|
const v = slot(node, child.key);
|
|
113
138
|
if (v === null) continue;
|
|
114
139
|
const c = child.clause ?? clause;
|
|
115
|
-
const d = depth + (child
|
|
116
|
-
if (Array.isArray(v)) for (const x of v) go(x, c, d);
|
|
140
|
+
const d = depth + childDepth(node, child, path);
|
|
141
|
+
if (Array.isArray(v)) { for (const x of v) if (typeof x !== "string") go(x, c, d); }
|
|
117
142
|
else go(v, c, d);
|
|
118
143
|
}
|
|
119
144
|
path.pop();
|
|
@@ -137,11 +162,11 @@ export function transform<T extends Query | Subquery>(root: T, f: (expr: Expr, c
|
|
|
137
162
|
const v = slot(node, child.key);
|
|
138
163
|
if (v === null) continue;
|
|
139
164
|
const c = child.clause ?? clause;
|
|
140
|
-
const d = depth + (child
|
|
165
|
+
const d = depth + childDepth(node, child, path);
|
|
141
166
|
let next: Slot;
|
|
142
167
|
if (Array.isArray(v)) {
|
|
143
168
|
let changed = false;
|
|
144
|
-
const mapped = v.map((x) => { const y = go(x, c, d); if (y !== x) changed = true; return y; });
|
|
169
|
+
const mapped = v.map((x) => { if (typeof x === "string") return x; const y = go(x, c, d); if (y !== x) changed = true; return y; });
|
|
145
170
|
next = changed ? mapped : v;
|
|
146
171
|
} else next = go(v, c, d);
|
|
147
172
|
if (next !== v) out = { ...out, [child.key]: next } as AstNode;
|