@omgbase/oqx 0.21.0 → 0.24.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 +17 -0
- package/README.md +79 -2
- package/dist/ast.d.ts +48 -3
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +17 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +7 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +9 -2
- package/dist/build.js.map +1 -1
- package/dist/engine.d.ts +3 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +153 -11
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -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.map +1 -1
- package/dist/lexer.js +13 -3
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +19 -2
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +5 -1
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +2 -0
- package/dist/optimize/ir.js.map +1 -1
- package/dist/optimize/rules.d.ts.map +1 -1
- package/dist/optimize/rules.js +4 -3
- package/dist/optimize/rules.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +406 -21
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +17 -0
- package/dist/print.js.map +1 -1
- package/dist/references.d.ts +28 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +287 -0
- package/dist/references.js.map +1 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +20 -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 +10 -0
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +64 -4
- package/src/build.ts +11 -4
- package/src/engine.ts +154 -11
- package/src/index.ts +3 -1
- package/src/lexer.ts +10 -4
- package/src/optimize/analysis.ts +17 -2
- package/src/optimize/ir.ts +7 -1
- package/src/optimize/rules.ts +4 -3
- package/src/parser.ts +328 -21
- package/src/print.ts +19 -3
- package/src/references.ts +293 -0
- package/src/resolve.ts +22 -2
- package/src/version.ts +1 -1
- package/src/walk.ts +12 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// The `references` static pass (spec/oqx/AST.md §5b, language 0.23): every
|
|
2
|
+
// NAME a query reads from a row — a bare identifier, a dotted navigation headed
|
|
3
|
+
// by one, an outer reference — with the clause it stands in and the SCOPE whose
|
|
4
|
+
// rows it is read from, described as the chain of expressions that produce
|
|
5
|
+
// those rows (the source, each `from` re-projection, each block receiver, each
|
|
6
|
+
// `follow` walk). The pass knows no data: a host that knows what rows an
|
|
7
|
+
// expression yields maps each scope to a row type and judges the name against
|
|
8
|
+
// it (omgbase: "is `target` a field of links?"). Built on `visit`, so a future
|
|
9
|
+
// node kind is walked here the day it is added to `CHILDREN`.
|
|
10
|
+
//
|
|
11
|
+
// What is a reference and what is not (the rules, pinned by
|
|
12
|
+
// `cases/references.json`):
|
|
13
|
+
// • a bare `ident` is a reference to the current row's property, with the
|
|
14
|
+
// name as written; a `member` chain headed by an `ident` or `outer` is ONE
|
|
15
|
+
// reference whose name is the full dotted path (`doc.layer`, `meta.owner`);
|
|
16
|
+
// a chain headed by anything else (a call, a block, a literal, a tuple) is
|
|
17
|
+
// not — its head is walked and the trailing `.name`s are ignored, since the
|
|
18
|
+
// head's value is not a row of any scope;
|
|
19
|
+
// • an `outer` (`^name`, `N^name`) is a reference read in the scope `levels`
|
|
20
|
+
// out; `$it` as a head is the row itself (`$it.title` ≡ `title`,
|
|
21
|
+
// `^$it.title` ≡ `^title`) and `$it` alone is no reference;
|
|
22
|
+
// • `$`-names (`$key`, `$depth`, `$stop`, …) are the language's values, never
|
|
23
|
+
// references; a `binding` is a host value; a function or method NAME is
|
|
24
|
+
// not a reference, its receiver and arguments are walked
|
|
25
|
+
// (`target.contains("x")` references `target`); a name a nested block
|
|
26
|
+
// LIFTS onto a scope (`^open: value`, SEMANTICS §19) is a binding of that
|
|
27
|
+
// scope, so reading it there (`select open`, `^open` from deeper) is not a
|
|
28
|
+
// reference either;
|
|
29
|
+
// • a `select` alias is not a reference where the language resolves it
|
|
30
|
+
// (SEMANTICS §14): in the same body's `where`, and in the items to its
|
|
31
|
+
// right — a bare identifier naming such an alias, or a chain headed by it,
|
|
32
|
+
// is skipped; an alias's own name inside its own item is the row field and
|
|
33
|
+
// IS a reference; `order by`, `frontier`, `limit`/`offset`, `from` and
|
|
34
|
+
// `follow` are not rewritten against aliases, so a name there is a
|
|
35
|
+
// reference whatever the `select` says;
|
|
36
|
+
// • a name read at the ROOT scope — the head of the top-level `source`
|
|
37
|
+
// (`from people`: clause `source`), an outer reference with as many carets
|
|
38
|
+
// as there are enclosing scopes (`^docs` from a top-level row) — is
|
|
39
|
+
// reported with depth 0 and no steps: it names one of the host's roots,
|
|
40
|
+
// and the host decides what to make of it;
|
|
41
|
+
// • a `follow` destination that is a plain expression is a reference read
|
|
42
|
+
// on the frontier row (clause `follow.destination`), like a `from`.
|
|
43
|
+
//
|
|
44
|
+
// The scope: `{ depth, steps }`. `depth` is the scope depth the name is
|
|
45
|
+
// evaluated at (`visit`'s depth; for an outer reference, that depth minus its
|
|
46
|
+
// levels). `steps` are the expressions that produce the scope's rows, root
|
|
47
|
+
// first, each with the depth of the rows it produces: the query's `source`
|
|
48
|
+
// (depth 1), each `from` re-projection in turn (the same depth — a `from`
|
|
49
|
+
// replaces the rows), a block's receiver (one deeper than where it is read),
|
|
50
|
+
// then the block's own `from`s; a `follow` node is a step too — the walk's
|
|
51
|
+
// frontier rows, at the body's depth, which the destinations are read on
|
|
52
|
+
// (a destination block's receiver is then one deeper). A name read inside the
|
|
53
|
+
// k-th `from` sees only the steps before it. For an outer reference the steps
|
|
54
|
+
// are cut after the last step at its scope's depth. The empty list is the root
|
|
55
|
+
// scope.
|
|
56
|
+
|
|
57
|
+
import type { AstNode, Expr, Follow, GroupBy, Query, SelectItem, Span, Subquery } from "./ast.ts";
|
|
58
|
+
import type { Clause } from "./walk.ts";
|
|
59
|
+
import { visit } from "./walk.ts";
|
|
60
|
+
|
|
61
|
+
/** One step of a scope's row chain: an expression (a source, a `from`, a block
|
|
62
|
+
* receiver, a plain destination), a `follow` node (the walk's frontier rows), or
|
|
63
|
+
* — since 0.24 — a `groupBy` node (the body's groups: the rows at the same depth
|
|
64
|
+
* replaced by one key record per group), with the scope depth of the rows it
|
|
65
|
+
* produces. */
|
|
66
|
+
export interface ReferenceStep {
|
|
67
|
+
readonly expr: Expr | Follow | GroupBy;
|
|
68
|
+
readonly depth: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ReferenceScope {
|
|
72
|
+
/** The scope depth the name is read at (SEMANTICS §2; 0 = the root scope). */
|
|
73
|
+
readonly depth: number;
|
|
74
|
+
/** The row chain producing the scope's rows, root first. */
|
|
75
|
+
readonly steps: readonly ReferenceStep[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Reference {
|
|
79
|
+
/** The name as written — the full dotted path for a navigation (`doc.layer`), without a `$it.` head. */
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly clause: Clause;
|
|
82
|
+
readonly scope: ReferenceScope;
|
|
83
|
+
/** The span of the whole reference (the chain, for a navigation). */
|
|
84
|
+
readonly span: Span;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A dotted name headed by an `ident` or `outer`: the head and the path after it. */
|
|
88
|
+
function dottedName(e: Expr): { head: Expr & { kind: "ident" | "outer" }; path: string[] } | null {
|
|
89
|
+
if (e.kind === "ident" || e.kind === "outer") return { head: e, path: [] };
|
|
90
|
+
if (e.kind === "member") {
|
|
91
|
+
const inner = dottedName(e.recv);
|
|
92
|
+
return inner === null ? null : { head: inner.head, path: [...inner.path, e.name] };
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The alias an item defines (SEMANTICS §14): a named `collect`, or a named non-lift field. */
|
|
98
|
+
function aliasName(it: SelectItem): string | null {
|
|
99
|
+
if (it.kind === "collect") return it.name === "" ? null : it.name;
|
|
100
|
+
return it.lift === 0 && it.name !== "" ? it.name : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Every name a query (or block body) reads from a row, in canonical source order. */
|
|
104
|
+
export function references(root: Query | Subquery): Reference[] {
|
|
105
|
+
const out: Reference[] = [];
|
|
106
|
+
const lifts = new Map<Query | Subquery, ReadonlySet<string>>();
|
|
107
|
+
visit(root, {
|
|
108
|
+
enter(node, ctx) {
|
|
109
|
+
const named = dottedName(node as Expr);
|
|
110
|
+
if (named === null) return;
|
|
111
|
+
const { head } = named;
|
|
112
|
+
// The head of a navigation is handled here, once, with the chain's full name.
|
|
113
|
+
let name = head.name;
|
|
114
|
+
let path = named.path;
|
|
115
|
+
if (name === "$it") {
|
|
116
|
+
if (path.length === 0) return false; // the row itself
|
|
117
|
+
name = path[0]!;
|
|
118
|
+
path = path.slice(1);
|
|
119
|
+
} else if (name.startsWith("$")) return false; // a language value
|
|
120
|
+
if (ctx.clause === null) return false; // unreachable: the root node is never a name
|
|
121
|
+
if (head.kind === "ident" && isResolvedAlias(name, ctx.path, ctx.clause)) return false;
|
|
122
|
+
const levels = head.kind === "outer" ? head.levels : 0;
|
|
123
|
+
const depth = ctx.depth - levels;
|
|
124
|
+
if (isLiftBound(name, depth, ctx.path, lifts)) return false;
|
|
125
|
+
// At a group scope (0.24) a bare key name is the key — an alias of the
|
|
126
|
+
// expression reported in clause `groupBy` — and `rows` is the language's
|
|
127
|
+
// value; neither reads a row.
|
|
128
|
+
if (isGroupValue(name, depth, ctx.path, node)) return false;
|
|
129
|
+
const full = [name, ...path].join(".");
|
|
130
|
+
const steps = scopeSteps(ctx.path, node);
|
|
131
|
+
const scoped = levels === 0 ? steps : cutAt(steps, depth);
|
|
132
|
+
out.push({ name: full, clause: ctx.clause, scope: { depth, steps: scoped }, span: node.span });
|
|
133
|
+
return false;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Whether a bare `name` in this position is a `select` alias the language
|
|
140
|
+
* substitutes (so the name is the alias, not a row property): in the body's
|
|
141
|
+
* `where` or `having`, any alias; in a `select` item, an alias defined to its LEFT. */
|
|
142
|
+
function isResolvedAlias(name: string, path: readonly AstNode[], clause: Clause): boolean {
|
|
143
|
+
if (clause !== "where" && clause !== "select" && clause !== "having") return false;
|
|
144
|
+
// The nearest body, and the item under it when we are inside its select.
|
|
145
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
146
|
+
const a = path[i]!;
|
|
147
|
+
if (a.kind !== "query" && a.kind !== "subquery") continue;
|
|
148
|
+
const index = new Map<string, number>();
|
|
149
|
+
a.select.forEach((it, j) => { const n = aliasName(it); if (n !== null) index.set(n, j); });
|
|
150
|
+
const j = index.get(name);
|
|
151
|
+
if (j === undefined) return false;
|
|
152
|
+
if (clause === "where" || clause === "having") return true;
|
|
153
|
+
const item = path[i + 1];
|
|
154
|
+
const at = item === undefined ? -1 : a.select.indexOf(item as SelectItem);
|
|
155
|
+
return at >= 0 && j < at;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether `name`, read at scope `depth`, is bound there by a lift (`^name: expr`
|
|
162
|
+
* in a nested block, SEMANTICS §19): the lifted value is appended to that
|
|
163
|
+
* scope's row record, so the name is a binding, not a row property. The body
|
|
164
|
+
* whose rows are at `depth` is the depth-th body on the path (every nested body
|
|
165
|
+
* is one scope deeper than the one around it).
|
|
166
|
+
*/
|
|
167
|
+
function isLiftBound(name: string, depth: number, path: readonly AstNode[], cache: Map<Query | Subquery, ReadonlySet<string>>): boolean {
|
|
168
|
+
if (depth < 1) return false;
|
|
169
|
+
const bodies = path.filter((a): a is Query | Subquery => a.kind === "query" || a.kind === "subquery");
|
|
170
|
+
const body = bodies[depth - 1];
|
|
171
|
+
if (body === undefined) return false;
|
|
172
|
+
let names = cache.get(body);
|
|
173
|
+
if (names === undefined) {
|
|
174
|
+
names = liftsLandingOn(body);
|
|
175
|
+
cache.set(body, names);
|
|
176
|
+
}
|
|
177
|
+
return names.has(name);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The names the nested blocks of `body` lift onto it: a `field` item with `lift` L in a block L scopes below it. */
|
|
181
|
+
function liftsLandingOn(body: Query | Subquery): ReadonlySet<string> {
|
|
182
|
+
const names = new Set<string>();
|
|
183
|
+
const own = body.kind === "subquery" ? 1 : 0;
|
|
184
|
+
visit(body, {
|
|
185
|
+
enter(node, ctx) {
|
|
186
|
+
if (node.kind !== "field" || node.lift === 0) return;
|
|
187
|
+
const below = ctx.path.filter((a) => a.kind === "subquery").length - own;
|
|
188
|
+
if (node.lift === below) names.add(node.name);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
return names;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Whether `name`, read at scope `depth`, is a value of a GROUP scope there
|
|
196
|
+
* (SEMANTICS §24): the body whose rows are at `depth` has a `group by`, the
|
|
197
|
+
* position is one of its post-group clauses — a select item, `having`, `order
|
|
198
|
+
* by` — not inside an aggregate's argument (which reads the group's rows), and
|
|
199
|
+
* the name is a key or `rows`.
|
|
200
|
+
*/
|
|
201
|
+
function isGroupValue(name: string, depth: number, path: readonly AstNode[], node: AstNode): boolean {
|
|
202
|
+
const i = bodyIndexAt(path, depth);
|
|
203
|
+
if (i === -1) return false;
|
|
204
|
+
const body = path[i] as Query | Subquery;
|
|
205
|
+
if (!atGroupScope(body, path, i, node)) return false;
|
|
206
|
+
return name === "rows" || body.groupBy!.keys.some((k) => k.name === name);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The index in `path` of the body whose rows are at scope `depth` (every nested body is one scope deeper), or -1. */
|
|
210
|
+
function bodyIndexAt(path: readonly AstNode[], depth: number): number {
|
|
211
|
+
if (depth < 1) return -1;
|
|
212
|
+
let seen = 0;
|
|
213
|
+
for (let i = 0; i < path.length; i++) {
|
|
214
|
+
const a = path[i]!;
|
|
215
|
+
if (a.kind !== "query" && a.kind !== "subquery") continue;
|
|
216
|
+
if (++seen === depth) return i;
|
|
217
|
+
}
|
|
218
|
+
return -1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Whether a position under `body` (at `path[i]`) reads the body's GROUP scope:
|
|
222
|
+
* the body is grouped, the position lies in a select item, `having` or `order
|
|
223
|
+
* by`, and no aggregate stands between the body and the position (an
|
|
224
|
+
* aggregate's argument reads the group's rows; a nested body is its own scope
|
|
225
|
+
* and ends the search). */
|
|
226
|
+
function atGroupScope(body: Query | Subquery, path: readonly AstNode[], i: number, node: AstNode): boolean {
|
|
227
|
+
if (body.groupBy === null) return false;
|
|
228
|
+
const child = path[i + 1] ?? node;
|
|
229
|
+
const post = body.select.includes(child as SelectItem) || child === body.having || (body.orderBy?.includes(child as never) ?? false);
|
|
230
|
+
if (!post) return false;
|
|
231
|
+
for (let j = i + 1; j < path.length; j++) {
|
|
232
|
+
const a = path[j]!;
|
|
233
|
+
if (a.kind === "query" || a.kind === "subquery") break;
|
|
234
|
+
if (a.kind === "aggregate") return false;
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The row chain of the scope a node is evaluated in, from its ancestors. */
|
|
240
|
+
function scopeSteps(path: readonly AstNode[], node: AstNode): ReferenceStep[] {
|
|
241
|
+
const steps: ReferenceStep[] = [];
|
|
242
|
+
let depth = 0;
|
|
243
|
+
for (let i = 0; i < path.length; i++) {
|
|
244
|
+
const a = path[i]!;
|
|
245
|
+
const next: AstNode = path[i + 1] ?? node;
|
|
246
|
+
switch (a.kind) {
|
|
247
|
+
case "query": {
|
|
248
|
+
if (next === a.source || next === a.limit || next === a.offset) break; // read at the root scope
|
|
249
|
+
depth = 1;
|
|
250
|
+
steps.push({ expr: a.source, depth });
|
|
251
|
+
pushFroms(steps, a.from, next, depth);
|
|
252
|
+
// A grouped body's post-group clauses read its groups (0.24): the rows
|
|
253
|
+
// at this depth replaced by one key record per group.
|
|
254
|
+
if (atGroupScope(a, path, i, node)) steps.push({ expr: a.groupBy!, depth });
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case "subquery": {
|
|
258
|
+
// The receiver step was pushed by the enclosing `op`; this body's rows
|
|
259
|
+
// are one deeper than where that receiver was read.
|
|
260
|
+
pushFroms(steps, a.from, next, depth);
|
|
261
|
+
if (atGroupScope(a, path, i, node)) steps.push({ expr: a.groupBy!, depth });
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "op": {
|
|
265
|
+
if (next !== a.sub) break; // inside the receiver: read where the block is read
|
|
266
|
+
depth += 1;
|
|
267
|
+
steps.push({ expr: a.receiver, depth });
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "follow": {
|
|
271
|
+
// The destinations are read on the walk's frontier rows, at the body's depth.
|
|
272
|
+
steps.push({ expr: a, depth });
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
default: break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return steps;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The `from` re-projections that apply at `next`: all of them unless `next` is one of them, in which case those before it. */
|
|
282
|
+
function pushFroms(steps: ReferenceStep[], from: readonly Expr[], next: AstNode, depth: number): void {
|
|
283
|
+
const at = from.indexOf(next as Expr);
|
|
284
|
+
const upto = at === -1 ? from.length : at;
|
|
285
|
+
for (let k = 0; k < upto; k++) steps.push({ expr: from[k]!, depth });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The steps up to and including the last one producing rows at `depth` (none for the root scope). */
|
|
289
|
+
function cutAt(steps: readonly ReferenceStep[], depth: number): ReferenceStep[] {
|
|
290
|
+
let end = 0;
|
|
291
|
+
steps.forEach((s, i) => { if (s.depth === depth) end = i + 1; });
|
|
292
|
+
return steps.slice(0, end);
|
|
293
|
+
}
|
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, 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,15 @@ 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);
|
|
57
64
|
const from = mapSame(node.from, (e) => resolveExpr(e, fail));
|
|
58
65
|
const orderBy = node.orderBy === null ? null : mapSame(node.orderBy, (o) => {
|
|
59
66
|
const expr = resolveExpr(o.expr, fail);
|
|
@@ -68,13 +75,22 @@ function resolveBody<T extends Query | Subquery>(node: T, fail: Fail): T {
|
|
|
68
75
|
const source = node.kind === "query" ? resolveExpr(node.source, fail) : null;
|
|
69
76
|
const same = where2 === node.where && select === node.select && follow === node.follow && from === node.from
|
|
70
77
|
&& orderBy === node.orderBy && limit === node.limit && offset === node.offset && frontier === node.frontier
|
|
78
|
+
&& groupBy === node.groupBy && having2 === node.having
|
|
71
79
|
&& (node.kind !== "query" || source === node.source);
|
|
72
80
|
if (same) return node;
|
|
73
|
-
const out = { ...node, where: where2, select, follow, frontier, from, orderBy, limit, offset };
|
|
81
|
+
const out = { ...node, where: where2, select, follow, frontier, groupBy, having: having2, from, orderBy, limit, offset };
|
|
74
82
|
if (node.kind === "query") (out as Query).source = source!;
|
|
75
83
|
return out;
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
function resolveGroupBy(g: GroupBy, fail: Fail): GroupBy {
|
|
87
|
+
const keys = mapSame(g.keys, (k) => {
|
|
88
|
+
const expr = resolveExpr(k.expr, fail);
|
|
89
|
+
return expr === k.expr ? k : { ...k, expr };
|
|
90
|
+
});
|
|
91
|
+
return keys === g.keys ? g : { ...g, keys };
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
function resolveOp(op: OpNode, fail: Fail): OpNode {
|
|
79
95
|
const receiver = resolveExpr(op.receiver, fail);
|
|
80
96
|
const sub = resolveBody(op.sub, fail);
|
|
@@ -145,6 +161,10 @@ function mapExpr(e: Expr, f: (e: Expr) => Expr | null): Expr {
|
|
|
145
161
|
const parts = mapSame(e.parts, (p) => (typeof p === "string" ? p : go(p)));
|
|
146
162
|
return parts === e.parts ? e : { ...e, parts };
|
|
147
163
|
}
|
|
164
|
+
case "aggregate": {
|
|
165
|
+
const arg = e.arg ? go(e.arg) : null;
|
|
166
|
+
return arg === e.arg ? e : { ...e, arg };
|
|
167
|
+
}
|
|
148
168
|
case "op": { // the receiver is read in this scope; the block is its own scope
|
|
149
169
|
const receiver = go(e.receiver);
|
|
150
170
|
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.24";
|
package/src/walk.ts
CHANGED
|
@@ -14,7 +14,8 @@ 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
|
+
| "groupBy" | "having"; // the `group by` keys and the post-group predicate (0.24)
|
|
18
19
|
|
|
19
20
|
/** Where a visited node sits. `depth` is the scope depth at which the node is
|
|
20
21
|
* evaluated (0 = the root scope: a top-level `source`, `limit`, `offset`; 1 =
|
|
@@ -57,6 +58,10 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
57
58
|
// The body's `frontier` (0.20) reads each seed in the row scope, like `where`.
|
|
58
59
|
{ key: "frontier", clause: "follow.frontier", depth: 1 },
|
|
59
60
|
{ key: "follow", clause: "follow", depth: 1 },
|
|
61
|
+
// `group by` keys are read on the rows, `having` on the groups — both at
|
|
62
|
+
// the row depth (the group scope sits where the rows did; 0.24).
|
|
63
|
+
{ key: "groupBy", clause: "groupBy", depth: 1 },
|
|
64
|
+
{ key: "having", clause: "having", depth: 1 },
|
|
60
65
|
{ key: "orderBy", clause: "orderBy", depth: 1 },
|
|
61
66
|
{ key: "limit", clause: "limit" },
|
|
62
67
|
{ key: "offset", clause: "offset" },
|
|
@@ -71,12 +76,16 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
71
76
|
// (`childDepth` tells the two apart by the block's position).
|
|
72
77
|
{ key: "frontier", clause: "follow.frontier", depth: -1 },
|
|
73
78
|
{ key: "follow", clause: "follow" },
|
|
79
|
+
{ key: "groupBy", clause: "groupBy" },
|
|
80
|
+
{ key: "having", clause: "having" },
|
|
74
81
|
{ key: "orderBy", clause: "orderBy" },
|
|
75
82
|
{ key: "limit", clause: "limit" },
|
|
76
83
|
{ key: "offset", clause: "offset" },
|
|
77
84
|
],
|
|
78
85
|
op: [{ key: "receiver" }, { key: "sub", depth: 1 }],
|
|
79
86
|
follow: [{ key: "destinations", clause: "follow.destination" }],
|
|
87
|
+
groupBy: [{ key: "keys" }],
|
|
88
|
+
key: [{ key: "expr" }],
|
|
80
89
|
order: [{ key: "expr" }],
|
|
81
90
|
field: [{ key: "expr" }],
|
|
82
91
|
collect: [{ key: "op" }],
|
|
@@ -99,6 +108,8 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
|
|
|
99
108
|
tuple: [{ key: "items" }],
|
|
100
109
|
// The interpolations; the text parts between them are strings, not nodes.
|
|
101
110
|
template: [{ key: "parts" }],
|
|
111
|
+
// The argument is read on each of the group's rows (0.24); `null` for `count()`.
|
|
112
|
+
aggregate: [{ key: "arg" }],
|
|
102
113
|
};
|
|
103
114
|
|
|
104
115
|
/** A child slot's value: a node, a list of nodes (a template's `parts` also
|