@omgbase/oqx 0.13.0 → 0.14.1

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 (59) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +184 -17
  3. package/dist/adapters/indexed.d.ts +13 -1
  4. package/dist/adapters/indexed.d.ts.map +1 -1
  5. package/dist/adapters/indexed.js +55 -24
  6. package/dist/adapters/indexed.js.map +1 -1
  7. package/dist/ast.d.ts +5 -1
  8. package/dist/ast.d.ts.map +1 -1
  9. package/dist/context.d.ts +24 -0
  10. package/dist/context.d.ts.map +1 -1
  11. package/dist/context.js.map +1 -1
  12. package/dist/engine.d.ts +55 -2
  13. package/dist/engine.d.ts.map +1 -1
  14. package/dist/engine.js +295 -37
  15. package/dist/engine.js.map +1 -1
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/optimize/analysis.d.ts +45 -0
  21. package/dist/optimize/analysis.d.ts.map +1 -0
  22. package/dist/optimize/analysis.js +194 -0
  23. package/dist/optimize/analysis.js.map +1 -0
  24. package/dist/optimize/driver.d.ts +8 -0
  25. package/dist/optimize/driver.d.ts.map +1 -0
  26. package/dist/optimize/driver.js +35 -0
  27. package/dist/optimize/driver.js.map +1 -0
  28. package/dist/optimize/hash-index.d.ts +34 -0
  29. package/dist/optimize/hash-index.d.ts.map +1 -0
  30. package/dist/optimize/hash-index.js +79 -0
  31. package/dist/optimize/hash-index.js.map +1 -0
  32. package/dist/optimize/index.d.ts +8 -0
  33. package/dist/optimize/index.d.ts.map +1 -0
  34. package/dist/optimize/index.js +11 -0
  35. package/dist/optimize/index.js.map +1 -0
  36. package/dist/optimize/ir.d.ts +84 -0
  37. package/dist/optimize/ir.d.ts.map +1 -0
  38. package/dist/optimize/ir.js +60 -0
  39. package/dist/optimize/ir.js.map +1 -0
  40. package/dist/optimize/rules.d.ts +8 -0
  41. package/dist/optimize/rules.d.ts.map +1 -0
  42. package/dist/optimize/rules.js +136 -0
  43. package/dist/optimize/rules.js.map +1 -0
  44. package/dist/parser.d.ts.map +1 -1
  45. package/dist/parser.js +59 -5
  46. package/dist/parser.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/adapters/indexed.ts +60 -24
  49. package/src/ast.ts +6 -1
  50. package/src/context.ts +24 -0
  51. package/src/engine.ts +329 -35
  52. package/src/index.ts +7 -2
  53. package/src/optimize/analysis.ts +183 -0
  54. package/src/optimize/driver.ts +37 -0
  55. package/src/optimize/hash-index.ts +91 -0
  56. package/src/optimize/index.ts +16 -0
  57. package/src/optimize/ir.ts +128 -0
  58. package/src/optimize/rules.ts +136 -0
  59. package/src/parser.ts +54 -6
package/src/parser.ts CHANGED
@@ -12,9 +12,15 @@
12
12
  // Clause order is FIXED (ADR-020). Within one clause body — the top level or a
13
13
  // consumer block — each clause appears at most once, in exactly this order:
14
14
  //
15
- // select <projection> from <source> where <predicate> follow <relation> {…}
15
+ // select <projection> from <source> where <predicate> follow <dest>, … {…}
16
16
  // order by … limit N offset N
17
17
  //
18
+ // A `follow` destination is a relation of the current row (a receiver, never
19
+ // an outer reference) or a destination block — a receiver (which MAY be an
20
+ // outer reference, `^people`) immediately followed by `collect`/`first`/
21
+ // `single` [`distinct`] `{ body }`, exactly the select-position directive
22
+ // `tryOp(false)` recognizes, re-evaluated per frontier row (language 0.14).
23
+ //
18
24
  // Every clause is optional except that a top-level body needs `from` (the
19
25
  // receiver-plus-consumer form `<receiver> <consumer> { block }` supplies the
20
26
  // source itself, so its block's `from` is an optional re-projection). Only
@@ -39,7 +45,7 @@ import type { Token, TokType } from "./lexer.ts";
39
45
  import { lexTemplate, lexString } from "./lexer.ts";
