@omgbase/oqx 0.23.0 → 0.25.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 +12 -0
- package/README.md +71 -2
- package/dist/ast.d.ts +53 -1
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +7 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +9 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +11 -2
- package/dist/build.js.map +1 -1
- package/dist/engine.d.ts +4 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +188 -13
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +24 -1
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +8 -1
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +3 -0
- package/dist/optimize/ir.js.map +1 -1
- package/dist/optimize/rules.d.ts.map +1 -1
- package/dist/optimize/rules.js +5 -3
- package/dist/optimize/rules.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +467 -22
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +25 -0
- package/dist/print.js.map +1 -1
- package/dist/references.d.ts +6 -4
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +104 -4
- package/dist/references.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +27 -1
- 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 +1 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +63 -2
- package/src/build.ts +13 -4
- package/src/engine.ts +186 -13
- package/src/index.ts +1 -1
- package/src/optimize/analysis.ts +18 -1
- package/src/optimize/ir.ts +11 -1
- package/src/optimize/rules.ts +5 -3
- package/src/parser.ts +387 -22
- package/src/print.ts +27 -3
- package/src/references.ts +95 -8
- package/src/resolve.ts +29 -2
- package/src/version.ts +1 -1
- package/src/walk.ts +18 -1
package/src/references.ts
CHANGED
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
// (`target.contains("x")` references `target`); a name a nested block
|
|
26
26
|
// LIFTS onto a scope (`^open: value`, SEMANTICS §19) is a binding of that
|
|
27
27
|
// scope, so reading it there (`select open`, `^open` from deeper) is not a
|
|
28
|
-
// reference either
|
|
28
|
+
// reference either — and neither is a name a `flatten` bound (0.25,
|
|
29
|
+
// SEMANTICS §25) in the clauses after it: a later `flatten`, `group by`,
|
|
30
|
+
// `having`, the select, `order by`, or `^name` from a block under them; in
|
|
31
|
+
// its own expression and before the clause (`where`, `frontier`, `follow`)
|
|
32
|
+
// the name is still the row's property;
|
|
29
33
|
// • a `select` alias is not a reference where the language resolves it
|
|
30
34
|
// (SEMANTICS §14): in the same body's `where`, and in the items to its
|
|
31
35
|
// right — a bare identifier naming such an alias, or a chain headed by it,
|
|
@@ -54,15 +58,17 @@
|
|
|
54
58
|
// are cut after the last step at its scope's depth. The empty list is the root
|
|
55
59
|
// scope.
|
|
56
60
|
|
|
57
|
-
import type { AstNode, Expr, Follow, Query, SelectItem, Span, Subquery } from "./ast.ts";
|
|
61
|
+
import type { AstNode, Expr, Flatten, Follow, GroupBy, Query, SelectItem, Span, Subquery } from "./ast.ts";
|
|
58
62
|
import type { Clause } from "./walk.ts";
|
|
59
63
|
import { visit } from "./walk.ts";
|
|
60
64
|
|
|
61
65
|
/** One step of a scope's row chain: an expression (a source, a `from`, a block
|
|
62
|
-
* receiver, a plain destination)
|
|
63
|
-
*
|
|
66
|
+
* receiver, a plain destination), a `follow` node (the walk's frontier rows), or
|
|
67
|
+
* — since 0.24 — a `groupBy` node (the body's groups: the rows at the same depth
|
|
68
|
+
* replaced by one key record per group), with the scope depth of the rows it
|
|
69
|
+
* produces. */
|
|
64
70
|
export interface ReferenceStep {
|
|
65
|
-
readonly expr: Expr | Follow;
|
|
71
|
+
readonly expr: Expr | Follow | GroupBy;
|
|
66
72
|
readonly depth: number;
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -120,6 +126,13 @@ export function references(root: Query | Subquery): Reference[] {
|
|
|
120
126
|
const levels = head.kind === "outer" ? head.levels : 0;
|
|
121
127
|
const depth = ctx.depth - levels;
|
|
122
128
|
if (isLiftBound(name, depth, ctx.path, lifts)) return false;
|
|
129
|
+
// A name a `flatten` bound (0.25) is an alias of the expression already
|
|
130
|
+
// reported in clause `flatten`, read in the clauses after it.
|
|
131
|
+
if (isFlattenBound(name, depth, ctx.path, node)) return false;
|
|
132
|
+
// At a group scope (0.24) a bare key name is the key — an alias of the
|
|
133
|
+
// expression reported in clause `groupBy` — and `rows` is the language's
|
|
134
|
+
// value; neither reads a row.
|
|
135
|
+
if (isGroupValue(name, depth, ctx.path, node)) return false;
|
|
123
136
|
const full = [name, ...path].join(".");
|
|
124
137
|
const steps = scopeSteps(ctx.path, node);
|
|
125
138
|
const scoped = levels === 0 ? steps : cutAt(steps, depth);
|
|
@@ -132,9 +145,9 @@ export function references(root: Query | Subquery): Reference[] {
|
|
|
132
145
|
|
|
133
146
|
/** Whether a bare `name` in this position is a `select` alias the language
|
|
134
147
|
* substitutes (so the name is the alias, not a row property): in the body's
|
|
135
|
-
* `where`, any alias; in a `select` item, an alias defined to its LEFT. */
|
|
148
|
+
* `where` or `having`, any alias; in a `select` item, an alias defined to its LEFT. */
|
|
136
149
|
function isResolvedAlias(name: string, path: readonly AstNode[], clause: Clause): boolean {
|
|
137
|
-
if (clause !== "where" && clause !== "select") return false;
|
|
150
|
+
if (clause !== "where" && clause !== "select" && clause !== "having") return false;
|
|
138
151
|
// The nearest body, and the item under it when we are inside its select.
|
|
139
152
|
for (let i = path.length - 1; i >= 0; i--) {
|
|
140
153
|
const a = path[i]!;
|
|
@@ -143,7 +156,7 @@ function isResolvedAlias(name: string, path: readonly AstNode[], clause: Clause)
|
|
|
143
156
|
a.select.forEach((it, j) => { const n = aliasName(it); if (n !== null) index.set(n, j); });
|
|
144
157
|
const j = index.get(name);
|
|
145
158
|
if (j === undefined) return false;
|
|
146
|
-
if (clause === "where") return true;
|
|
159
|
+
if (clause === "where" || clause === "having") return true;
|
|
147
160
|
const item = path[i + 1];
|
|
148
161
|
const at = item === undefined ? -1 : a.select.indexOf(item as SelectItem);
|
|
149
162
|
return at >= 0 && j < at;
|
|
@@ -185,6 +198,76 @@ function liftsLandingOn(body: Query | Subquery): ReadonlySet<string> {
|
|
|
185
198
|
return names;
|
|
186
199
|
}
|
|
187
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Whether `name`, read at scope `depth`, is bound there by a `flatten` of the
|
|
203
|
+
* body whose rows are at `depth` (SEMANTICS §25) — the position lies in a
|
|
204
|
+
* clause evaluated after that flatten: a later `flatten`, `group by`, `having`,
|
|
205
|
+
* the select, or `order by` (`where`, `frontier`, `follow` and the body's
|
|
206
|
+
* sources run before any flatten, and a flatten's own expression before itself).
|
|
207
|
+
*/
|
|
208
|
+
function isFlattenBound(name: string, depth: number, path: readonly AstNode[], node: AstNode): boolean {
|
|
209
|
+
const i = bodyIndexAt(path, depth);
|
|
210
|
+
if (i === -1) return false;
|
|
211
|
+
const body = path[i] as Query | Subquery;
|
|
212
|
+
if (body.flatten.length === 0) return false;
|
|
213
|
+
const child = path[i + 1] ?? node;
|
|
214
|
+
let upto: number;
|
|
215
|
+
if (body.select.includes(child as SelectItem) || child === body.groupBy || child === body.having || (body.orderBy?.includes(child as never) ?? false)) {
|
|
216
|
+
upto = body.flatten.length;
|
|
217
|
+
} else {
|
|
218
|
+
const k = body.flatten.indexOf(child as Flatten);
|
|
219
|
+
if (k === -1) return false;
|
|
220
|
+
upto = k;
|
|
221
|
+
}
|
|
222
|
+
for (let j = 0; j < upto; j++) if (body.flatten[j]!.name === name) return true;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Whether `name`, read at scope `depth`, is a value of a GROUP scope there
|
|
228
|
+
* (SEMANTICS §24): the body whose rows are at `depth` has a `group by`, the
|
|
229
|
+
* position is one of its post-group clauses — a select item, `having`, `order
|
|
230
|
+
* by` — not inside an aggregate's argument (which reads the group's rows), and
|
|
231
|
+
* the name is a key or `rows`.
|
|
232
|
+
*/
|
|
233
|
+
function isGroupValue(name: string, depth: number, path: readonly AstNode[], node: AstNode): boolean {
|
|
234
|
+
const i = bodyIndexAt(path, depth);
|
|
235
|
+
if (i === -1) return false;
|
|
236
|
+
const body = path[i] as Query | Subquery;
|
|
237
|
+
if (!atGroupScope(body, path, i, node)) return false;
|
|
238
|
+
return name === "rows" || body.groupBy!.keys.some((k) => k.name === name);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The index in `path` of the body whose rows are at scope `depth` (every nested body is one scope deeper), or -1. */
|
|
242
|
+
function bodyIndexAt(path: readonly AstNode[], depth: number): number {
|
|
243
|
+
if (depth < 1) return -1;
|
|
244
|
+
let seen = 0;
|
|
245
|
+
for (let i = 0; i < path.length; i++) {
|
|
246
|
+
const a = path[i]!;
|
|
247
|
+
if (a.kind !== "query" && a.kind !== "subquery") continue;
|
|
248
|
+
if (++seen === depth) return i;
|
|
249
|
+
}
|
|
250
|
+
return -1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Whether a position under `body` (at `path[i]`) reads the body's GROUP scope:
|
|
254
|
+
* the body is grouped, the position lies in a select item, `having` or `order
|
|
255
|
+
* by`, and no aggregate stands between the body and the position (an
|
|
256
|
+
* aggregate's argument reads the group's rows; a nested body is its own scope
|
|
257
|
+
* and ends the search). */
|
|
258
|
+
function atGroupScope(body: Query | Subquery, path: readonly AstNode[], i: number, node: AstNode): boolean {
|
|
259
|
+
if (body.groupBy === null) return false;
|
|
260
|
+
const child = path[i + 1] ?? node;
|
|
261
|
+
const post = body.select.includes(child as SelectItem) || child === body.having || (body.orderBy?.includes(child as never) ?? false);
|
|
262
|
+
if (!post) return false;
|
|
263
|
+
for (let j = i + 1; j < path.length; j++) {
|
|
264
|
+
const a = path[j]!;
|
|
265
|
+
if (a.kind === "query" || a.kind === "subquery") break;
|
|
266
|
+
if (a.kind === "aggregate") return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
188
271
|
/** The row chain of the scope a node is evaluated in, from its ancestors. */
|
|
189
272
|
function scopeSteps(path: readonly AstNode[], node: AstNode): ReferenceStep[] {
|
|
190
273
|
const steps: ReferenceStep[] = [];
|
|
@@ -198,12 +281,16 @@ function scopeSteps(path: readonly AstNode[], node: AstNode): ReferenceStep[] {
|
|
|
198
281
|
depth = 1;
|
|
199
282
|
steps.push({ expr: a.source, depth });
|
|
200
283
|
pushFroms(steps, a.from, next, depth);
|
|
284
|
+
// A grouped body's post-group clauses read its groups (0.24): the rows
|
|
285
|
+
// at this depth replaced by one key record per group.
|
|
286
|
+
if (atGroupScope(a, path, i, node)) steps.push({ expr: a.groupBy!, depth });
|
|
201
287
|
break;
|
|
202
288
|
}
|
|
203
289
|
case "subquery": {
|
|
204
290
|
// The receiver step was pushed by the enclosing `op`; this body's rows
|
|
205
291
|
// are one deeper than where that receiver was read.
|
|
206
292
|
pushFroms(steps, a.from, next, depth);
|
|
293
|
+
if (atGroupScope(a, path, i, node)) steps.push({ expr: a.groupBy!, depth });
|
|
207
294
|
break;
|
|
208
295
|
}
|
|
209
296
|
case "op": {
|
package/src/resolve.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// given. Untouched nodes are returned by reference, so a cached query keeps the
|
|
27
27
|
// identity of every block the rewrite did not reach.
|
|
28
28
|
|
|
29
|
-
import type { Expr, Follow, FollowDestination, OpNode, Query, SelectItem, Subquery, Where } from "./ast.ts";
|
|
29
|
+
import type { Expr, Flatten, Follow, FollowDestination, GroupBy, OpNode, Query, SelectItem, Subquery, Where } from "./ast.ts";
|
|
30
30
|
import { OqxError } from "./errors.ts";
|
|
31
31
|
|
|
32
32
|
/** How a resolution failure is reported: the parser raises its positioned parse
|
|
@@ -52,8 +52,22 @@ function resolveBody<T extends Query | Subquery>(node: T, fail: Fail): T {
|
|
|
52
52
|
// cycle report follows the where's own path (`c → a → b → c`), as pinned.
|
|
53
53
|
const where = node.where === null ? null : resolveWhere(node.select, node.where, fail);
|
|
54
54
|
const where2 = where === null ? null : resolveNestedWhere(where, fail);
|
|
55
|
+
// `having` (0.24) is the post-group `where`: rewritten against the select
|
|
56
|
+
// exactly as `where` is (`select n: count() … having n > 1`).
|
|
57
|
+
const having = node.having === null ? null : resolveWhere(node.select, node.having, fail);
|
|
58
|
+
const having2 = having === null ? null : resolveNestedWhere(having, fail);
|
|
55
59
|
const select = resolveSelect(node.select, fail);
|
|
56
60
|
const follow = node.follow === null ? null : resolveFollow(node.follow, fail);
|
|
61
|
+
// The keys are read on the rows, not rewritten (a key IS the name the post-group
|
|
62
|
+
// clauses read); their nested blocks resolve against their own select.
|
|
63
|
+
const groupBy = node.groupBy === null ? null : resolveGroupBy(node.groupBy, fail);
|
|
64
|
+
// A `flatten` expression (0.25) reads the row, not the projection — like
|
|
65
|
+
// `order by` and `frontier` it is not rewritten against the select; its
|
|
66
|
+
// nested blocks resolve against their own select.
|
|
67
|
+
const flatten = mapSame(node.flatten, (f: Flatten) => {
|
|
68
|
+
const expr = resolveExpr(f.expr, fail);
|
|
69
|
+
return expr === f.expr ? f : { ...f, expr };
|
|
70
|
+
});
|
|
57
71
|
const from = mapSame(node.from, (e) => resolveExpr(e, fail));
|
|
58
72
|
const orderBy = node.orderBy === null ? null : mapSame(node.orderBy, (o) => {
|
|
59
73
|
const expr = resolveExpr(o.expr, fail);
|
|
@@ -68,13 +82,22 @@ function resolveBody<T extends Query | Subquery>(node: T, fail: Fail): T {
|
|
|
68
82
|
const source = node.kind === "query" ? resolveExpr(node.source, fail) : null;
|
|
69
83
|
const same = where2 === node.where && select === node.select && follow === node.follow && from === node.from
|
|
70
84
|
&& orderBy === node.orderBy && limit === node.limit && offset === node.offset && frontier === node.frontier
|
|
85
|
+
&& groupBy === node.groupBy && having2 === node.having && flatten === node.flatten
|
|
71
86
|
&& (node.kind !== "query" || source === node.source);
|
|
72
87
|
if (same) return node;
|
|
73
|
-
const out = { ...node, where: where2, select, follow, frontier, from, orderBy, limit, offset };
|
|
88
|
+
const out = { ...node, where: where2, select, follow, frontier, flatten, groupBy, having: having2, from, orderBy, limit, offset };
|
|
74
89
|
if (node.kind === "query") (out as Query).source = source!;
|
|
75
90
|
return out;
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
function resolveGroupBy(g: GroupBy, fail: Fail): GroupBy {
|
|
94
|
+
const keys = mapSame(g.keys, (k) => {
|
|
95
|
+
const expr = resolveExpr(k.expr, fail);
|
|
96
|
+
return expr === k.expr ? k : { ...k, expr };
|
|
97
|
+
});
|
|
98
|
+
return keys === g.keys ? g : { ...g, keys };
|
|
99
|
+
}
|
|
100
|
+
|
|
78
101
|
function resolveOp(op: OpNode, fail: Fail): OpNode {
|
|
79
102
|
const receiver = resolveExpr(op.receiver, fail);
|
|
80
103
|
const sub = resolveBody(op.sub, fail);
|
|
@@ -145,6 +168,10 @@ function mapExpr(e: Expr, f: (e: Expr) => Expr | null): Expr {
|
|
|
145
168
|
const parts = mapSame(e.parts, (p) => (typeof p === "string" ? p : go(p)));
|
|
146
169
|
return parts === e.parts ? e : { ...e, parts };
|
|
147
170
|
}
|
|
171
|
+
case "aggregate": {
|
|
172
|
+
const arg = e.arg ? go(e.arg) : null;
|
|
173
|
+
return arg === e.arg ? e : { ...e, arg };
|
|
174
|
+
}
|
|
148
175
|
case "op": { // the receiver is read in this scope; the block is its own scope
|
|
149
176
|
const receiver = go(e.receiver);
|
|
150
177
|
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.25";
|
package/src/walk.ts
CHANGED
|
@@ -14,7 +14,9 @@ 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.frontier"
|
|
17
|
+
| "follow" | "follow.destination" | "follow.frontier"
|
|
18
|
+
| "flatten" // a `flatten` node and its expression (0.25)
|
|
19
|
+
| "groupBy" | "having"; // the `group by` keys and the post-group predicate (0.24)
|
|
18
20
|
|
|
19
21
|
/** Where a visited node sits. `depth` is the scope depth at which the node is
|
|
20
22
|
* evaluated (0 = the root scope: a top-level `source`, `limit`, `offset`; 1 =
|
|
@@ -57,6 +59,13 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
57
59
|
// The body's `frontier` (0.20) reads each seed in the row scope, like `where`.
|
|
58
60
|
{ key: "frontier", clause: "follow.frontier", depth: 1 },
|
|
59
61
|
{ key: "follow", clause: "follow", depth: 1 },
|
|
62
|
+
// A `flatten` expression is read on each row, like `where` (0.25); a
|
|
63
|
+
// flattened row sits at the same depth as its parent.
|
|
64
|
+
{ key: "flatten", clause: "flatten", depth: 1 },
|
|
65
|
+
// `group by` keys are read on the rows, `having` on the groups — both at
|
|
66
|
+
// the row depth (the group scope sits where the rows did; 0.24).
|
|
67
|
+
{ key: "groupBy", clause: "groupBy", depth: 1 },
|
|
68
|
+
{ key: "having", clause: "having", depth: 1 },
|
|
60
69
|
{ key: "orderBy", clause: "orderBy", depth: 1 },
|
|
61
70
|
{ key: "limit", clause: "limit" },
|
|
62
71
|
{ key: "offset", clause: "offset" },
|
|
@@ -71,12 +80,18 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
71
80
|
// (`childDepth` tells the two apart by the block's position).
|
|
72
81
|
{ key: "frontier", clause: "follow.frontier", depth: -1 },
|
|
73
82
|
{ key: "follow", clause: "follow" },
|
|
83
|
+
{ key: "flatten", clause: "flatten" },
|
|
84
|
+
{ key: "groupBy", clause: "groupBy" },
|
|
85
|
+
{ key: "having", clause: "having" },
|
|
74
86
|
{ key: "orderBy", clause: "orderBy" },
|
|
75
87
|
{ key: "limit", clause: "limit" },
|
|
76
88
|
{ key: "offset", clause: "offset" },
|
|
77
89
|
],
|
|
78
90
|
op: [{ key: "receiver" }, { key: "sub", depth: 1 }],
|
|
79
91
|
follow: [{ key: "destinations", clause: "follow.destination" }],
|
|
92
|
+
flatten: [{ key: "expr" }],
|
|
93
|
+
groupBy: [{ key: "keys" }],
|
|
94
|
+
key: [{ key: "expr" }],
|
|
80
95
|
order: [{ key: "expr" }],
|
|
81
96
|
field: [{ key: "expr" }],
|
|
82
97
|
collect: [{ key: "op" }],
|
|
@@ -99,6 +114,8 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
99
114
|
tuple: [{ key: "items" }],
|
|
100
115
|
// The interpolations; the text parts between them are strings, not nodes.
|
|
101
116
|
template: [{ key: "parts" }],
|
|
117
|
+
// The argument is read on each of the group's rows (0.24); `null` for `count()`.
|
|
118
|
+
aggregate: [{ key: "arg" }],
|
|
102
119
|
};
|
|
103
120
|
|
|
104
121
|
/** A child slot's value: a node, a list of nodes (a template's `parts` also
|