@omgbase/oqx 0.25.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/dist/parser.js CHANGED
@@ -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.
@@ -128,6 +139,29 @@ export function parseTemplate(fragments, values) {
128
139
  export function parseString(src) {
129
140
  return new Parser(lexString(src)).parseQuery();
130
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
+ }
131
165
  // A copy of a node with another span (a parenthesized operand takes the span of
132
166
  // its parentheses).
133
167
  function withSpan(node, span) {
@@ -145,6 +179,11 @@ class Parser {
145
179
  * top-level row's clauses, one deeper per block body and per follow `where`.
146
180
  * An absolute reference `N^name` reads `depth − N` scopes out. */
147
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;
148
187
  constructor(tokens) {
149
188
  this.tokens = tokens;
150
189
  }
@@ -188,9 +227,10 @@ class Parser {
188
227
  const start = this.start();
189
228
  // Directive form: `<receiver> <consumer> { … }` consuming the whole query.
190
229
  let directive = this.tryOp(false);
191
- if (directive && !this.at("eof") && (this.at("lbracket") || this.atValueDirective())) {
230
+ if (directive && !this.at("eof") && (this.at("lbracket") || this.atBlock() !== null)) {
192
231
  // A longer chain whose outermost node is still a directive
193
- // (`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).
194
234
  const save = this.pos;
195
235
  try {
196
236
  const e = this.parsePostfix(directive);
@@ -231,7 +271,7 @@ class Parser {
231
271
  if (!this.at("eof"))
232
272
  this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause`);
233
273
  if (body.froms.length === 0) {
234
- 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)");
235
275
  }
236
276
  return {
237
277
  kind: "query",
@@ -316,7 +356,18 @@ class Parser {
316
356
  if (this.at("kw", "from")) {
317
357
  enter("from");
318
358
  this.next();
319
- 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
+ }
320
371
  continue;
321
372
  }
322
373
  if (this.at("kw", "where")) {
@@ -1028,7 +1079,7 @@ class Parser {
1028
1079
  if (it.kind === "collect")
1029
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 { … }`");
1030
1081
  if (ctx.top)
1031
- 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)");
1032
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 }`)");
1033
1084
  }
1034
1085
  const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
@@ -1058,8 +1109,9 @@ class Parser {
1058
1109
  this.next(); // ':'
1059
1110
  const op = this.tryOp(false);
1060
1111
  if (op) {
1061
- // `name: jobs first { }.pay`: the directive is the head of a longer value.
1062
- 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)
1063
1115
  return this.finishItem(name, this.parseOr(op), lift, start);
1064
1116
  if (op.op !== "collect" && op.op !== "first" && op.op !== "single" && op.op !== "count") {
1065
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)`);
@@ -1360,14 +1412,19 @@ class Parser {
1360
1412
  return this.atConsumerBlock();
1361
1413
  }
1362
1414
  // The cursor is at a consumer word that opens a block — `<op> {` or
1363
- // `<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.
1364
1421
  atConsumerBlock() {
1365
1422
  if (!this.at("ident") || !CONSUMERS.has(this.peek().value))
1366
1423
  return null;
1367
1424
  const after = this.peekAt(1);
1368
1425
  const opThenBrace = after?.type === "lbrace";
1369
1426
  const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
1370
- return opThenBrace || opDistinctBrace ? this.peek().value : null;
1427
+ return opThenBrace || opDistinctBrace || this.bareDirectives ? this.peek().value : null;
1371
1428
  }
1372
1429
  // `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
1373
1430
  // at the consumer word, at `distinct`, or at the `{` of the implicit `collect`.
@@ -1382,22 +1439,35 @@ class Parser {
1382
1439
  this.next();
1383
1440
  distinct = true;
1384
1441
  }
1385
- const lbrace = this.next().pos; // '{'
1386
- const defer = this.deferLookup;
1387
- this.deferLookup = false;
1388
- // The receiver was read at the enclosing depth; the block's rows are one deeper.
1389
- const body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect", destination }));
1390
- this.deferLookup = defer;
1391
- if (!this.at("rbrace"))
1392
- this.fail(`expected '}' to close the ${op} { … } block`);
1393
- this.next();
1394
- // `x collect distinct { group by k }`: the directive's distinct is as
1395
- // redundant as the body's own (checked in parseBody).
1396
- if (distinct && body.groupBy)
1397
- 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
+ }
1398
1468
  const sub = {
1399
1469
  kind: "subquery",
1400
- span: this.sp(lbrace),
1470
+ span: subSpan,
1401
1471
  from: body.froms,
1402
1472
  where: body.where,
1403
1473
  select: body.select,
@@ -1412,7 +1482,38 @@ class Parser {
1412
1482
  limit: body.limit,
1413
1483
  offset: body.offset,
1414
1484
  };
1415
- 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
+ };
1416
1517
  }
1417
1518
  // A tuple `(e1, e2, …)` at the cursor's `(` (0.20): two or more comma-separated
1418
1519
  // expressions. A single parenthesized expression is grouping, not a tuple —