40
46
  import { OqxError } from "./errors.ts";
41
47
  import type {
42
- Query, Where, Expr, OpNode, Subquery, SelectItem, OrderSpec, Follow, Consumer, RelOp,
48
+ Query, Where, Expr, OpNode, Subquery, SelectItem, OrderSpec, Follow, FollowDestination, Consumer, RelOp,
43
49
  } from "./ast.ts";
44
50
 
45
51
  const CONSUMERS = new Set<string>(["collect", "exists", "none", "count", "first", "single"]);
@@ -393,11 +399,15 @@ class Parser {
393
399
  this.fail("expected a relation after `follow distinct` (`follow distinct <relation>`)");
394
400
  }
395
401
  }
396
- if (this.at("caret")) {
397
- this.fail("`follow` takes a relation of the current row (`follow <relation>`); an outer reference `^name` is not allowed there");
402
+ const destinations: FollowDestination[] = [this.parseFollowDestination()];
403
+ while (this.at("comma")) {
404
+ this.next();
405
+ if (!this.at("ident") && !this.at("binding") && !this.at("caret")) {
406
+ this.fail("expected a relation after ',' (`follow <relation>, <relation>`)");
407
+ }
408
+ destinations.push(this.parseFollowDestination());
398
409
  }
399
- const receiver = this.parseReceiver();
400
- const follow: Follow = { receiver, distinct, where: null, frontier: null, depth: null, by: null };
410
+ const follow: Follow = { destinations, distinct, where: null, frontier: null, depth: null, by: null };
401
411
  if (!this.at("lbrace")) return follow;
402
412
  this.next(); // '{'
403
413
  while (!this.at("eof") && !this.at("rbrace")) {
@@ -429,6 +439,44 @@ class Parser {
429
439
  return follow;
430
440
  }
431
441
 
442
+ // One `follow` destination. The lookahead mirrors `tryOp`: a receiver (binding,
443
+ // or carets + a dotted name) followed by a consumer word and `{` / `distinct {`
444
+ // is a destination block, parsed by `tryOp(false)` itself — but only the
445
+ // select-position consumers (collect/first/single) yield rows to walk; the
446
+ // where-position tests are rejected by name before their body is read. With
447
+ // no directive the tokens are re-read as a plain relation of the current row,
448
+ // where an outer reference is still an error (a `^name` head only makes sense
449
+ // as the receiver of a block, since the block is what varies per row).
450
+ private parseFollowDestination(): FollowDestination {
451
+ const start = this.pos;
452
+ let isReceiver = false;
453
+ if (this.at("binding")) { this.next(); isReceiver = true; }
454
+ else if (this.at("ident") || this.at("caret")) {
455
+ this.parseCarets();
456
+ if (this.at("ident")) { this.parseNavFrom(this.next()); isReceiver = true; }
457
+ }
458
+ if (isReceiver && this.at("ident") && CONSUMERS.has(this.peek().value)) {
459
+ const op = this.peek().value;
460
+ const after = this.peekAt(1);
461
+ const opThenBrace = after?.type === "lbrace";
462
+ const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
463
+ if (opThenBrace || opDistinctBrace) {
464
+ if (op !== "collect" && op !== "first" && op !== "single") {
465
+ this.fail(`a follow destination must use collect/first/single, not \`${op}\` (exists/none/count are where-position tests)`);
466
+ }
467
+ this.pos = start;
468
+ const block = this.tryOp(false);
469
+ if (!block) this.fail("expected a destination block (`<receiver> collect { … }`) after `follow`");
470
+ return block;
471
+ }
472
+ }
473
+ this.pos = start;
474
+ if (this.at("caret")) {
475
+ this.fail("`follow` takes a relation of the current row (`follow <relation>`); an outer reference `^name` is not allowed there — it may only head a destination block (`follow ^name collect { … }`)");
476
+ }
477
+ return this.parseReceiver();
478
+ }
479
+
432
480
  // A receiver/source: a `${…}` binding, or a dotted identifier navigation chain
433
481
  // whose head may be an outer reference (`^rel`, `^^root.rel`) — since a bare
434
482
  // name is the current row's own property, an enclosing row's relation or a