@omgbase/oqx 0.23.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 +6 -0
- package/README.md +42 -2
- package/dist/ast.d.ts +35 -1
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +7 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +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 +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +136 -9
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +15 -1
- 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 +402 -20
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +16 -0
- package/dist/print.js.map +1 -1
- package/dist/references.d.ts +6 -4
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +65 -3
- package/dist/references.js.map +1 -1
- 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 +44 -2
- package/src/build.ts +11 -4
- package/src/engine.ts +136 -9
- package/src/index.ts +1 -1
- package/src/optimize/analysis.ts +12 -1
- package/src/optimize/ir.ts +7 -1
- package/src/optimize/rules.ts +4 -3
- package/src/parser.ts +324 -20
- package/src/print.ts +18 -3
- package/src/references.ts +62 -7
- package/src/resolve.ts +22 -2
- package/src/version.ts +1 -1
- package/src/walk.ts +12 -1
package/dist/parser.js
CHANGED
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
// misreads.
|
|
80
80
|
import { lexTemplate, lexString } from "./lexer.js";
|
|
81
81
|
import { OqxError } from "./errors.js";
|
|
82
|
+
import { AGGREGATE_NAMES, isAggregateName } from "./ast.js";
|
|
82
83
|
import { resolveSelect, resolveWhere } from "./resolve.js";
|
|
83
84
|
import { printTemplate } from "./print.js";
|
|
84
85
|
const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "single"]);
|
|
@@ -86,8 +87,10 @@ const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "singl
|
|
|
86
87
|
// `frontier` is a clause of every body that has a `follow` (a destination
|
|
87
88
|
// block's marks the occurrences it reaches, any other body's its seeds — 0.20);
|
|
88
89
|
// `depth` is legal only in a follow destination block, and the order sentence
|
|
89
|
-
// names it only there.
|
|
90
|
-
|
|
90
|
+
// names it only there. `group by` and `having` (0.24) sit after `follow`: the
|
|
91
|
+
// 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
|
+
const CLAUSE_ORDER = ["select", "from", "where", "frontier", "depth", "follow", "group by", "having", "order by", "limit", "offset"];
|
|
91
94
|
const DESTINATION_ONLY = new Set(["depth"]);
|
|
92
95
|
// The clauses a body may hold, in order.
|
|
93
96
|
function clausesOf(ctx) {
|
|
@@ -98,8 +101,11 @@ function clausesOf(ctx) {
|
|
|
98
101
|
const CLAUSE_WORDS = new Set([
|
|
99
102
|
"collect", "exists", "none", "count", "first", "single",
|
|
100
103
|
"order", "by", "asc", "desc", "follow", "distinct", "frontier", "depth", "in", "values",
|
|
101
|
-
"limit", "offset",
|
|
104
|
+
"limit", "offset", "group", "having",
|
|
102
105
|
]);
|
|
106
|
+
// The names a group scope resolves besides its keys (SEMANTICS §24): the
|
|
107
|
+
// group's rows, the key record itself, and its identity.
|
|
108
|
+
const GROUP_SCOPE_NAMES = new Set(["rows", "$it", "$key"]);
|
|
103
109
|
const RELOPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
104
110
|
// The comparison level (GRAMMAR §4): the six relational operators and, since
|
|
105
111
|
// 0.22, the four regex operators — sugar over `matches`, non-associative like
|
|
@@ -235,6 +241,8 @@ class Parser {
|
|
|
235
241
|
consumer: "collect",
|
|
236
242
|
follow: body.follow,
|
|
237
243
|
frontier: body.frontier,
|
|
244
|
+
groupBy: body.groupBy,
|
|
245
|
+
having: body.having,
|
|
238
246
|
distinct: body.distinct,
|
|
239
247
|
values: body.values,
|
|
240
248
|
limit: body.limit,
|
|
@@ -246,8 +254,8 @@ class Parser {
|
|
|
246
254
|
queryFromOp(e, start) {
|
|
247
255
|
return {
|
|
248
256
|
kind: "query", span: this.sp(start), source: e.receiver, from: e.sub.from, where: e.sub.where, select: e.sub.select,
|
|
249
|
-
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, frontier: e.sub.frontier,
|
|
250
|
-
limit: e.sub.limit, offset: e.sub.offset,
|
|
257
|
+
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, frontier: e.sub.frontier, groupBy: e.sub.groupBy, having: e.sub.having,
|
|
258
|
+
distinct: e.distinct, values: e.sub.values, limit: e.sub.limit, offset: e.sub.offset,
|
|
251
259
|
};
|
|
252
260
|
}
|
|
253
261
|
tokDesc() {
|
|
@@ -267,6 +275,8 @@ class Parser {
|
|
|
267
275
|
let follow = null;
|
|
268
276
|
let frontier = null;
|
|
269
277
|
let depth = null;
|
|
278
|
+
let groupBy = null;
|
|
279
|
+
let having = null;
|
|
270
280
|
let distinct = false;
|
|
271
281
|
let values = false;
|
|
272
282
|
let limit = null;
|
|
@@ -335,6 +345,20 @@ class Parser {
|
|
|
335
345
|
follow = this.atDepth(rowDepth, () => this.parseFollow());
|
|
336
346
|
continue;
|
|
337
347
|
}
|
|
348
|
+
// `group by k, name: expr` (0.24): the keys are read on the rows, like `where`.
|
|
349
|
+
if (this.atGroupBy()) {
|
|
350
|
+
enter("group by");
|
|
351
|
+
groupBy = this.atDepth(rowDepth, () => this.parseGroupBy());
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
// `having P` (0.24): a predicate over each GROUP, read at the row depth
|
|
355
|
+
// (the group scope sits where the rows did).
|
|
356
|
+
if (this.atHaving()) {
|
|
357
|
+
enter("having");
|
|
358
|
+
this.next();
|
|
359
|
+
having = this.atDepth(rowDepth, () => this.parseWhere());
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
338
362
|
if (this.atOrderBy()) {
|
|
339
363
|
enter("order by");
|
|
340
364
|
this.next();
|
|
@@ -379,15 +403,269 @@ class Parser {
|
|
|
379
403
|
// Validate the alias references of this body (cycles, a block alias inside a
|
|
380
404
|
// `where` expression, a `select` item naming an item to its right); the
|
|
381
405
|
// surface form is kept. `where` first: its cycle report follows its own path.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
406
|
+
// A grouped body without a projection yields its keys and a count (0.24);
|
|
407
|
+
// the items are synthesized before the alias validation sees the select.
|
|
408
|
+
if (groupBy && select.length === 0)
|
|
409
|
+
select = defaultGroupProjection(groupBy, (msg) => this.failAt(groupBy.span[0], msg));
|
|
410
|
+
const resolvedWhere = where ? resolveWhere(select, where, (msg) => this.fail(msg)) : null;
|
|
411
|
+
const resolvedHaving = having ? resolveWhere(select, having, (msg) => this.fail(msg)) : null;
|
|
412
|
+
// The resolved items: an aggregate reached through an alias (`n: count(),
|
|
413
|
+
// xs: collect(n)`) is judged where it will be evaluated.
|
|
414
|
+
const resolvedSelect = resolveSelect(select, (msg) => this.fail(msg));
|
|
385
415
|
// Outside a destination block `frontier` marks the seeds of this body's walk,
|
|
386
416
|
// so it needs one.
|
|
387
417
|
if (frontier && !follow && !ctx.destination) {
|
|
388
418
|
this.fail("`frontier` needs a `follow` — a body's `frontier <expr>` marks which of its rows (the walk's seeds) are frontiers; a destination block's marks the occurrences reached through it");
|
|
389
419
|
}
|
|
390
|
-
|
|
420
|
+
// Grouping (0.24): `having` needs a `group by`; `distinct` is redundant with
|
|
421
|
+
// one; aggregates may stand only in the post-group clauses; and every bare
|
|
422
|
+
// name read at the group scope must be a key, an alias, or the group's rows.
|
|
423
|
+
if (having && !groupBy) {
|
|
424
|
+
this.failAt(having.span[0], "`having` needs a `group by` — `having <pred>` filters the groups a body yields; a per-row filter is `where`");
|
|
425
|
+
}
|
|
426
|
+
if (groupBy) {
|
|
427
|
+
if (distinct)
|
|
428
|
+
this.failAt(groupBy.span[0], DISTINCT_WITH_GROUP);
|
|
429
|
+
this.checkGroupedBody({ select, resolvedSelect, having, resolvedHaving, orderBy, froms, where: resolvedWhere, frontier, limit, offset, groupBy });
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
this.rejectAggregates({ select, resolvedSelect, having: null, resolvedHaving: null, orderBy, froms, where: resolvedWhere, frontier, limit, offset, groupBy: null });
|
|
433
|
+
}
|
|
434
|
+
return { froms, where, select, orderBy, follow, frontier, depth, groupBy, having, distinct, values, limit, offset };
|
|
435
|
+
}
|
|
436
|
+
// ---- grouping (0.24) ------------------------------------------------------
|
|
437
|
+
// `group by` is a clause only as the two-word sequence (a field named `group`
|
|
438
|
+
// stays a name: `select group from r`).
|
|
439
|
+
atGroupBy() {
|
|
440
|
+
const t = this.peek();
|
|
441
|
+
const nx = this.peekAt(1);
|
|
442
|
+
return t.type === "ident" && t.value === "group" && !!nx && nx.type === "ident" && nx.value === "by";
|
|
443
|
+
}
|
|
444
|
+
// `having` is a clause when a predicate can follow it — a value start, or an
|
|
445
|
+
// aggregate call whose name is a consumer word (`having count() > 1`:
|
|
446
|
+
// `count` alone cannot begin a value, `count(` can); otherwise a field name.
|
|
447
|
+
atHaving() {
|
|
448
|
+
if (!this.at("ident", "having"))
|
|
449
|
+
return false;
|
|
450
|
+
const nx = this.peekAt(1);
|
|
451
|
+
return this.startsValue(nx) || (nx?.type === "ident" && isAggregateName(nx.value) && this.peekAt(2)?.type === "lparen");
|
|
452
|
+
}
|
|
453
|
+
parseGroupBy() {
|
|
454
|
+
const start = this.start();
|
|
455
|
+
this.next();
|
|
456
|
+
this.next(); // `group` `by`
|
|
457
|
+
const keys = [this.parseGroupKey()];
|
|
458
|
+
while (this.at("comma")) {
|
|
459
|
+
this.next();
|
|
460
|
+
keys.push(this.parseGroupKey());
|
|
461
|
+
}
|
|
462
|
+
const seen = new Set();
|
|
463
|
+
for (const k of keys) {
|
|
464
|
+
if (seen.has(k.name))
|
|
465
|
+
this.failAt(k.span[0], `duplicate group key '${k.name}' — each key needs its own name (alias one: \`other: expr\`)`);
|
|
466
|
+
seen.add(k.name);
|
|
467
|
+
}
|
|
468
|
+
return { kind: "groupBy", span: this.sp(start), keys };
|
|
469
|
+
}
|
|
470
|
+
// One key, shaped like a select item: `name: expr`, or an unaliased navigation
|
|
471
|
+
// keyed by its last segment; any other expression needs the alias, because the
|
|
472
|
+
// alias is how the post-group clauses refer to the key.
|
|
473
|
+
parseGroupKey() {
|
|
474
|
+
const start = this.start();
|
|
475
|
+
if (this.at("ident") && this.peekAt(1)?.type === "colon") {
|
|
476
|
+
const name = this.next().value;
|
|
477
|
+
this.next(); // ':'
|
|
478
|
+
const expr = this.parseValueExpr();
|
|
479
|
+
return { kind: "key", span: this.sp(start), name, expr };
|
|
480
|
+
}
|
|
481
|
+
if (!this.canStartValue())
|
|
482
|
+
this.fail("expected a group key after `group by` (`group by <name>` or `group by <alias>: <expr>`)");
|
|
483
|
+
const expr = this.parseValueExpr();
|
|
484
|
+
const name = navKey(expr);
|
|
485
|
+
if (name === null) {
|
|
486
|
+
this.failAt(start, `a group key that is not a plain name needs an alias (\`group by key: ${describe(expr)}\`) — the alias is the name the select, \`having\` and \`order by\` read it by`);
|
|
487
|
+
}
|
|
488
|
+
return { kind: "key", span: this.sp(start), name, expr };
|
|
489
|
+
}
|
|
490
|
+
// `name(` where `name` is an aggregate: `count()`, `sum(e)`, `collect(distinct
|
|
491
|
+
// e)`. The cursor is at the `(`. Arity and the `distinct` modifier are checked
|
|
492
|
+
// here; WHERE the aggregate may stand is the body's business (`checkGroupedBody`).
|
|
493
|
+
parseAggregate(name, start) {
|
|
494
|
+
this.next(); // '('
|
|
495
|
+
let distinct = false;
|
|
496
|
+
if (this.at("ident", "distinct") && this.startsValue(this.peekAt(1))) {
|
|
497
|
+
this.next();
|
|
498
|
+
distinct = true;
|
|
499
|
+
}
|
|
500
|
+
let arg = null;
|
|
501
|
+
if (!this.at("rparen")) {
|
|
502
|
+
arg = this.parseValueExpr();
|
|
503
|
+
if (this.at("comma"))
|
|
504
|
+
this.fail(`\`${name}(…)\` takes one argument — an aggregate reduces one expression over the group's rows`);
|
|
505
|
+
}
|
|
506
|
+
if (!this.at("rparen"))
|
|
507
|
+
this.fail(`expected ')' to close \`${name}(…)\``);
|
|
508
|
+
this.next();
|
|
509
|
+
if (arg === null && name !== "count")
|
|
510
|
+
this.fail(`\`${name}()\` needs an argument (\`${name}(<expr>)\`) — only \`count()\` counts the group's rows without one`);
|
|
511
|
+
if (distinct && (name === "any" || name === "all")) {
|
|
512
|
+
this.fail(`\`${name}(distinct …)\`: \`distinct\` applies to a value aggregate (count, sum, min, max, avg, collect, first, last), not to a predicate aggregate`);
|
|
513
|
+
}
|
|
514
|
+
return { kind: "aggregate", span: [start, this.end()], name, arg, distinct };
|
|
515
|
+
}
|
|
516
|
+
// A free call: an aggregate node for an aggregate name (reserved since 0.24),
|
|
517
|
+
// an ordinary `call` otherwise. The cursor is at the `(`.
|
|
518
|
+
parseFreeCall(name, start) {
|
|
519
|
+
if (isAggregateName(name))
|
|
520
|
+
return this.parseAggregate(name, start);
|
|
521
|
+
const args = this.parseArgs();
|
|
522
|
+
return { kind: "call", span: this.sp(start), recv: null, name, args };
|
|
523
|
+
}
|
|
524
|
+
// The clauses an aggregate may NOT stand in — every clause read before the
|
|
525
|
+
// rows are grouped (`from`, `where`, `frontier`, the keys, the bound) — and,
|
|
526
|
+
// in a body without `group by`, every clause. The `where` is the alias-resolved
|
|
527
|
+
// one, so an aggregate reached through a select alias is caught too.
|
|
528
|
+
rejectAggregates(b) {
|
|
529
|
+
const found = [];
|
|
530
|
+
const collect = (exprs, clause) => {
|
|
531
|
+
const out = [];
|
|
532
|
+
for (const e of exprs)
|
|
533
|
+
if (e)
|
|
534
|
+
aggregatesIn(e, out);
|
|
535
|
+
for (const a of out)
|
|
536
|
+
found.push([a, clause]);
|
|
537
|
+
};
|
|
538
|
+
const where = [];
|
|
539
|
+
if (b.where)
|
|
540
|
+
whereAggregates(b.where, where);
|
|
541
|
+
for (const a of where)
|
|
542
|
+
found.push([a, "`where`"]);
|
|
543
|
+
collect(b.froms, "`from`");
|
|
544
|
+
collect([b.frontier], "`frontier`");
|
|
545
|
+
collect([b.limit], "`limit`");
|
|
546
|
+
collect([b.offset], "`offset`");
|
|
547
|
+
if (b.groupBy)
|
|
548
|
+
collect(b.groupBy.keys.map((k) => k.expr), "a `group by` key");
|
|
549
|
+
if (!b.groupBy) {
|
|
550
|
+
collect(b.resolvedSelect.filter((it) => it.kind === "field").map((it) => it.expr), "a select item");
|
|
551
|
+
if (b.orderBy)
|
|
552
|
+
collect(b.orderBy.map((o) => o.expr), "`order by`");
|
|
553
|
+
}
|
|
554
|
+
const hit = found[0];
|
|
555
|
+
if (!hit)
|
|
556
|
+
return;
|
|
557
|
+
const [agg, clause] = hit;
|
|
558
|
+
if (!b.groupBy) {
|
|
559
|
+
this.failAt(agg.span[0], `\`${describe(agg)}\` is an aggregate: it is read after grouping and may stand only in a select item, \`having\` or \`order by\` of a body with \`group by\` — this body has none`);
|
|
560
|
+
}
|
|
561
|
+
this.failAt(agg.span[0], `\`${describe(agg)}\` is an aggregate and cannot be read in ${clause}${clause === "`where`" ? " (a select alias standing for one included)" : ""} — the rows are not grouped yet there; aggregates stand in a select item, \`having\` or \`order by\``);
|
|
562
|
+
}
|
|
563
|
+
// A grouped body: aggregates only after the grouping (and never nested), and
|
|
564
|
+
// every bare name at the group scope a key, an alias, or the group's rows.
|
|
565
|
+
checkGroupedBody(b) {
|
|
566
|
+
this.rejectAggregates(b);
|
|
567
|
+
// An aggregate's argument is read on the group's rows, where there is no
|
|
568
|
+
// group to aggregate: `sum(count())` is an error, not a double reduction.
|
|
569
|
+
const post = [];
|
|
570
|
+
for (const it of b.resolvedSelect)
|
|
571
|
+
if (it.kind === "field")
|
|
572
|
+
aggregatesIn(it.expr, post);
|
|
573
|
+
if (b.resolvedHaving)
|
|
574
|
+
whereAggregates(b.resolvedHaving, post);
|
|
575
|
+
if (b.orderBy)
|
|
576
|
+
for (const o of b.orderBy)
|
|
577
|
+
aggregatesIn(o.expr, post);
|
|
578
|
+
const nested = nestedAggregate(post);
|
|
579
|
+
if (nested)
|
|
580
|
+
this.failAt(nested.span[0], `\`${describe(nested)}\`: an aggregate cannot nest — its argument is read on each of the group's rows, where there is no group to reduce`);
|
|
581
|
+
const keys = new Set(b.groupBy.keys.map((k) => k.name));
|
|
582
|
+
const valid = (aliases) => new Set([...keys, ...GROUP_SCOPE_NAMES, ...aliases]);
|
|
583
|
+
const aliases = [];
|
|
584
|
+
for (const it of b.select) {
|
|
585
|
+
const names = valid(aliases);
|
|
586
|
+
if (it.kind === "field")
|
|
587
|
+
this.checkGroupExpr(it.expr, names, "a select item");
|
|
588
|
+
else
|
|
589
|
+
this.checkGroupExpr(it.op.receiver, names, "a select item");
|
|
590
|
+
const own = it.kind === "collect" ? (it.name === "" ? null : it.name) : it.lift === 0 && it.name !== "" ? it.name : null;
|
|
591
|
+
if (own !== null)
|
|
592
|
+
aliases.push(own);
|
|
593
|
+
}
|
|
594
|
+
if (b.having)
|
|
595
|
+
this.checkGroupWhere(b.having, valid(aliases), "`having`");
|
|
596
|
+
if (b.orderBy)
|
|
597
|
+
for (const o of b.orderBy)
|
|
598
|
+
this.checkGroupExpr(o.expr, valid([]), "`order by`");
|
|
599
|
+
}
|
|
600
|
+
checkGroupWhere(w, valid, where) {
|
|
601
|
+
switch (w.kind) {
|
|
602
|
+
case "and":
|
|
603
|
+
case "or":
|
|
604
|
+
w.parts.forEach((p) => this.checkGroupWhere(p, valid, where));
|
|
605
|
+
return;
|
|
606
|
+
case "not":
|
|
607
|
+
this.checkGroupWhere(w.expr, valid, where);
|
|
608
|
+
return;
|
|
609
|
+
case "scalar":
|
|
610
|
+
this.checkGroupExpr(w.expr, valid, where);
|
|
611
|
+
return;
|
|
612
|
+
case "op":
|
|
613
|
+
this.checkGroupExpr(w.receiver, valid, where);
|
|
614
|
+
return; // the block is its own scope
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Every bare name `e` reads at the group scope (an aggregate's argument reads
|
|
618
|
+
// the group's rows and is not checked; a nested block is its own scope, only
|
|
619
|
+
// its receiver is read here). `inCall` names the enclosing non-aggregate free
|
|
620
|
+
// call, for the "not an aggregate" hint.
|
|
621
|
+
checkGroupExpr(e, valid, where, inCall = null) {
|
|
622
|
+
switch (e.kind) {
|
|
623
|
+
case "ident":
|
|
624
|
+
if (!valid.has(e.name)) {
|
|
625
|
+
const hint = inCall === null ? "" : `; \`${inCall}(…)\` is not an aggregate — the aggregates are ${[...AGGREGATE_NAMES].join(", ")}`;
|
|
626
|
+
this.failAt(e.span[0], `\`${e.name}\` is not a group key or aggregate — ${where} of a grouped body reads the group: a key (\`group by ${e.name}\`), an aggregate over its rows (\`collect(${e.name})\`), or the rows themselves (\`rows collect { select ${e.name} }\`)${hint}`);
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
case "lit":
|
|
630
|
+
case "binding":
|
|
631
|
+
case "outer":
|
|
632
|
+
case "aggregate": return;
|
|
633
|
+
case "member":
|
|
634
|
+
this.checkGroupExpr(e.recv, valid, where, inCall);
|
|
635
|
+
return;
|
|
636
|
+
case "call":
|
|
637
|
+
if (e.recv)
|
|
638
|
+
this.checkGroupExpr(e.recv, valid, where, inCall);
|
|
639
|
+
for (const a of e.args)
|
|
640
|
+
this.checkGroupExpr(a, valid, where, e.recv === null ? e.name : inCall);
|
|
641
|
+
return;
|
|
642
|
+
case "unary":
|
|
643
|
+
case "required":
|
|
644
|
+
this.checkGroupExpr(e.expr, valid, where, inCall);
|
|
645
|
+
return;
|
|
646
|
+
case "binary":
|
|
647
|
+
case "logical":
|
|
648
|
+
case "in":
|
|
649
|
+
this.checkGroupExpr(e.left, valid, where, inCall);
|
|
650
|
+
this.checkGroupExpr(e.right, valid, where, inCall);
|
|
651
|
+
return;
|
|
652
|
+
case "range":
|
|
653
|
+
if (e.lo)
|
|
654
|
+
this.checkGroupExpr(e.lo, valid, where, inCall);
|
|
655
|
+
if (e.hi)
|
|
656
|
+
this.checkGroupExpr(e.hi, valid, where, inCall);
|
|
657
|
+
return;
|
|
658
|
+
case "tuple":
|
|
659
|
+
e.items.forEach((x) => this.checkGroupExpr(x, valid, where, inCall));
|
|
660
|
+
return;
|
|
661
|
+
case "template":
|
|
662
|
+
e.parts.forEach((p) => { if (typeof p !== "string")
|
|
663
|
+
this.checkGroupExpr(p, valid, where, inCall); });
|
|
664
|
+
return;
|
|
665
|
+
case "op":
|
|
666
|
+
this.checkGroupExpr(e.receiver, valid, where, inCall);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
391
669
|
}
|
|
392
670
|
// The keyword-less run that opens a body. At the top level it is always the
|
|
393
671
|
// projection. In a block it is a `where`-first body when the leading expression
|
|
@@ -472,6 +750,10 @@ class Parser {
|
|
|
472
750
|
this.fail("expected an integer after `depth` (`depth 1`..`depth 8`)");
|
|
473
751
|
if (t.value === "frontier")
|
|
474
752
|
this.fail(`unexpected 'frontier' after \`${last}\` — \`frontier\` takes an expression (\`frontier <expr>\`) and belongs to a body that has a \`follow\``);
|
|
753
|
+
if (t.value === "group")
|
|
754
|
+
this.fail(`unexpected 'group' after \`${last}\` — a grouping is written \`group by <key>[, <key>…]\` (a key is a name or \`alias: <expr>\`)`);
|
|
755
|
+
if (t.value === "having")
|
|
756
|
+
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\``);
|
|
475
757
|
}
|
|
476
758
|
if (last === "from" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
477
759
|
this.fail(`unexpected \`${t.value}\` after \`from\` — a whole-query consumer is written \`<collection> ${t.value} { … }\`; to project a field named ${t.value} write \`select ${t.value} from …\`; a predicate needs \`where\``);
|
|
@@ -640,10 +922,8 @@ class Parser {
|
|
|
640
922
|
? { kind: "outer", span: [start, head.end], levels, name: head.value }
|
|
641
923
|
: { kind: "ident", span: [start, head.end], name: head.value };
|
|
642
924
|
let name = head.value;
|
|
643
|
-
if (levels === 0 && this.at("lparen"))
|
|
644
|
-
|
|
645
|
-
expr = { kind: "call", span: this.sp(start), recv: null, name: head.value, args };
|
|
646
|
-
}
|
|
925
|
+
if (levels === 0 && this.at("lparen"))
|
|
926
|
+
expr = this.parseFreeCall(head.value, start);
|
|
647
927
|
while (this.at("dot")) {
|
|
648
928
|
this.next();
|
|
649
929
|
if (!this.at("ident"))
|
|
@@ -719,8 +999,8 @@ class Parser {
|
|
|
719
999
|
// `name: jobs first { }.pay`: the directive is the head of a longer value.
|
|
720
1000
|
if (this.atScalarContinuation() || this.atValueDirective())
|
|
721
1001
|
return this.finishItem(name, this.parseOr(op), lift, start);
|
|
722
|
-
if (op.op !== "collect" && op.op !== "first" && op.op !== "single") {
|
|
723
|
-
this.fail(`projection '${name}' must use collect/first/single, not ${op.op} (exists/none
|
|
1002
|
+
if (op.op !== "collect" && op.op !== "first" && op.op !== "single" && op.op !== "count") {
|
|
1003
|
+
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)`);
|
|
724
1004
|
}
|
|
725
1005
|
return this.finishItem(name, op, lift, start);
|
|
726
1006
|
}
|
|
@@ -1049,6 +1329,10 @@ class Parser {
|
|
|
1049
1329
|
if (!this.at("rbrace"))
|
|
1050
1330
|
this.fail(`expected '}' to close the ${op} { … } block`);
|
|
1051
1331
|
this.next();
|
|
1332
|
+
// `x collect distinct { group by k }`: the directive's distinct is as
|
|
1333
|
+
// redundant as the body's own (checked in parseBody).
|
|
1334
|
+
if (distinct && body.groupBy)
|
|
1335
|
+
this.failAt(body.groupBy.span[0], DISTINCT_WITH_GROUP);
|
|
1052
1336
|
const sub = {
|
|
1053
1337
|
kind: "subquery",
|
|
1054
1338
|
span: this.sp(lbrace),
|
|
@@ -1059,6 +1343,8 @@ class Parser {
|
|
|
1059
1343
|
follow: body.follow,
|
|
1060
1344
|
frontier: body.frontier,
|
|
1061
1345
|
depth: body.depth,
|
|
1346
|
+
groupBy: body.groupBy,
|
|
1347
|
+
having: body.having,
|
|
1062
1348
|
values: body.values,
|
|
1063
1349
|
limit: body.limit,
|
|
1064
1350
|
offset: body.offset,
|
|
@@ -1130,7 +1416,7 @@ class Parser {
|
|
|
1130
1416
|
this.next();
|
|
1131
1417
|
const subSpan = [lbracket, this.end()];
|
|
1132
1418
|
const sub = (parts) => ({
|
|
1133
|
-
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, frontier: null, depth: null, values: false, limit: null, offset: null, ...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,
|
|
1134
1420
|
});
|
|
1135
1421
|
const bang = this.atOp("!") ? this.next() : null;
|
|
1136
1422
|
if (index) {
|
|
@@ -1291,9 +1577,9 @@ class Parser {
|
|
|
1291
1577
|
}
|
|
1292
1578
|
}
|
|
1293
1579
|
else if (this.at("lparen") && left === undefined && expr.kind === "ident") {
|
|
1294
|
-
// free function call: name(args) — a bare name read here, not `(f)(x)
|
|
1295
|
-
|
|
1296
|
-
expr =
|
|
1580
|
+
// free function call: name(args) — a bare name read here, not `(f)(x)`;
|
|
1581
|
+
// an aggregate name makes it an `aggregate` node (0.24)
|
|
1582
|
+
expr = this.parseFreeCall(expr.name, start);
|
|
1297
1583
|
}
|
|
1298
1584
|
else if (this.at("lbracket")) {
|
|
1299
1585
|
expr = this.parseBracket(expr);
|
|
@@ -1320,9 +1606,12 @@ class Parser {
|
|
|
1320
1606
|
}
|
|
1321
1607
|
return expr;
|
|
1322
1608
|
}
|
|
1609
|
+
// The directives that have a value form: collect/first/single, and since 0.24
|
|
1610
|
+
// `count { … }` (the number of rows the block yields). exists/none are
|
|
1611
|
+
// predicates only.
|
|
1323
1612
|
atValueDirective() {
|
|
1324
1613
|
const op = this.atConsumerBlock();
|
|
1325
|
-
return op === "collect" || op === "first" || op === "single";
|
|
1614
|
+
return op === "collect" || op === "first" || op === "single" || op === "count";
|
|
1326
1615
|
}
|
|
1327
1616
|
parseArgs() {
|
|
1328
1617
|
this.next(); // '('
|
|
@@ -1440,6 +1729,99 @@ class Parser {
|
|
|
1440
1729
|
return { kind: "template", span: [open.pos, this.end()], parts };
|
|
1441
1730
|
}
|
|
1442
1731
|
}
|
|
1732
|
+
// ---- grouping helpers (0.24) --------------------------------------------------
|
|
1733
|
+
const DISTINCT_WITH_GROUP = "`distinct` is redundant with `group by` — a grouped body already yields one row per distinct key tuple; drop `distinct`";
|
|
1734
|
+
// The projection of a grouped body that writes none (SEMANTICS §24): every key
|
|
1735
|
+
// by its name, then `count: count()`. Synthesized here so the tree shows what
|
|
1736
|
+
// the body projects and the engine has no grouped "row itself" to define; the
|
|
1737
|
+
// nodes carry the empty span (they did not come from source).
|
|
1738
|
+
function defaultGroupProjection(groupBy, fail) {
|
|
1739
|
+
if (groupBy.keys.some((k) => k.name === "count")) {
|
|
1740
|
+
fail("a group key named `count` needs an explicit `select` — the default projection of a grouped body is its keys plus `count: count()`");
|
|
1741
|
+
}
|
|
1742
|
+
const items = groupBy.keys.map((k) => ({ kind: "field", span: [0, 0], name: k.name, expr: { kind: "ident", span: [0, 0], name: k.name }, lift: 0 }));
|
|
1743
|
+
items.push({ kind: "field", span: [0, 0], name: "count", expr: { kind: "aggregate", span: [0, 0], name: "count", arg: null, distinct: false }, lift: 0 });
|
|
1744
|
+
return items;
|
|
1745
|
+
}
|
|
1746
|
+
// Every aggregate node in `e` read at THIS scope (a nested block's body is its
|
|
1747
|
+
// own scope and is not entered; a block's receiver is). An aggregate's argument
|
|
1748
|
+
// is entered too, so a nested aggregate is found by `nestedAggregate`.
|
|
1749
|
+
function aggregatesIn(e, out) {
|
|
1750
|
+
switch (e.kind) {
|
|
1751
|
+
case "aggregate":
|
|
1752
|
+
out.push(e);
|
|
1753
|
+
if (e.arg)
|
|
1754
|
+
aggregatesIn(e.arg, out);
|
|
1755
|
+
return;
|
|
1756
|
+
case "lit":
|
|
1757
|
+
case "ident":
|
|
1758
|
+
case "outer":
|
|
1759
|
+
case "binding": return;
|
|
1760
|
+
case "member":
|
|
1761
|
+
aggregatesIn(e.recv, out);
|
|
1762
|
+
return;
|
|
1763
|
+
case "call":
|
|
1764
|
+
if (e.recv)
|
|
1765
|
+
aggregatesIn(e.recv, out);
|
|
1766
|
+
e.args.forEach((a) => aggregatesIn(a, out));
|
|
1767
|
+
return;
|
|
1768
|
+
case "unary":
|
|
1769
|
+
case "required":
|
|
1770
|
+
aggregatesIn(e.expr, out);
|
|
1771
|
+
return;
|
|
1772
|
+
case "binary":
|
|
1773
|
+
case "logical":
|
|
1774
|
+
case "in":
|
|
1775
|
+
aggregatesIn(e.left, out);
|
|
1776
|
+
aggregatesIn(e.right, out);
|
|
1777
|
+
return;
|
|
1778
|
+
case "range":
|
|
1779
|
+
if (e.lo)
|
|
1780
|
+
aggregatesIn(e.lo, out);
|
|
1781
|
+
if (e.hi)
|
|
1782
|
+
aggregatesIn(e.hi, out);
|
|
1783
|
+
return;
|
|
1784
|
+
case "tuple":
|
|
1785
|
+
e.items.forEach((x) => aggregatesIn(x, out));
|
|
1786
|
+
return;
|
|
1787
|
+
case "template":
|
|
1788
|
+
e.parts.forEach((p) => { if (typeof p !== "string")
|
|
1789
|
+
aggregatesIn(p, out); });
|
|
1790
|
+
return;
|
|
1791
|
+
case "op":
|
|
1792
|
+
aggregatesIn(e.receiver, out);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function whereAggregates(w, out) {
|
|
1797
|
+
switch (w.kind) {
|
|
1798
|
+
case "and":
|
|
1799
|
+
case "or":
|
|
1800
|
+
w.parts.forEach((p) => whereAggregates(p, out));
|
|
1801
|
+
return;
|
|
1802
|
+
case "not":
|
|
1803
|
+
whereAggregates(w.expr, out);
|
|
1804
|
+
return;
|
|
1805
|
+
case "scalar":
|
|
1806
|
+
aggregatesIn(w.expr, out);
|
|
1807
|
+
return;
|
|
1808
|
+
case "op":
|
|
1809
|
+
aggregatesIn(w.receiver, out);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
// The first aggregate whose argument holds another (`sum(count())`), if any.
|
|
1814
|
+
function nestedAggregate(found) {
|
|
1815
|
+
for (const a of found) {
|
|
1816
|
+
if (a.kind !== "aggregate" || !a.arg)
|
|
1817
|
+
continue;
|
|
1818
|
+
const inner = [];
|
|
1819
|
+
aggregatesIn(a.arg, inner);
|
|
1820
|
+
if (inner.length > 0)
|
|
1821
|
+
return a;
|
|
1822
|
+
}
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1443
1825
|
// The default key of an unaliased projection item: the last segment of a bare /
|
|
1444
1826
|
// dotted / outer navigation (`name`, `meta.slug` → "slug", `^name`), else null.
|
|
1445
1827
|
function navKey(e) {
|