@omgbase/oqx 0.24.0 → 0.26.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 +13 -0
- package/README.md +267 -202
- package/dist/ast.d.ts +19 -1
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +3 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +4 -2
- package/dist/build.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +95 -44
- package/dist/engine.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 +12 -2
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +4 -1
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +1 -0
- package/dist/optimize/ir.js.map +1 -1
- package/dist/optimize/rules.d.ts.map +1 -1
- package/dist/optimize/rules.js +3 -2
- package/dist/optimize/rules.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +206 -42
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts +2 -2
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +88 -25
- package/dist/print.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +39 -1
- package/dist/references.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +9 -2
- 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 +5 -0
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +20 -1
- package/src/build.ts +5 -3
- package/src/engine.ts +89 -39
- package/src/optimize/analysis.ts +9 -2
- package/src/optimize/ir.ts +5 -1
- package/src/optimize/rules.ts +3 -2
- package/src/parser.ts +193 -41
- package/src/print.ts +92 -30
- package/src/references.ts +34 -2
- package/src/resolve.ts +10 -3
- package/src/version.ts +1 -1
- package/src/walk.ts +6 -0
package/dist/parser.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// consumer block — each clause appears at most once, in exactly this order:
|
|
14
14
|
//
|
|
15
15
|
// select <projection> from <source> where <predicate> follow <dest>, … {…}
|
|
16
|
-
// order by … limit N offset N
|
|
16
|
+
// flatten <expr> [{…}] [as <name>] … group by … having … order by … limit N offset N
|
|
17
17
|
//
|
|
18
18
|
// A `follow` destination is a relation of the current row (a receiver, never
|
|
19
19
|
// an outer reference) or a destination block — a receiver (which MAY be an
|
|
@@ -24,17 +24,27 @@
|
|
|
24
24
|
// that destination's block, whose body admits two clauses no other block has —
|
|
25
25
|
// `frontier P` and `depth n`, in the clause order after `where`.
|
|
26
26
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// A query names its source BEFORE its body (0.26, the collection-scoped form):
|
|
28
|
+
// `<collection> [<consumer>] [distinct] { body }` — the receiver supplies the
|
|
29
|
+
// rows, so the body never names its source and its `from` is the re-projection
|
|
30
|
+
// (a flatMap). The clause form `select … from S …` is still read as the same
|
|
31
|
+
// tree (its top-level body needs `from`); the printer never writes it. Only
|
|
30
32
|
// `select` may drop its keyword, and only when it is the first clause written
|
|
31
|
-
// (`name, age
|
|
32
|
-
//
|
|
33
|
-
// is syntactically a predicate — not a bare name, a
|
|
34
|
-
// opens a `where`-first body (`people
|
|
35
|
-
//
|
|
33
|
+
// (`people { name, age }`); every other clause always carries its keyword. A
|
|
34
|
+
// body with a receiver may instead lead with a predicate (since 0.17): a
|
|
35
|
+
// leading expression that is syntactically a predicate — not a bare name, a
|
|
36
|
+
// dotted navigation or a lift — opens a `where`-first body (`people { age > 50
|
|
37
|
+
// }`, `people exists { age > 50 }`); at the clause form's top level a leading
|
|
38
|
+
// predicate is still an error, since `where` would precede `from`. An
|
|
36
39
|
// out-of-order clause is a parse error naming the order.
|
|
37
40
|
//
|
|
41
|
+
// Two 0.26 shapes desugar HERE as well: a consumer word with no block is the
|
|
42
|
+
// directive over the empty block (`jobs first` ≡ `jobs first { }`, `jobs count
|
|
43
|
+
// > 1`, `_nodes exists`; a consumer word right after a complete receiver can be
|
|
44
|
+
// nothing else), and a projection-only block over a `collect`/`first`/`single`
|
|
45
|
+
// directive whose block projects nothing is that directive's projection
|
|
46
|
+
// (`jobs[0] { name }` ≡ `jobs first { name offset 0 }`, `mergeProjectionBlock`).
|
|
47
|
+
//
|
|
38
48
|
// Sugar (0.17) desugars HERE, to existing nodes: `x { … }` is `x collect { … }`
|
|
39
49
|
// wherever a value stands (a select item, a where leaf, a follow destination, a
|
|
40
50
|
// call argument — since 0.20 without exception) and `x distinct { … }` is
|
|
@@ -81,6 +91,7 @@ import { lexTemplate, lexString } from "./lexer.js";
|
|
|
81
91
|
import { OqxError } from "./errors.js";
|
|
82
92
|
import { AGGREGATE_NAMES, isAggregateName } from "./ast.js";
|
|
83
93
|
import { resolveSelect, resolveWhere } from "./resolve.js";
|
|
94
|
+
import { visit } from "./walk.js";
|
|
84
95
|
import { printTemplate } from "./print.js";
|
|
85
96
|
const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "single"]);
|
|
86
97
|
// The fixed clause order of a body (ADR-020). Each clause appears at most once.
|
|
@@ -89,8 +100,11 @@ const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "singl
|
|
|
89
100
|
// `depth` is legal only in a follow destination block, and the order sentence
|
|
90
101
|
// names it only there. `group by` and `having` (0.24) sit after `follow`: the
|
|
91
102
|
// rows a body yields — the walk's occurrences, when it has one — are what gets
|
|
92
|
-
// grouped, and the bound and the ordering then apply to the groups.
|
|
93
|
-
|
|
103
|
+
// grouped, and the bound and the ordering then apply to the groups. `flatten`
|
|
104
|
+
// (0.25) sits between them: it fans the rows a body yields — the occurrences,
|
|
105
|
+
// after a walk — out per element, and is the one clause that may repeat (each
|
|
106
|
+
// `flatten` fans out the rows the previous produced, SEMANTICS §25).
|
|
107
|
+
const CLAUSE_ORDER = ["select", "from", "where", "frontier", "depth", "follow", "flatten", "group by", "having", "order by", "limit", "offset"];
|
|
94
108
|
const DESTINATION_ONLY = new Set(["depth"]);
|
|
95
109
|
// The clauses a body may hold, in order.
|
|
96
110
|
function clausesOf(ctx) {
|
|
@@ -101,7 +115,7 @@ function clausesOf(ctx) {
|
|
|
101
115
|
const CLAUSE_WORDS = new Set([
|
|
102
116
|
"collect", "exists", "none", "count", "first", "single",
|
|
103
117
|
"order", "by", "asc", "desc", "follow", "distinct", "frontier", "depth", "in", "values",
|
|
104
|
-
"limit", "offset", "group", "having",
|
|
118
|
+
"limit", "offset", "group", "having", "flatten", "as",
|
|
105
119
|
]);
|
|
106
120
|
// The names a group scope resolves besides its keys (SEMANTICS §24): the
|
|
107
121
|
// group's rows, the key record itself, and its identity.
|
|
@@ -125,6 +139,29 @@ export function parseTemplate(fragments, values) {
|
|
|
125
139
|
export function parseString(src) {
|
|
126
140
|
return new Parser(lexString(src)).parseQuery();
|
|
127
141
|
}
|
|
142
|
+
/** The clauses of an omitted block (`jobs first` — the empty block `{ }`). */
|
|
143
|
+
function emptyBody() {
|
|
144
|
+
return { froms: [], where: null, select: [], orderBy: null, follow: null, frontier: null, depth: null, flatten: [], groupBy: null, having: null, distinct: false, values: false, limit: null, offset: null };
|
|
145
|
+
}
|
|
146
|
+
/** The names a projection would shadow in a `where` of the same body: every
|
|
147
|
+
* item's name except a plain self-projection (`pay: pay`, `pay`), which
|
|
148
|
+
* substitutes a name for itself. */
|
|
149
|
+
function shadowingNames(items) {
|
|
150
|
+
return items
|
|
151
|
+
.filter((it) => it.name !== "" && !(it.kind === "field" && it.lift === 0 && it.expr.kind === "ident" && it.expr.name === it.name))
|
|
152
|
+
.map((it) => it.name);
|
|
153
|
+
}
|
|
154
|
+
/** Whether a bare identifier anywhere under `w` has one of `names` (the alias
|
|
155
|
+
* guard of the projection-block merge; conservative: nested blocks included). */
|
|
156
|
+
function readsAnyOf(w, names) {
|
|
157
|
+
const wanted = new Set(names.filter((n) => n !== ""));
|
|
158
|
+
if (wanted.size === 0)
|
|
159
|
+
return false;
|
|
160
|
+
let found = false;
|
|
161
|
+
visit(w, { enter(node) { if (node.kind === "ident" && wanted.has(node.name))
|
|
162
|
+
found = true; return !found; } });
|
|
163
|
+
return found;
|
|
164
|
+
}
|
|
128
165
|
// A copy of a node with another span (a parenthesized operand takes the span of
|
|
129
166
|
// its parentheses).
|
|
130
167
|
function withSpan(node, span) {
|
|
@@ -142,6 +179,11 @@ class Parser {
|
|
|
142
179
|
* top-level row's clauses, one deeper per block body and per follow `where`.
|
|
143
180
|
* An absolute reference `N^name` reads `depth − N` scopes out. */
|
|
144
181
|
depth = 0;
|
|
182
|
+
/** Whether a consumer word with no block is read as a directive over the
|
|
183
|
+
* empty block (`jobs first` ≡ `jobs first { }`, since 0.26). Off only while
|
|
184
|
+
* the clause form's top-level `from` expression is read, so `from people
|
|
185
|
+
* count` keeps its whole-query hint (a count is never a source). */
|
|
186
|
+
bareDirectives = true;
|
|
145
187
|
constructor(tokens) {
|
|
146
188
|
this.tokens = tokens;
|
|
147
189
|
}
|
|
@@ -185,9 +227,10 @@ class Parser {
|
|
|
185
227
|
const start = this.start();
|
|
186
228
|
// Directive form: `<receiver> <consumer> { … }` consuming the whole query.
|
|
187
229
|
let directive = this.tryOp(false);
|
|
188
|
-
if (directive && !this.at("eof") && (this.at("lbracket") || this.
|
|
230
|
+
if (directive && !this.at("eof") && (this.at("lbracket") || this.atBlock() !== null)) {
|
|
189
231
|
// A longer chain whose outermost node is still a directive
|
|
190
|
-
// (`people first { } first { }` — how `people[p][0]` prints back
|
|
232
|
+
// (`people first { } first { }` — how `people[p][0]` prints back; a block
|
|
233
|
+
// over the directive, `people[0] { name }`, merges into it, 0.26).
|
|
191
234
|
const save = this.pos;
|
|
192
235
|
try {
|
|
193
236
|
const e = this.parsePostfix(directive);
|
|
@@ -228,7 +271,7 @@ class Parser {
|
|
|
228
271
|
if (!this.at("eof"))
|
|
229
272
|
this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause`);
|
|
230
273
|
if (body.froms.length === 0) {
|
|
231
|
-
this.fail("a query must name its source
|
|
274
|
+
this.fail("a query must name its source before its body: `<collection> { … }` (or `<collection> <consumer> { … }`; the clause form `select … from <collection> …` is still read)");
|
|
232
275
|
}
|
|
233
276
|
return {
|
|
234
277
|
kind: "query",
|
|
@@ -241,6 +284,7 @@ class Parser {
|
|
|
241
284
|
consumer: "collect",
|
|
242
285
|
follow: body.follow,
|
|
243
286
|
frontier: body.frontier,
|
|
287
|
+
flatten: body.flatten,
|
|
244
288
|
groupBy: body.groupBy,
|
|
245
289
|
having: body.having,
|
|
246
290
|
distinct: body.distinct,
|
|
@@ -254,7 +298,7 @@ class Parser {
|
|
|
254
298
|
queryFromOp(e, start) {
|
|
255
299
|
return {
|
|
256
300
|
kind: "query", span: this.sp(start), source: e.receiver, from: e.sub.from, where: e.sub.where, select: e.sub.select,
|
|
257
|
-
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, frontier: e.sub.frontier, groupBy: e.sub.groupBy, having: e.sub.having,
|
|
301
|
+
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, frontier: e.sub.frontier, flatten: e.sub.flatten, groupBy: e.sub.groupBy, having: e.sub.having,
|
|
258
302
|
distinct: e.distinct, values: e.sub.values, limit: e.sub.limit, offset: e.sub.offset,
|
|
259
303
|
};
|
|
260
304
|
}
|
|
@@ -275,6 +319,7 @@ class Parser {
|
|
|
275
319
|
let follow = null;
|
|
276
320
|
let frontier = null;
|
|
277
321
|
let depth = null;
|
|
322
|
+
const flatten = [];
|
|
278
323
|
let groupBy = null;
|
|
279
324
|
let having = null;
|
|
280
325
|
let distinct = false;
|
|
@@ -288,7 +333,9 @@ class Parser {
|
|
|
288
333
|
const rowDepth = ctx.top ? 1 : this.depth;
|
|
289
334
|
const enter = (clause) => {
|
|
290
335
|
const idx = CLAUSE_ORDER.indexOf(clause);
|
|
291
|
-
|
|
336
|
+
// `flatten` is the one clause that may repeat (0.25): each fans out the
|
|
337
|
+
// rows the previous produced.
|
|
338
|
+
if (idx === stage && clause !== "flatten")
|
|
292
339
|
this.fail(`duplicate \`${clause}\` clause`);
|
|
293
340
|
if (idx < stage) {
|
|
294
341
|
this.fail(`\`${clause}\` must come before \`${CLAUSE_ORDER[stage]}\` — OQX clause order is ${clausesOf(ctx).join(", ")}`);
|
|
@@ -309,7 +356,18 @@ class Parser {
|
|
|
309
356
|
if (this.at("kw", "from")) {
|
|
310
357
|
enter("from");
|
|
311
358
|
this.next();
|
|
312
|
-
|
|
359
|
+
// At the clause form's top level a consumer word after the source is the
|
|
360
|
+
// whole-query mistake (`from people count`): kept as the hint below,
|
|
361
|
+
// not read as a braceless directive (a count is never a source).
|
|
362
|
+
const bare = this.bareDirectives;
|
|
363
|
+
if (ctx.top)
|
|
364
|
+
this.bareDirectives = false;
|
|
365
|
+
try {
|
|
366
|
+
froms.push(this.parseValueExpr());
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
this.bareDirectives = bare;
|
|
370
|
+
}
|
|
313
371
|
continue;
|
|
314
372
|
}
|
|
315
373
|
if (this.at("kw", "where")) {
|
|
@@ -345,6 +403,14 @@ class Parser {
|
|
|
345
403
|
follow = this.atDepth(rowDepth, () => this.parseFollow());
|
|
346
404
|
continue;
|
|
347
405
|
}
|
|
406
|
+
// `flatten <expr> [{ … }] [as <name>]` (0.25): the value is read on each
|
|
407
|
+
// row, like `where`; a brace after a receiver is the receiver-block sugar
|
|
408
|
+
// (the `collect` op), so the block needs no slot of its own.
|
|
409
|
+
if (this.atFlatten()) {
|
|
410
|
+
enter("flatten");
|
|
411
|
+
flatten.push(this.atDepth(rowDepth, () => this.parseFlatten(flatten)));
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
348
414
|
// `group by k, name: expr` (0.24): the keys are read on the rows, like `where`.
|
|
349
415
|
if (this.atGroupBy()) {
|
|
350
416
|
enter("group by");
|
|
@@ -426,12 +492,56 @@ class Parser {
|
|
|
426
492
|
if (groupBy) {
|
|
427
493
|
if (distinct)
|
|
428
494
|
this.failAt(groupBy.span[0], DISTINCT_WITH_GROUP);
|
|
429
|
-
this.checkGroupedBody({ select, resolvedSelect, having, resolvedHaving, orderBy, froms, where: resolvedWhere, frontier, limit, offset, groupBy });
|
|
495
|
+
this.checkGroupedBody({ select, resolvedSelect, having, resolvedHaving, orderBy, froms, where: resolvedWhere, frontier, flatten, limit, offset, groupBy });
|
|
430
496
|
}
|
|
431
497
|
else {
|
|
432
|
-
this.rejectAggregates({ select, resolvedSelect, having: null, resolvedHaving: null, orderBy, froms, where: resolvedWhere, frontier, limit, offset, groupBy: null });
|
|
498
|
+
this.rejectAggregates({ select, resolvedSelect, having: null, resolvedHaving: null, orderBy, froms, where: resolvedWhere, frontier, flatten, limit, offset, groupBy: null });
|
|
433
499
|
}
|
|
434
|
-
return { froms, where, select, orderBy, follow, frontier, depth, groupBy, having, distinct, values, limit, offset };
|
|
500
|
+
return { froms, where, select, orderBy, follow, frontier, depth, flatten, groupBy, having, distinct, values, limit, offset };
|
|
501
|
+
}
|
|
502
|
+
// ---- flatten (0.25) -------------------------------------------------------
|
|
503
|
+
// `flatten` is a clause when a value can follow the word; otherwise it is a
|
|
504
|
+
// field name (`select flatten from r`).
|
|
505
|
+
atFlatten() {
|
|
506
|
+
if (!this.at("ident", "flatten"))
|
|
507
|
+
return false;
|
|
508
|
+
const nx = this.peekAt(1);
|
|
509
|
+
// A value start — or an aggregate call whose name is a consumer word
|
|
510
|
+
// (`flatten collect(x)`), admitted so the aggregate check can name it.
|
|
511
|
+
return this.startsValue(nx) || (nx?.type === "ident" && isAggregateName(nx.value) && this.peekAt(2)?.type === "lparen");
|
|
512
|
+
}
|
|
513
|
+
// `flatten <expr> [as <name>]`. The expression is any value — a block after a
|
|
514
|
+
// receiver is read by the postfix grammar as the implicit `collect` op (0.20),
|
|
515
|
+
// which is exactly the block over the coerced collection SEMANTICS §25 wants.
|
|
516
|
+
// The name defaults to a plain navigation's last segment (as a select item's
|
|
517
|
+
// or a group key's does); anything else needs `as`. Names are unique within
|
|
518
|
+
// the body, never a `$`-name (the language's), a keyword or a literal word.
|
|
519
|
+
parseFlatten(earlier) {
|
|
520
|
+
const start = this.start();
|
|
521
|
+
this.next(); // `flatten`
|
|
522
|
+
const expr = this.parseValueExpr();
|
|
523
|
+
let name;
|
|
524
|
+
if (this.at("ident", "as")) {
|
|
525
|
+
this.next();
|
|
526
|
+
const t = this.peek();
|
|
527
|
+
if (t.type !== "ident")
|
|
528
|
+
this.fail("expected a name after `as` (`flatten <expr> as <name>`)");
|
|
529
|
+
if (LITERAL_WORDS.has(t.value))
|
|
530
|
+
this.fail(`\`${t.value}\` is a literal and cannot name a flattened value (\`flatten <expr> as <name>\`)`);
|
|
531
|
+
if (t.value.startsWith("$"))
|
|
532
|
+
this.fail(`a flattened value cannot be bound to a \`$\`-name (\`${t.value}\`) — \`$\` belongs to the language; choose another name`);
|
|
533
|
+
name = this.next().value;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
const key = navKey(expr);
|
|
537
|
+
if (key === null) {
|
|
538
|
+
this.failAt(start, `\`flatten ${describe(expr)}\` needs a name (\`flatten ${describe(expr)} as <name>\`) — only a plain navigation names itself by its last segment`);
|
|
539
|
+
}
|
|
540
|
+
name = key;
|
|
541
|
+
}
|
|
542
|
+
if (earlier.some((f) => f.name === name))
|
|
543
|
+
this.failAt(start, `duplicate flatten name '${name}' — each flatten binds its own name (\`flatten <expr> as <other>\`)`);
|
|
544
|
+
return { kind: "flatten", span: this.sp(start), expr, name };
|
|
435
545
|
}
|
|
436
546
|
// ---- grouping (0.24) ------------------------------------------------------
|
|
437
547
|
// `group by` is a clause only as the two-word sequence (a field named `group`
|
|
@@ -542,6 +652,7 @@ class Parser {
|
|
|
542
652
|
found.push([a, "`where`"]);
|
|
543
653
|
collect(b.froms, "`from`");
|
|
544
654
|
collect([b.frontier], "`frontier`");
|
|
655
|
+
collect(b.flatten.map((f) => f.expr), "`flatten`");
|
|
545
656
|
collect([b.limit], "`limit`");
|
|
546
657
|
collect([b.offset], "`offset`");
|
|
547
658
|
if (b.groupBy)
|
|
@@ -750,6 +861,8 @@ class Parser {
|
|
|
750
861
|
this.fail("expected an integer after `depth` (`depth 1`..`depth 8`)");
|
|
751
862
|
if (t.value === "frontier")
|
|
752
863
|
this.fail(`unexpected 'frontier' after \`${last}\` — \`frontier\` takes an expression (\`frontier <expr>\`) and belongs to a body that has a \`follow\``);
|
|
864
|
+
if (t.value === "flatten")
|
|
865
|
+
this.fail(`unexpected 'flatten' after \`${last}\` — \`flatten\` takes a value to split into rows (\`flatten <expr> [{ … }] [as <name>]\`)`);
|
|
753
866
|
if (t.value === "group")
|
|
754
867
|
this.fail(`unexpected 'group' after \`${last}\` — a grouping is written \`group by <key>[, <key>…]\` (a key is a name or \`alias: <expr>\`)`);
|
|
755
868
|
if (t.value === "having")
|
|
@@ -966,7 +1079,7 @@ class Parser {
|
|
|
966
1079
|
if (it.kind === "collect")
|
|
967
1080
|
this.fail("a nested block in a projection needs a name (`name: <relation> collect { … }`); as a predicate, a consumer test is written `where <relation> exists { … }`");
|
|
968
1081
|
if (ctx.top)
|
|
969
|
-
this.fail("a leading expression is a projection (select): an item that is not a plain name needs an alias (`name: expr`) or `values`; to filter by it write `where …` after `from` — at the top level a predicate is never implicit (the clause order puts `from` first)");
|
|
1082
|
+
this.fail("a leading expression is a projection (select): an item that is not a plain name needs an alias (`name: expr`) or `values`; to filter by it write `<collection> { <predicate> }`, or write `where …` after `from` — at the top level of the clause form a predicate is never implicit (the clause order puts `from` first)");
|
|
970
1083
|
this.fail("an item that is not a plain name needs an alias (`name: expr`) or `values`; to filter by it write `where …` after the projection (a block may lead with a predicate only when nothing is projected: `{ age > 18 }`)");
|
|
971
1084
|
}
|
|
972
1085
|
const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
|
|
@@ -996,8 +1109,9 @@ class Parser {
|
|
|
996
1109
|
this.next(); // ':'
|
|
997
1110
|
const op = this.tryOp(false);
|
|
998
1111
|
if (op) {
|
|
999
|
-
// `name: jobs first { }.pay`: the directive is the head of a longer value
|
|
1000
|
-
|
|
1112
|
+
// `name: jobs first { }.pay`: the directive is the head of a longer value
|
|
1113
|
+
// (a block over it, `jobs[0] { name }`, merges into it — 0.26).
|
|
1114
|
+
if (this.atScalarContinuation() || this.atBlock() !== null)
|
|
1001
1115
|
return this.finishItem(name, this.parseOr(op), lift, start);
|
|
1002
1116
|
if (op.op !== "collect" && op.op !== "first" && op.op !== "single" && op.op !== "count") {
|
|
1003
1117
|
this.fail(`projection '${name}' must use collect/first/single/count, not ${op.op} (exists/none are where-position tests; \`count { … }\` is a value since 0.24)`);
|
|
@@ -1298,14 +1412,19 @@ class Parser {
|
|
|
1298
1412
|
return this.atConsumerBlock();
|
|
1299
1413
|
}
|
|
1300
1414
|
// The cursor is at a consumer word that opens a block — `<op> {` or
|
|
1301
|
-
// `<op> distinct {
|
|
1415
|
+
// `<op> distinct {` — or, since 0.26, at the word alone: a consumer word
|
|
1416
|
+
// right after a complete receiver can be nothing else (OQX never places two
|
|
1417
|
+
// operands side by side), and it reads as the directive over the empty block
|
|
1418
|
+
// (`jobs first` ≡ `jobs first { }`, `_nodes exists`, `jobs count > 1`). The
|
|
1419
|
+
// braceless reading is off only inside the clause form's top-level `from`
|
|
1420
|
+
// (`bareDirectives`). Consumes nothing.
|
|
1302
1421
|
atConsumerBlock() {
|
|
1303
1422
|
if (!this.at("ident") || !CONSUMERS.has(this.peek().value))
|
|
1304
1423
|
return null;
|
|
1305
1424
|
const after = this.peekAt(1);
|
|
1306
1425
|
const opThenBrace = after?.type === "lbrace";
|
|
1307
1426
|
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
1308
|
-
return opThenBrace || opDistinctBrace ? this.peek().value : null;
|
|
1427
|
+
return opThenBrace || opDistinctBrace || this.bareDirectives ? this.peek().value : null;
|
|
1309
1428
|
}
|
|
1310
1429
|
// `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
|
|
1311
1430
|
// at the consumer word, at `distinct`, or at the `{` of the implicit `collect`.
|
|
@@ -1320,22 +1439,35 @@ class Parser {
|
|
|
1320
1439
|
this.next();
|
|
1321
1440
|
distinct = true;
|
|
1322
1441
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
this.
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
this.
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1442
|
+
let body;
|
|
1443
|
+
let subSpan;
|
|
1444
|
+
if (this.at("lbrace")) {
|
|
1445
|
+
const lbrace = this.next().pos; // '{'
|
|
1446
|
+
const defer = this.deferLookup;
|
|
1447
|
+
this.deferLookup = false;
|
|
1448
|
+
// The receiver was read at the enclosing depth; the block's rows are one deeper.
|
|
1449
|
+
body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect", destination }));
|
|
1450
|
+
this.deferLookup = defer;
|
|
1451
|
+
if (!this.at("rbrace"))
|
|
1452
|
+
this.fail(`expected '}' to close the ${op} { … } block`);
|
|
1453
|
+
this.next();
|
|
1454
|
+
// `x collect distinct { group by k }`: the directive's distinct is as
|
|
1455
|
+
// redundant as the body's own (checked in parseBody).
|
|
1456
|
+
if (distinct && body.groupBy)
|
|
1457
|
+
this.failAt(body.groupBy.span[0], DISTINCT_WITH_GROUP);
|
|
1458
|
+
subSpan = this.sp(lbrace);
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
// A consumer word with no block (0.26) is the directive over the empty
|
|
1462
|
+
// block `{ }` — the word was written (only a brace makes a bare
|
|
1463
|
+
// `collect`), so nothing more is consumed; the block spans nothing, at
|
|
1464
|
+
// the end of the word.
|
|
1465
|
+
body = emptyBody();
|
|
1466
|
+
subSpan = [this.end(), this.end()];
|
|
1467
|
+
}
|
|
1336
1468
|
const sub = {
|
|
1337
1469
|
kind: "subquery",
|
|
1338
|
-
span:
|
|
1470
|
+
span: subSpan,
|
|
1339
1471
|
from: body.froms,
|
|
1340
1472
|
where: body.where,
|
|
1341
1473
|
select: body.select,
|
|
@@ -1343,13 +1475,45 @@ class Parser {
|
|
|
1343
1475
|
follow: body.follow,
|
|
1344
1476
|
frontier: body.frontier,
|
|
1345
1477
|
depth: body.depth,
|
|
1478
|
+
flatten: body.flatten,
|
|
1346
1479
|
groupBy: body.groupBy,
|
|
1347
1480
|
having: body.having,
|
|
1348
1481
|
values: body.values,
|
|
1349
1482
|
limit: body.limit,
|
|
1350
1483
|
offset: body.offset,
|
|
1351
1484
|
};
|
|
1352
|
-
return
|
|
1485
|
+
return this.mergeProjectionBlock(receiver, op, distinct || body.distinct, sub, startPos);
|
|
1486
|
+
}
|
|
1487
|
+
// A projection-only block over a `collect`/`first`/`single` directive whose
|
|
1488
|
+
// own block projects nothing IS that directive's projection (0.26):
|
|
1489
|
+
// `jobs[0] { name }` ≡ `jobs first { name offset 0 }`, `jobs[p] { name }` ≡
|
|
1490
|
+
// `jobs first { name where p }`, `jobs[0] { }` ≡ `jobs[0]`. The outer items
|
|
1491
|
+
// are read on the very rows the inner block filtered, at the same depth, so
|
|
1492
|
+
// the merge is exact — except when the outer block carries a clause of its
|
|
1493
|
+
// own (those run AFTER the pick: `jobs[0] { name where pay > 2 }` is the
|
|
1494
|
+
// first job, kept only if it pays), when the inner directive dedups
|
|
1495
|
+
// (`distinct` compares the projection), or when an outer alias would shadow a
|
|
1496
|
+
// bare name the inner `where` reads (`jobs[pay > 2] { pay: employer }`): then
|
|
1497
|
+
// the two directives stay.
|
|
1498
|
+
mergeProjectionBlock(receiver, op, distinct, sub, startPos) {
|
|
1499
|
+
const plain = { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct };
|
|
1500
|
+
if (receiver.kind !== "op" || op !== "collect")
|
|
1501
|
+
return plain;
|
|
1502
|
+
const inner = receiver;
|
|
1503
|
+
if ((inner.op !== "collect" && inner.op !== "first" && inner.op !== "single") || inner.distinct || inner.countCmp !== null)
|
|
1504
|
+
return plain;
|
|
1505
|
+
if (inner.sub.select.length > 0 || inner.sub.values)
|
|
1506
|
+
return plain;
|
|
1507
|
+
const projectionOnly = sub.from.length === 0 && sub.where === null && sub.orderBy === null && sub.follow === null && sub.frontier === null
|
|
1508
|
+
&& sub.depth === null && sub.flatten.length === 0 && sub.groupBy === null && sub.having === null && sub.limit === null && sub.offset === null;
|
|
1509
|
+
if (!projectionOnly)
|
|
1510
|
+
return plain;
|
|
1511
|
+
if (inner.sub.where && readsAnyOf(inner.sub.where, shadowingNames(sub.select)))
|
|
1512
|
+
return plain;
|
|
1513
|
+
return {
|
|
1514
|
+
kind: "op", span: this.sp(startPos), receiver: inner.receiver, op: inner.op, countCmp: null, distinct,
|
|
1515
|
+
sub: { ...inner.sub, select: sub.select, values: sub.values, span: [inner.sub.span[0], sub.span[1]] },
|
|
1516
|
+
};
|
|
1353
1517
|
}
|
|
1354
1518
|
// A tuple `(e1, e2, …)` at the cursor's `(` (0.20): two or more comma-separated
|
|
1355
1519
|
// expressions. A single parenthesized expression is grouping, not a tuple —
|
|
@@ -1416,7 +1580,7 @@ class Parser {
|
|
|
1416
1580
|
this.next();
|
|
1417
1581
|
const subSpan = [lbracket, this.end()];
|
|
1418
1582
|
const sub = (parts) => ({
|
|
1419
|
-
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, frontier: null, depth: null, groupBy: null, having: null, values: false, limit: null, offset: null, ...parts,
|
|
1583
|
+
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, frontier: null, depth: null, flatten: [], groupBy: null, having: null, values: false, limit: null, offset: null, ...parts,
|
|
1420
1584
|
});
|
|
1421
1585
|
const bang = this.atOp("!") ? this.next() : null;
|
|
1422
1586
|
if (index) {
|