@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/src/parser.ts
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
|
|
@@ -83,10 +93,11 @@ import { lexTemplate, lexString } from "./lexer.ts";
|
|
|
83
93
|
import { OqxError } from "./errors.ts";
|
|
84
94
|
import type {
|
|
85
95
|
Query, Where, Expr, OpNode, Subquery, SelectItem, OrderSpec, Follow, FollowDestination, Consumer, RelOp,
|
|
86
|
-
BinaryOp, Span, TemplatePart, GroupBy, GroupKey, AggregateName,
|
|
96
|
+
BinaryOp, Span, TemplatePart, Flatten, GroupBy, GroupKey, AggregateName,
|
|
87
97
|
} from "./ast.ts";
|
|
88
98
|
import { AGGREGATE_NAMES, isAggregateName } from "./ast.ts";
|
|
89
99
|
import { resolveSelect, resolveWhere } from "./resolve.ts";
|
|
100
|
+
import { visit } from "./walk.ts";
|
|
90
101
|
import { printTemplate } from "./print.ts";
|
|
91
102
|
|
|
92
103
|
const CONSUMERS = new Set<string>(["collect", "exists", "none", "count", "first", "single"]);
|
|
@@ -96,8 +107,11 @@ const CONSUMERS = new Set<string>(["collect", "exists", "none", "count", "first"
|
|
|
96
107
|
// `depth` is legal only in a follow destination block, and the order sentence
|
|
97
108
|
// names it only there. `group by` and `having` (0.24) sit after `follow`: the
|
|
98
109
|
// rows a body yields — the walk's occurrences, when it has one — are what gets
|
|
99
|
-
// grouped, and the bound and the ordering then apply to the groups.
|
|
100
|
-
|
|
110
|
+
// grouped, and the bound and the ordering then apply to the groups. `flatten`
|
|
111
|
+
// (0.25) sits between them: it fans the rows a body yields — the occurrences,
|
|
112
|
+
// after a walk — out per element, and is the one clause that may repeat (each
|
|
113
|
+
// `flatten` fans out the rows the previous produced, SEMANTICS §25).
|
|
114
|
+
const CLAUSE_ORDER = ["select", "from", "where", "frontier", "depth", "follow", "flatten", "group by", "having", "order by", "limit", "offset"] as const;
|
|
101
115
|
type Clause = (typeof CLAUSE_ORDER)[number];
|
|
102
116
|
const DESTINATION_ONLY: ReadonlySet<Clause> = new Set<Clause>(["depth"]);
|
|
103
117
|
// The clauses a body may hold, in order.
|
|
@@ -109,7 +123,7 @@ function clausesOf(ctx: BodyCtx): Clause[] {
|
|
|
109
123
|
const CLAUSE_WORDS = new Set<string>([
|
|
110
124
|
"collect", "exists", "none", "count", "first", "single",
|
|
111
125
|
"order", "by", "asc", "desc", "follow", "distinct", "frontier", "depth", "in", "values",
|
|
112
|
-
"limit", "offset", "group", "having",
|
|
126
|
+
"limit", "offset", "group", "having", "flatten", "as",
|
|
113
127
|
]);
|
|
114
128
|
// The names a group scope resolves besides its keys (SEMANTICS §24): the
|
|
115
129
|
// group's rows, the key record itself, and its identity.
|
|
@@ -158,6 +172,7 @@ interface BodyClauses {
|
|
|
158
172
|
follow: Follow | null;
|
|
159
173
|
frontier: Expr | null;
|
|
160
174
|
depth: number | null;
|
|
175
|
+
flatten: Flatten[];
|
|
161
176
|
groupBy: GroupBy | null;
|
|
162
177
|
having: Where | null;
|
|
163
178
|
distinct: boolean;
|
|
@@ -166,6 +181,30 @@ interface BodyClauses {
|
|
|
166
181
|
offset: Expr | null;
|
|
167
182
|
}
|
|
168
183
|
|
|
184
|
+
/** The clauses of an omitted block (`jobs first` — the empty block `{ }`). */
|
|
185
|
+
function emptyBody(): BodyClauses {
|
|
186
|
+
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 };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The names a projection would shadow in a `where` of the same body: every
|
|
190
|
+
* item's name except a plain self-projection (`pay: pay`, `pay`), which
|
|
191
|
+
* substitutes a name for itself. */
|
|
192
|
+
function shadowingNames(items: SelectItem[]): string[] {
|
|
193
|
+
return items
|
|
194
|
+
.filter((it) => it.name !== "" && !(it.kind === "field" && it.lift === 0 && it.expr.kind === "ident" && it.expr.name === it.name))
|
|
195
|
+
.map((it) => it.name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Whether a bare identifier anywhere under `w` has one of `names` (the alias
|
|
199
|
+
* guard of the projection-block merge; conservative: nested blocks included). */
|
|
200
|
+
function readsAnyOf(w: Where, names: string[]): boolean {
|
|
201
|
+
const wanted = new Set(names.filter((n) => n !== ""));
|
|
202
|
+
if (wanted.size === 0) return false;
|
|
203
|
+
let found = false;
|
|
204
|
+
visit(w, { enter(node) { if (node.kind === "ident" && wanted.has(node.name)) found = true; return !found; } });
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
|
|
169
208
|
// A copy of a node with another span (a parenthesized operand takes the span of
|
|
170
209
|
// its parentheses).
|
|
171
210
|
function withSpan<T extends { span: Span }>(node: T, span: Span): T {
|
|
@@ -184,6 +223,11 @@ class Parser {
|
|
|
184
223
|
* top-level row's clauses, one deeper per block body and per follow `where`.
|
|
185
224
|
* An absolute reference `N^name` reads `depth − N` scopes out. */
|
|
186
225
|
private depth = 0;
|
|
226
|
+
/** Whether a consumer word with no block is read as a directive over the
|
|
227
|
+
* empty block (`jobs first` ≡ `jobs first { }`, since 0.26). Off only while
|
|
228
|
+
* the clause form's top-level `from` expression is read, so `from people
|
|
229
|
+
* count` keeps its whole-query hint (a count is never a source). */
|
|
230
|
+
private bareDirectives = true;
|
|
187
231
|
|
|
188
232
|
constructor(tokens: Token[]) {
|
|
189
233
|
this.tokens = tokens;
|
|
@@ -226,9 +270,10 @@ class Parser {
|
|
|
226
270
|
const start = this.start();
|
|
227
271
|
// Directive form: `<receiver> <consumer> { … }` consuming the whole query.
|
|
228
272
|
let directive = this.tryOp(false);
|
|
229
|
-
if (directive && !this.at("eof") && (this.at("lbracket") || this.
|
|
273
|
+
if (directive && !this.at("eof") && (this.at("lbracket") || this.atBlock() !== null)) {
|
|
230
274
|
// A longer chain whose outermost node is still a directive
|
|
231
|
-
// (`people first { } first { }` — how `people[p][0]` prints back
|
|
275
|
+
// (`people first { } first { }` — how `people[p][0]` prints back; a block
|
|
276
|
+
// over the directive, `people[0] { name }`, merges into it, 0.26).
|
|
232
277
|
const save = this.pos;
|
|
233
278
|
try {
|
|
234
279
|
const e = this.parsePostfix(directive);
|
|
@@ -260,7 +305,7 @@ class Parser {
|
|
|
260
305
|
const body = this.parseBody(TOP_CTX);
|
|
261
306
|
if (!this.at("eof")) this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause`);
|
|
262
307
|
if (body.froms.length === 0) {
|
|
263
|
-
this.fail("a query must name its source
|
|
308
|
+
this.fail("a query must name its source before its body: `<collection> { … }` (or `<collection> <consumer> { … }`; the clause form `select … from <collection> …` is still read)");
|
|
264
309
|
}
|
|
265
310
|
return {
|
|
266
311
|
kind: "query",
|
|
@@ -273,6 +318,7 @@ class Parser {
|
|
|
273
318
|
consumer: "collect",
|
|
274
319
|
follow: body.follow,
|
|
275
320
|
frontier: body.frontier,
|
|
321
|
+
flatten: body.flatten,
|
|
276
322
|
groupBy: body.groupBy,
|
|
277
323
|
having: body.having,
|
|
278
324
|
distinct: body.distinct,
|
|
@@ -287,7 +333,7 @@ class Parser {
|
|
|
287
333
|
private queryFromOp(e: OpNode, start: number): Query {
|
|
288
334
|
return {
|
|
289
335
|
kind: "query", span: this.sp(start), source: e.receiver, from: e.sub.from, where: e.sub.where, select: e.sub.select,
|
|
290
|
-
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, frontier: e.sub.frontier, groupBy: e.sub.groupBy, having: e.sub.having,
|
|
336
|
+
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,
|
|
291
337
|
distinct: e.distinct, values: e.sub.values, limit: e.sub.limit, offset: e.sub.offset,
|
|
292
338
|
};
|
|
293
339
|
}
|
|
@@ -310,6 +356,7 @@ class Parser {
|
|
|
310
356
|
let follow: Follow | null = null;
|
|
311
357
|
let frontier: Expr | null = null;
|
|
312
358
|
let depth: number | null = null;
|
|
359
|
+
const flatten: Flatten[] = [];
|
|
313
360
|
let groupBy: GroupBy | null = null;
|
|
314
361
|
let having: Where | null = null;
|
|
315
362
|
let distinct = false;
|
|
@@ -324,7 +371,9 @@ class Parser {
|
|
|
324
371
|
|
|
325
372
|
const enter = (clause: Clause): void => {
|
|
326
373
|
const idx = CLAUSE_ORDER.indexOf(clause);
|
|
327
|
-
|
|
374
|
+
// `flatten` is the one clause that may repeat (0.25): each fans out the
|
|
375
|
+
// rows the previous produced.
|
|
376
|
+
if (idx === stage && clause !== "flatten") this.fail(`duplicate \`${clause}\` clause`);
|
|
328
377
|
if (idx < stage) {
|
|
329
378
|
this.fail(`\`${clause}\` must come before \`${CLAUSE_ORDER[stage]}\` — OQX clause order is ${clausesOf(ctx).join(", ")}`);
|
|
330
379
|
}
|
|
@@ -342,7 +391,12 @@ class Parser {
|
|
|
342
391
|
if (this.at("kw", "from")) {
|
|
343
392
|
enter("from");
|
|
344
393
|
this.next();
|
|
345
|
-
|
|
394
|
+
// At the clause form's top level a consumer word after the source is the
|
|
395
|
+
// whole-query mistake (`from people count`): kept as the hint below,
|
|
396
|
+
// not read as a braceless directive (a count is never a source).
|
|
397
|
+
const bare = this.bareDirectives;
|
|
398
|
+
if (ctx.top) this.bareDirectives = false;
|
|
399
|
+
try { froms.push(this.parseValueExpr()); } finally { this.bareDirectives = bare; }
|
|
346
400
|
continue;
|
|
347
401
|
}
|
|
348
402
|
if (this.at("kw", "where")) {
|
|
@@ -377,6 +431,14 @@ class Parser {
|
|
|
377
431
|
follow = this.atDepth(rowDepth, () => this.parseFollow());
|
|
378
432
|
continue;
|
|
379
433
|
}
|
|
434
|
+
// `flatten <expr> [{ … }] [as <name>]` (0.25): the value is read on each
|
|
435
|
+
// row, like `where`; a brace after a receiver is the receiver-block sugar
|
|
436
|
+
// (the `collect` op), so the block needs no slot of its own.
|
|
437
|
+
if (this.atFlatten()) {
|
|
438
|
+
enter("flatten");
|
|
439
|
+
flatten.push(this.atDepth(rowDepth, () => this.parseFlatten(flatten)));
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
380
442
|
// `group by k, name: expr` (0.24): the keys are read on the rows, like `where`.
|
|
381
443
|
if (this.atGroupBy()) {
|
|
382
444
|
enter("group by");
|
|
@@ -451,11 +513,52 @@ class Parser {
|
|
|
451
513
|
}
|
|
452
514
|
if (groupBy) {
|
|
453
515
|
if (distinct) this.failAt(groupBy.span[0], DISTINCT_WITH_GROUP);
|
|
454
|
-
this.checkGroupedBody({ select, resolvedSelect, having, resolvedHaving, orderBy, froms, where: resolvedWhere, frontier, limit, offset, groupBy });
|
|
516
|
+
this.checkGroupedBody({ select, resolvedSelect, having, resolvedHaving, orderBy, froms, where: resolvedWhere, frontier, flatten, limit, offset, groupBy });
|
|
455
517
|
} else {
|
|
456
|
-
this.rejectAggregates({ select, resolvedSelect, having: null, resolvedHaving: null, orderBy, froms, where: resolvedWhere, frontier, limit, offset, groupBy: null });
|
|
518
|
+
this.rejectAggregates({ select, resolvedSelect, having: null, resolvedHaving: null, orderBy, froms, where: resolvedWhere, frontier, flatten, limit, offset, groupBy: null });
|
|
457
519
|
}
|
|
458
|
-
return { froms, where, select, orderBy, follow, frontier, depth, groupBy, having, distinct, values, limit, offset };
|
|
520
|
+
return { froms, where, select, orderBy, follow, frontier, depth, flatten, groupBy, having, distinct, values, limit, offset };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ---- flatten (0.25) -------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
// `flatten` is a clause when a value can follow the word; otherwise it is a
|
|
526
|
+
// field name (`select flatten from r`).
|
|
527
|
+
private atFlatten(): boolean {
|
|
528
|
+
if (!this.at("ident", "flatten")) return false;
|
|
529
|
+
const nx = this.peekAt(1);
|
|
530
|
+
// A value start — or an aggregate call whose name is a consumer word
|
|
531
|
+
// (`flatten collect(x)`), admitted so the aggregate check can name it.
|
|
532
|
+
return this.startsValue(nx) || (nx?.type === "ident" && isAggregateName(nx.value) && this.peekAt(2)?.type === "lparen");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// `flatten <expr> [as <name>]`. The expression is any value — a block after a
|
|
536
|
+
// receiver is read by the postfix grammar as the implicit `collect` op (0.20),
|
|
537
|
+
// which is exactly the block over the coerced collection SEMANTICS §25 wants.
|
|
538
|
+
// The name defaults to a plain navigation's last segment (as a select item's
|
|
539
|
+
// or a group key's does); anything else needs `as`. Names are unique within
|
|
540
|
+
// the body, never a `$`-name (the language's), a keyword or a literal word.
|
|
541
|
+
private parseFlatten(earlier: readonly Flatten[]): Flatten {
|
|
542
|
+
const start = this.start();
|
|
543
|
+
this.next(); // `flatten`
|
|
544
|
+
const expr = this.parseValueExpr();
|
|
545
|
+
let name: string;
|
|
546
|
+
if (this.at("ident", "as")) {
|
|
547
|
+
this.next();
|
|
548
|
+
const t = this.peek();
|
|
549
|
+
if (t.type !== "ident") this.fail("expected a name after `as` (`flatten <expr> as <name>`)");
|
|
550
|
+
if (LITERAL_WORDS.has(t.value)) this.fail(`\`${t.value}\` is a literal and cannot name a flattened value (\`flatten <expr> as <name>\`)`);
|
|
551
|
+
if (t.value.startsWith("$")) this.fail(`a flattened value cannot be bound to a \`$\`-name (\`${t.value}\`) — \`$\` belongs to the language; choose another name`);
|
|
552
|
+
name = this.next().value;
|
|
553
|
+
} else {
|
|
554
|
+
const key = navKey(expr);
|
|
555
|
+
if (key === null) {
|
|
556
|
+
this.failAt(start, `\`flatten ${describe(expr)}\` needs a name (\`flatten ${describe(expr)} as <name>\`) — only a plain navigation names itself by its last segment`);
|
|
557
|
+
}
|
|
558
|
+
name = key;
|
|
559
|
+
}
|
|
560
|
+
if (earlier.some((f) => f.name === name)) this.failAt(start, `duplicate flatten name '${name}' — each flatten binds its own name (\`flatten <expr> as <other>\`)`);
|
|
561
|
+
return { kind: "flatten", span: this.sp(start), expr, name };
|
|
459
562
|
}
|
|
460
563
|
|
|
461
564
|
// ---- grouping (0.24) ------------------------------------------------------
|
|
@@ -555,6 +658,7 @@ class Parser {
|
|
|
555
658
|
for (const a of where) found.push([a, "`where`"]);
|
|
556
659
|
collect(b.froms, "`from`");
|
|
557
660
|
collect([b.frontier], "`frontier`");
|
|
661
|
+
collect(b.flatten.map((f) => f.expr), "`flatten`");
|
|
558
662
|
collect([b.limit], "`limit`");
|
|
559
663
|
collect([b.offset], "`offset`");
|
|
560
664
|
if (b.groupBy) collect(b.groupBy.keys.map((k) => k.expr), "a `group by` key");
|
|
@@ -713,6 +817,7 @@ class Parser {
|
|
|
713
817
|
}
|
|
714
818
|
if (t.value === "depth") this.fail("expected an integer after `depth` (`depth 1`..`depth 8`)");
|
|
715
819
|
if (t.value === "frontier") this.fail(`unexpected 'frontier' after \`${last}\` — \`frontier\` takes an expression (\`frontier <expr>\`) and belongs to a body that has a \`follow\``);
|
|
820
|
+
if (t.value === "flatten") this.fail(`unexpected 'flatten' after \`${last}\` — \`flatten\` takes a value to split into rows (\`flatten <expr> [{ … }] [as <name>]\`)`);
|
|
716
821
|
if (t.value === "group") this.fail(`unexpected 'group' after \`${last}\` — a grouping is written \`group by <key>[, <key>…]\` (a key is a name or \`alias: <expr>\`)`);
|
|
717
822
|
if (t.value === "having") this.fail(`unexpected 'having' after \`${last}\` — \`having\` takes a predicate over each group (\`having count() > 1\`) and belongs to a body that has a \`group by\``);
|
|
718
823
|
}
|
|
@@ -921,7 +1026,7 @@ class Parser {
|
|
|
921
1026
|
for (const it of items) {
|
|
922
1027
|
if (it.name === "") {
|
|
923
1028
|
if (it.kind === "collect") 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 { … }`");
|
|
924
|
-
if (ctx.top) 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)");
|
|
1029
|
+
if (ctx.top) 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)");
|
|
925
1030
|
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 }`)");
|
|
926
1031
|
}
|
|
927
1032
|
const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
|
|
@@ -950,8 +1055,9 @@ class Parser {
|
|
|
950
1055
|
this.next(); // ':'
|
|
951
1056
|
const op = this.tryOp(false);
|
|
952
1057
|
if (op) {
|
|
953
|
-
// `name: jobs first { }.pay`: the directive is the head of a longer value
|
|
954
|
-
|
|
1058
|
+
// `name: jobs first { }.pay`: the directive is the head of a longer value
|
|
1059
|
+
// (a block over it, `jobs[0] { name }`, merges into it — 0.26).
|
|
1060
|
+
if (this.atScalarContinuation() || this.atBlock() !== null) return this.finishItem(name, this.parseOr(op), lift, start);
|
|
955
1061
|
if (op.op !== "collect" && op.op !== "first" && op.op !== "single" && op.op !== "count") {
|
|
956
1062
|
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)`);
|
|
957
1063
|
}
|
|
@@ -1208,13 +1314,18 @@ class Parser {
|
|
|
1208
1314
|
}
|
|
1209
1315
|
|
|
1210
1316
|
// The cursor is at a consumer word that opens a block — `<op> {` or
|
|
1211
|
-
// `<op> distinct {
|
|
1317
|
+
// `<op> distinct {` — or, since 0.26, at the word alone: a consumer word
|
|
1318
|
+
// right after a complete receiver can be nothing else (OQX never places two
|
|
1319
|
+
// operands side by side), and it reads as the directive over the empty block
|
|
1320
|
+
// (`jobs first` ≡ `jobs first { }`, `_nodes exists`, `jobs count > 1`). The
|
|
1321
|
+
// braceless reading is off only inside the clause form's top-level `from`
|
|
1322
|
+
// (`bareDirectives`). Consumes nothing.
|
|
1212
1323
|
private atConsumerBlock(): Consumer | null {
|
|
1213
1324
|
if (!this.at("ident") || !CONSUMERS.has(this.peek().value)) return null;
|
|
1214
1325
|
const after = this.peekAt(1);
|
|
1215
1326
|
const opThenBrace = after?.type === "lbrace";
|
|
1216
1327
|
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
1217
|
-
return opThenBrace || opDistinctBrace ? (this.peek().value as Consumer) : null;
|
|
1328
|
+
return opThenBrace || opDistinctBrace || this.bareDirectives ? (this.peek().value as Consumer) : null;
|
|
1218
1329
|
}
|
|
1219
1330
|
|
|
1220
1331
|
// `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
|
|
@@ -1226,20 +1337,32 @@ class Parser {
|
|
|
1226
1337
|
if (this.at("ident") && CONSUMERS.has(this.peek().value)) this.next(); // the consumer word (absent for `x { … }`)
|
|
1227
1338
|
let distinct = false;
|
|
1228
1339
|
if (this.at("ident", "distinct")) { this.next(); distinct = true; }
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
this.
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1340
|
+
let body: BodyClauses;
|
|
1341
|
+
let subSpan: Span;
|
|
1342
|
+
if (this.at("lbrace")) {
|
|
1343
|
+
const lbrace = this.next().pos; // '{'
|
|
1344
|
+
const defer = this.deferLookup;
|
|
1345
|
+
this.deferLookup = false;
|
|
1346
|
+
// The receiver was read at the enclosing depth; the block's rows are one deeper.
|
|
1347
|
+
body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect", destination }));
|
|
1348
|
+
this.deferLookup = defer;
|
|
1349
|
+
if (!this.at("rbrace")) this.fail(`expected '}' to close the ${op} { … } block`);
|
|
1350
|
+
this.next();
|
|
1351
|
+
// `x collect distinct { group by k }`: the directive's distinct is as
|
|
1352
|
+
// redundant as the body's own (checked in parseBody).
|
|
1353
|
+
if (distinct && body.groupBy) this.failAt(body.groupBy.span[0], DISTINCT_WITH_GROUP);
|
|
1354
|
+
subSpan = this.sp(lbrace);
|
|
1355
|
+
} else {
|
|
1356
|
+
// A consumer word with no block (0.26) is the directive over the empty
|
|
1357
|
+
// block `{ }` — the word was written (only a brace makes a bare
|
|
1358
|
+
// `collect`), so nothing more is consumed; the block spans nothing, at
|
|
1359
|
+
// the end of the word.
|
|
1360
|
+
body = emptyBody();
|
|
1361
|
+
subSpan = [this.end(), this.end()];
|
|
1362
|
+
}
|
|
1240
1363
|
const sub: Subquery = {
|
|
1241
1364
|
kind: "subquery",
|
|
1242
|
-
span:
|
|
1365
|
+
span: subSpan,
|
|
1243
1366
|
from: body.froms,
|
|
1244
1367
|
where: body.where,
|
|
1245
1368
|
select: body.select,
|
|
@@ -1247,13 +1370,41 @@ class Parser {
|
|
|
1247
1370
|
follow: body.follow,
|
|
1248
1371
|
frontier: body.frontier,
|
|
1249
1372
|
depth: body.depth,
|
|
1373
|
+
flatten: body.flatten,
|
|
1250
1374
|
groupBy: body.groupBy,
|
|
1251
1375
|
having: body.having,
|
|
1252
1376
|
values: body.values,
|
|
1253
1377
|
limit: body.limit,
|
|
1254
1378
|
offset: body.offset,
|
|
1255
1379
|
};
|
|
1256
|
-
return
|
|
1380
|
+
return this.mergeProjectionBlock(receiver, op, distinct || body.distinct, sub, startPos);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// A projection-only block over a `collect`/`first`/`single` directive whose
|
|
1384
|
+
// own block projects nothing IS that directive's projection (0.26):
|
|
1385
|
+
// `jobs[0] { name }` ≡ `jobs first { name offset 0 }`, `jobs[p] { name }` ≡
|
|
1386
|
+
// `jobs first { name where p }`, `jobs[0] { }` ≡ `jobs[0]`. The outer items
|
|
1387
|
+
// are read on the very rows the inner block filtered, at the same depth, so
|
|
1388
|
+
// the merge is exact — except when the outer block carries a clause of its
|
|
1389
|
+
// own (those run AFTER the pick: `jobs[0] { name where pay > 2 }` is the
|
|
1390
|
+
// first job, kept only if it pays), when the inner directive dedups
|
|
1391
|
+
// (`distinct` compares the projection), or when an outer alias would shadow a
|
|
1392
|
+
// bare name the inner `where` reads (`jobs[pay > 2] { pay: employer }`): then
|
|
1393
|
+
// the two directives stay.
|
|
1394
|
+
private mergeProjectionBlock(receiver: Expr, op: Consumer, distinct: boolean, sub: Subquery, startPos: number): OpNode {
|
|
1395
|
+
const plain: OpNode = { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct };
|
|
1396
|
+
if (receiver.kind !== "op" || op !== "collect") return plain;
|
|
1397
|
+
const inner = receiver;
|
|
1398
|
+
if ((inner.op !== "collect" && inner.op !== "first" && inner.op !== "single") || inner.distinct || inner.countCmp !== null) return plain;
|
|
1399
|
+
if (inner.sub.select.length > 0 || inner.sub.values) return plain;
|
|
1400
|
+
const projectionOnly = sub.from.length === 0 && sub.where === null && sub.orderBy === null && sub.follow === null && sub.frontier === null
|
|
1401
|
+
&& sub.depth === null && sub.flatten.length === 0 && sub.groupBy === null && sub.having === null && sub.limit === null && sub.offset === null;
|
|
1402
|
+
if (!projectionOnly) return plain;
|
|
1403
|
+
if (inner.sub.where && readsAnyOf(inner.sub.where, shadowingNames(sub.select))) return plain;
|
|
1404
|
+
return {
|
|
1405
|
+
kind: "op", span: this.sp(startPos), receiver: inner.receiver, op: inner.op, countCmp: null, distinct,
|
|
1406
|
+
sub: { ...inner.sub, select: sub.select, values: sub.values, span: [inner.sub.span[0], sub.span[1]] },
|
|
1407
|
+
};
|
|
1257
1408
|
}
|
|
1258
1409
|
|
|
1259
1410
|
// A tuple `(e1, e2, …)` at the cursor's `(` (0.20): two or more comma-separated
|
|
@@ -1307,7 +1458,7 @@ class Parser {
|
|
|
1307
1458
|
this.next();
|
|
1308
1459
|
const subSpan: Span = [lbracket, this.end()];
|
|
1309
1460
|
const sub = (parts: Partial<Subquery>): Subquery => ({
|
|
1310
|
-
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,
|
|
1461
|
+
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,
|
|
1311
1462
|
});
|
|
1312
1463
|
const bang = this.atOp("!") ? this.next() : null;
|
|
1313
1464
|
if (index) {
|
|
@@ -1603,6 +1754,7 @@ interface GroupCheck {
|
|
|
1603
1754
|
froms: Expr[];
|
|
1604
1755
|
where: Where | null;
|
|
1605
1756
|
frontier: Expr | null;
|
|
1757
|
+
flatten: readonly Flatten[];
|
|
1606
1758
|
limit: Expr | null;
|
|
1607
1759
|
offset: Expr | null;
|
|
1608
1760
|
groupBy: GroupBy | null;
|
package/src/print.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// The canonical printer (spec/oqx/AST.md §6): the one source text of a tree.
|
|
2
2
|
// Single spaces, double-quoted strings with GRAMMAR §1 escapes, the minimal
|
|
3
|
-
// parentheses the precedence table (GRAMMAR §4) needs,
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// parentheses the precedence table (GRAMMAR §4) needs, the collection-scoped
|
|
4
|
+
// form `<source> { … }` for every query whose source can head a block (since
|
|
5
|
+
// 0.26; `collect` is the bare brace, every other consumer its word), the
|
|
6
|
+
// projection leading its body without `select`, the connectives as the words
|
|
7
|
+
// `and` / `or` (since 0.18; `!` stays `!`), every clause in its canonical
|
|
8
|
+
// position (a destination block's `frontier` and `depth` after its `where`,
|
|
9
|
+
// since 0.20). The law both spec runners enforce over every fixture query:
|
|
8
10
|
//
|
|
9
11
|
// strip(parse(print(parse(q)))) ≡ strip(parse(q))
|
|
10
12
|
//
|
|
@@ -12,15 +14,15 @@
|
|
|
12
14
|
// binding is a value, never source text: `print` throws on one; `printTemplate`
|
|
13
15
|
// emits template fragments around each binding instead.
|
|
14
16
|
|
|
15
|
-
import type { AstNode, Expr, Follow, GroupBy, GroupKey, OpNode, OrderSpec, Query, SelectItem, Subquery, Where } from "./ast.ts";
|
|
17
|
+
import type { AstNode, Expr, Flatten, Follow, GroupBy, GroupKey, OpNode, OrderSpec, Query, SelectItem, Subquery, Where } from "./ast.ts";
|
|
16
18
|
import { OqxError } from "./errors.ts";
|
|
17
19
|
|
|
18
20
|
/** The template form of a tree: `strings` has `count + 1` fragments; the i-th gap
|
|
19
21
|
* stands for the binding `indices[i]` of the printed tree, so a caller re-runs it
|
|
20
22
|
* as `parseTemplate(strings, count)` with the values permuted by `indices`. In a
|
|
21
23
|
* tree straight from `parseTemplate`, `indices` is `0..count-1` unless the
|
|
22
|
-
* canonical
|
|
23
|
-
*
|
|
24
|
+
* canonical form moved a binding past another (`select x: ${1} from ${0}` prints
|
|
25
|
+
* as `${0} { x: ${1} }`). */
|
|
24
26
|
export interface Template {
|
|
25
27
|
strings: string[];
|
|
26
28
|
count: number;
|
|
@@ -83,6 +85,7 @@ function render(n: AstNode): Out {
|
|
|
83
85
|
case "subquery": return block(n);
|
|
84
86
|
case "op": return op(n);
|
|
85
87
|
case "follow": return follow(n);
|
|
88
|
+
case "flatten": return flatten(n);
|
|
86
89
|
case "groupBy": return groupBy(n);
|
|
87
90
|
case "key": return key(n);
|
|
88
91
|
case "order": return order(n);
|
|
@@ -95,29 +98,65 @@ function render(n: AstNode): Out {
|
|
|
95
98
|
// ---- query / bodies ---------------------------------------------------------
|
|
96
99
|
|
|
97
100
|
function query(q: Query): Out {
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
// The collection-scoped form (since 0.26): `<source> [<consumer>] [distinct]
|
|
102
|
+
// { body }` — the source heads the body, `collect` is the bare brace (the
|
|
103
|
+
// directive word is written after an `op` source, where a bare brace is not
|
|
104
|
+
// read as a block), `distinct` stays on the head.
|
|
105
|
+
if (canHeadBlock(q.source)) {
|
|
106
|
+
return cat(expr(q.source), directive(q.consumer, q.distinct, q.source, q));
|
|
107
|
+
}
|
|
108
|
+
// A source that cannot head a block — a literal, a template, a range,
|
|
109
|
+
// arithmetic, a logical or `in` operand, a unary, an aggregate: every one an
|
|
110
|
+
// eval error or a degenerate query — keeps the clause form `select … from S …`
|
|
111
|
+
// (a `collect`; another consumer over such a source is unparseable either way).
|
|
112
|
+
// `select` is always written — except for a leading bare item literally named
|
|
113
|
+
// `distinct`, which the keyword would swallow as the modifier.
|
|
114
|
+
const first = q.select[0];
|
|
115
|
+
const bareDistinct = !q.distinct && first?.kind === "field" && first.name === "distinct"
|
|
116
|
+
&& first.expr.kind === "ident" && first.expr.name === "distinct";
|
|
117
|
+
const clauses: Out[] = [];
|
|
118
|
+
if (q.select.length > 0) {
|
|
119
|
+
const items = projection(q.select, q.values);
|
|
120
|
+
clauses.push(bareDistinct ? items : cat(`select ${q.distinct ? "distinct " : ""}`, items));
|
|
115
121
|
}
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
clauses.push(cat("from ", expr(q.source)));
|
|
123
|
+
clauses.push(...tail(q));
|
|
124
|
+
return join(clauses, " ");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Whether `e { … }` parses with `e` as the receiver (GRAMMAR §5 and the postfix
|
|
128
|
+
// chain of §4): a tuple, or a chain — navigation, calls, `!`, directives — whose
|
|
129
|
+
// head is a bare name, an outer reference, a binding or a free call. A literal
|
|
130
|
+
// or template head is "not a collection"; an operator (`-x`, `a + b`, `a..b`,
|
|
131
|
+
// `a in b`, `a or b`) or an aggregate cannot head a receiver.
|
|
132
|
+
function canHeadBlock(e: Expr): boolean {
|
|
133
|
+
switch (e.kind) {
|
|
134
|
+
case "ident": case "outer": case "binding": case "tuple": return true;
|
|
135
|
+
case "call": return e.recv === null || chainHead(e.recv);
|
|
136
|
+
case "member": return chainHead(e.recv);
|
|
137
|
+
case "required": return chainHead(e.expr);
|
|
138
|
+
case "op": return chainHead(e.receiver);
|
|
139
|
+
default: return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The head of a postfix chain that may take a block: as `canHeadBlock`, but a
|
|
144
|
+
// tuple is a receiver only on its own (`(a, b).x { … }` is not read as one).
|
|
145
|
+
function chainHead(e: Expr): boolean {
|
|
146
|
+
return e.kind !== "tuple" && canHeadBlock(e);
|
|
118
147
|
}
|
|
119
148
|
|
|
120
|
-
|
|
149
|
+
// The text between a receiver and its block: ` { ` for the bare-brace `collect`
|
|
150
|
+
// (since 0.26 the canonical spelling), ` distinct { ` for a distinct collect,
|
|
151
|
+
// ` <op> [distinct] { ` for the other consumers — and for a `collect` over an
|
|
152
|
+
// `op` receiver, where a bare brace right after a written directive is not
|
|
153
|
+
// read as a block (`jobs first { offset 0 } collect { … }`).
|
|
154
|
+
function directiveWord(consumer: OpNode["op"], distinct: boolean, receiver: Expr): string {
|
|
155
|
+
if (consumer === "collect" && receiver.kind !== "op") return distinct ? " distinct " : " ";
|
|
156
|
+
return ` ${consumer}${distinct ? " distinct" : ""} `;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type Body = Pick<Subquery, "select" | "values" | "from" | "where" | "follow" | "flatten" | "groupBy" | "having" | "orderBy" | "limit" | "offset">
|
|
121
160
|
& Partial<Pick<Subquery, "frontier" | "depth">>;
|
|
122
161
|
|
|
123
162
|
// The clauses after `from` (a query) or after the projection (a block): the
|
|
@@ -130,6 +169,7 @@ function tail(b: Omit<Body, "select" | "values">): Out[] {
|
|
|
130
169
|
if (b.frontier) out.push(cat("frontier ", expr(b.frontier)));
|
|
131
170
|
if (b.depth != null) out.push([`depth ${number(b.depth)}`]);
|
|
132
171
|
if (b.follow) out.push(follow(b.follow));
|
|
172
|
+
for (const f of b.flatten) out.push(flatten(f));
|
|
133
173
|
if (b.groupBy) out.push(groupBy(b.groupBy));
|
|
134
174
|
if (b.having) out.push(cat("having ", where(b.having)));
|
|
135
175
|
if (b.orderBy && b.orderBy.length > 0) out.push(cat("order by ", join(b.orderBy.map(order), ", ")));
|
|
@@ -172,17 +212,39 @@ function order(o: OrderSpec): Out {
|
|
|
172
212
|
function op(o: OpNode): Out {
|
|
173
213
|
return cat(
|
|
174
214
|
receiver(o.receiver),
|
|
175
|
-
|
|
176
|
-
block(o.sub),
|
|
215
|
+
directive(o.op, o.distinct, o.receiver, o.sub),
|
|
177
216
|
o.countCmp ? ` ${o.countCmp.op} ${number(o.countCmp.value)}` : "",
|
|
178
217
|
);
|
|
179
218
|
}
|
|
180
219
|
|
|
220
|
+
// A directive's word and block after its receiver: the bare brace for
|
|
221
|
+
// `collect`, the word for every other consumer — and, when the word is written
|
|
222
|
+
// and the block is empty, the word alone (`jobs first`, `_nodes exists`, `jobs
|
|
223
|
+
// count distinct`; since 0.26 an omitted block is the empty block).
|
|
224
|
+
function directive(consumer: OpNode["op"], distinct: boolean, receiver: Expr, b: Body): Out {
|
|
225
|
+
const word = directiveWord(consumer, distinct, receiver);
|
|
226
|
+
if (word !== " " && word !== " distinct " && emptyBody(b)) return [word.trimEnd()];
|
|
227
|
+
return cat(word, braces(body(b)));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function emptyBody(b: Body): boolean {
|
|
231
|
+
return b.select.length === 0 && !b.values && b.from.length === 0 && b.where === null && (b.frontier ?? null) === null && (b.depth ?? null) === null
|
|
232
|
+
&& b.follow === null && b.flatten.length === 0 && b.groupBy === null && b.having === null && (b.orderBy === null || b.orderBy.length === 0)
|
|
233
|
+
&& b.limit === null && b.offset === null;
|
|
234
|
+
}
|
|
235
|
+
|
|
181
236
|
function follow(f: Follow): Out {
|
|
182
237
|
const dests = join(f.destinations.map((d) => (d.kind === "op" ? op(d) : expr(d))), ", ");
|
|
183
238
|
return cat(`follow${f.distinct ? " distinct" : ""} `, dests);
|
|
184
239
|
}
|
|
185
240
|
|
|
241
|
+
// `as <name>` is always written (0.25), as every select item and group key is
|
|
242
|
+
// aliased: the tree does not record whether it was, and the name is what the
|
|
243
|
+
// later clauses read.
|
|
244
|
+
function flatten(f: Flatten): Out {
|
|
245
|
+
return cat("flatten ", expr(f.expr), ` as ${f.name}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
186
248
|
// Every key aliased, as every select item is (`layer: layer`).
|
|
187
249
|
function groupBy(g: GroupBy): Out {
|
|
188
250
|
return cat("group by ", join(g.keys.map(key), ", "));
|