@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +267 -202
  3. package/dist/ast.d.ts +19 -1
  4. package/dist/ast.d.ts.map +1 -1
  5. package/dist/ast.js.map +1 -1
  6. package/dist/build.d.ts +3 -1
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +4 -2
  9. package/dist/build.js.map +1 -1
  10. package/dist/engine.d.ts +2 -0
  11. package/dist/engine.d.ts.map +1 -1
  12. package/dist/engine.js +95 -44
  13. package/dist/engine.js.map +1 -1
  14. package/dist/optimize/analysis.d.ts +3 -2
  15. package/dist/optimize/analysis.d.ts.map +1 -1
  16. package/dist/optimize/analysis.js +12 -2
  17. package/dist/optimize/analysis.js.map +1 -1
  18. package/dist/optimize/ir.d.ts +4 -1
  19. package/dist/optimize/ir.d.ts.map +1 -1
  20. package/dist/optimize/ir.js +1 -0
  21. package/dist/optimize/ir.js.map +1 -1
  22. package/dist/optimize/rules.d.ts.map +1 -1
  23. package/dist/optimize/rules.js +3 -2
  24. package/dist/optimize/rules.js.map +1 -1
  25. package/dist/parser.d.ts.map +1 -1
  26. package/dist/parser.js +206 -42
  27. package/dist/parser.js.map +1 -1
  28. package/dist/print.d.ts +2 -2
  29. package/dist/print.d.ts.map +1 -1
  30. package/dist/print.js +88 -25
  31. package/dist/print.js.map +1 -1
  32. package/dist/references.d.ts.map +1 -1
  33. package/dist/references.js +39 -1
  34. package/dist/references.js.map +1 -1
  35. package/dist/resolve.d.ts.map +1 -1
  36. package/dist/resolve.js +9 -2
  37. package/dist/resolve.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/walk.d.ts +1 -1
  41. package/dist/walk.d.ts.map +1 -1
  42. package/dist/walk.js +5 -0
  43. package/dist/walk.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/ast.ts +20 -1
  46. package/src/build.ts +5 -3
  47. package/src/engine.ts +89 -39
  48. package/src/optimize/analysis.ts +9 -2
  49. package/src/optimize/ir.ts +5 -1
  50. package/src/optimize/rules.ts +3 -2
  51. package/src/parser.ts +193 -41
  52. package/src/print.ts +92 -30
  53. package/src/references.ts +34 -2
  54. package/src/resolve.ts +10 -3
  55. package/src/version.ts +1 -1
  56. 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
- // Every clause is optional except that a top-level body needs `from` (the
28
- // receiver-plus-consumer form `<receiver> <consumer> { block }` supplies the
29
- // source itself, so its block's `from` is an optional re-projection). Only
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 from people`); every other clause always carries its keyword. A
32
- // BLOCK may instead lead with a predicate (since 0.17): a leading expression that
33
- // is syntactically a predicate — not a bare name, a dotted navigation or a lift —
34
- // opens a `where`-first body (`people exists { age > 50 }`); at the top level a
35
- // leading predicate is still an error, since `where` would precede `from`. An
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
- const CLAUSE_ORDER = ["select", "from", "where", "frontier", "depth", "follow", "group by", "having", "order by", "limit", "offset"];
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.atValueDirective())) {
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 with `from <collection>` (or be `<collection> <consumer> { … }`)");
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
- if (idx === stage)
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
- froms.push(this.parseValueExpr());
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
- if (this.atScalarContinuation() || this.atValueDirective())
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 {`. Consumes nothing.
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
- const lbrace = this.next().pos; // '{'
1324
- const defer = this.deferLookup;
1325
- this.deferLookup = false;
1326
- // The receiver was read at the enclosing depth; the block's rows are one deeper.
1327
- const body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect", destination }));
1328
- this.deferLookup = defer;
1329
- if (!this.at("rbrace"))
1330
- this.fail(`expected '}' to close the ${op} { … } block`);
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);
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: this.sp(lbrace),
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 { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct: distinct || body.distinct };
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) {