@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +42 -2
  3. package/dist/ast.d.ts +35 -1
  4. package/dist/ast.d.ts.map +1 -1
  5. package/dist/ast.js +7 -1
  6. package/dist/ast.js.map +1 -1
  7. package/dist/build.d.ts +7 -1
  8. package/dist/build.d.ts.map +1 -1
  9. package/dist/build.js +9 -2
  10. package/dist/build.js.map +1 -1
  11. package/dist/engine.d.ts +2 -0
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +136 -9
  14. package/dist/engine.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/optimize/analysis.d.ts.map +1 -1
  20. package/dist/optimize/analysis.js +15 -1
  21. package/dist/optimize/analysis.js.map +1 -1
  22. package/dist/optimize/ir.d.ts +5 -1
  23. package/dist/optimize/ir.d.ts.map +1 -1
  24. package/dist/optimize/ir.js +2 -0
  25. package/dist/optimize/ir.js.map +1 -1
  26. package/dist/optimize/rules.d.ts.map +1 -1
  27. package/dist/optimize/rules.js +4 -3
  28. package/dist/optimize/rules.js.map +1 -1
  29. package/dist/parser.d.ts.map +1 -1
  30. package/dist/parser.js +402 -20
  31. package/dist/parser.js.map +1 -1
  32. package/dist/print.d.ts.map +1 -1
  33. package/dist/print.js +16 -0
  34. package/dist/print.js.map +1 -1
  35. package/dist/references.d.ts +6 -4
  36. package/dist/references.d.ts.map +1 -1
  37. package/dist/references.js +65 -3
  38. package/dist/references.js.map +1 -1
  39. package/dist/resolve.d.ts.map +1 -1
  40. package/dist/resolve.js +20 -1
  41. package/dist/resolve.js.map +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/dist/walk.d.ts +1 -1
  45. package/dist/walk.d.ts.map +1 -1
  46. package/dist/walk.js +10 -0
  47. package/dist/walk.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/ast.ts +44 -2
  50. package/src/build.ts +11 -4
  51. package/src/engine.ts +136 -9
  52. package/src/index.ts +1 -1
  53. package/src/optimize/analysis.ts +12 -1
  54. package/src/optimize/ir.ts +7 -1
  55. package/src/optimize/rules.ts +4 -3
  56. package/src/parser.ts +324 -20
  57. package/src/print.ts +18 -3
  58. package/src/references.ts +62 -7
  59. package/src/resolve.ts +22 -2
  60. package/src/version.ts +1 -1
  61. package/src/walk.ts +12 -1
package/src/references.ts CHANGED
@@ -54,15 +54,17 @@
54
54
  // are cut after the last step at its scope's depth. The empty list is the root
55
55
  // scope.
56
56
 
57
- import type { AstNode, Expr, Follow, Query, SelectItem, Span, Subquery } from "./ast.ts";
57
+ import type { AstNode, Expr, Follow, GroupBy, Query, SelectItem, Span, Subquery } from "./ast.ts";
58
58
  import type { Clause } from "./walk.ts";
59
59
  import { visit } from "./walk.ts";
60
60
 
61
61
  /** One step of a scope's row chain: an expression (a source, a `from`, a block
62
- * receiver, a plain destination) or a `follow` node (the walk's frontier rows),
63
- * with the scope depth of the rows it produces. */
62
+ * receiver, a plain destination), a `follow` node (the walk's frontier rows), or
63
+ * — since 0.24 — a `groupBy` node (the body's groups: the rows at the same depth
64
+ * replaced by one key record per group), with the scope depth of the rows it
65
+ * produces. */
64
66
  export interface ReferenceStep {
65
- readonly expr: Expr | Follow;
67
+ readonly expr: Expr | Follow | GroupBy;
66
68
  readonly depth: number;
67
69
  }
68
70
 
@@ -120,6 +122,10 @@ export function references(root: Query | Subquery): Reference[] {
120
122
  const levels = head.kind === "outer" ? head.levels : 0;
121
123
  const depth = ctx.depth - levels;
122
124
  if (isLiftBound(name, depth, ctx.path, lifts)) return false;
125
+ // At a group scope (0.24) a bare key name is the key — an alias of the
126
+ // expression reported in clause `groupBy` — and `rows` is the language's
127
+ // value; neither reads a row.
128
+ if (isGroupValue(name, depth, ctx.path, node)) return false;
123
129
  const full = [name, ...path].join(".");
124
130
  const steps = scopeSteps(ctx.path, node);
125
131
  const scoped = levels === 0 ? steps : cutAt(steps, depth);
@@ -132,9 +138,9 @@ export function references(root: Query | Subquery): Reference[] {
132
138
 
133
139
  /** Whether a bare `name` in this position is a `select` alias the language
134
140
  * substitutes (so the name is the alias, not a row property): in the body's
135
- * `where`, any alias; in a `select` item, an alias defined to its LEFT. */
141
+ * `where` or `having`, any alias; in a `select` item, an alias defined to its LEFT. */
136
142
  function isResolvedAlias(name: string, path: readonly AstNode[], clause: Clause): boolean {
137
- if (clause !== "where" && clause !== "select") return false;
143
+ if (clause !== "where" && clause !== "select" && clause !== "having") return false;
138
144
  // The nearest body, and the item under it when we are inside its select.
139
145
  for (let i = path.length - 1; i >= 0; i--) {
140
146
  const a = path[i]!;
@@ -143,7 +149,7 @@ function isResolvedAlias(name: string, path: readonly AstNode[], clause: Clause)
143
149
  a.select.forEach((it, j) => { const n = aliasName(it); if (n !== null) index.set(n, j); });
144
150
  const j = index.get(name);
145
151
  if (j === undefined) return false;
146
- if (clause === "where") return true;
152
+ if (clause === "where" || clause === "having") return true;
147
153
  const item = path[i + 1];
148
154
  const at = item === undefined ? -1 : a.select.indexOf(item as SelectItem);
149
155
  return at >= 0 && j < at;
@@ -185,6 +191,51 @@ function liftsLandingOn(body: Query | Subquery): ReadonlySet<string> {
185
191
  return names;
186
192
  }
187
193
 
194
+ /**
195
+ * Whether `name`, read at scope `depth`, is a value of a GROUP scope there
196
+ * (SEMANTICS §24): the body whose rows are at `depth` has a `group by`, the
197
+ * position is one of its post-group clauses — a select item, `having`, `order
198
+ * by` — not inside an aggregate's argument (which reads the group's rows), and
199
+ * the name is a key or `rows`.
200
+ */
201
+ function isGroupValue(name: string, depth: number, path: readonly AstNode[], node: AstNode): boolean {
202
+ const i = bodyIndexAt(path, depth);
203
+ if (i === -1) return false;
204
+ const body = path[i] as Query | Subquery;
205
+ if (!atGroupScope(body, path, i, node)) return false;
206
+ return name === "rows" || body.groupBy!.keys.some((k) => k.name === name);
207
+ }
208
+
209
+ /** The index in `path` of the body whose rows are at scope `depth` (every nested body is one scope deeper), or -1. */
210
+ function bodyIndexAt(path: readonly AstNode[], depth: number): number {
211
+ if (depth < 1) return -1;
212
+ let seen = 0;
213
+ for (let i = 0; i < path.length; i++) {
214
+ const a = path[i]!;
215
+ if (a.kind !== "query" && a.kind !== "subquery") continue;
216
+ if (++seen === depth) return i;
217
+ }
218
+ return -1;
219
+ }
220
+
221
+ /** Whether a position under `body` (at `path[i]`) reads the body's GROUP scope:
222
+ * the body is grouped, the position lies in a select item, `having` or `order
223
+ * by`, and no aggregate stands between the body and the position (an
224
+ * aggregate's argument reads the group's rows; a nested body is its own scope
225
+ * and ends the search). */
226
+ function atGroupScope(body: Query | Subquery, path: readonly AstNode[], i: number, node: AstNode): boolean {
227
+ if (body.groupBy === null) return false;
228
+ const child = path[i + 1] ?? node;
229
+ const post = body.select.includes(child as SelectItem) || child === body.having || (body.orderBy?.includes(child as never) ?? false);
230
+ if (!post) return false;
231
+ for (let j = i + 1; j < path.length; j++) {
232
+ const a = path[j]!;
233
+ if (a.kind === "query" || a.kind === "subquery") break;
234
+ if (a.kind === "aggregate") return false;
235
+ }
236
+ return true;
237
+ }
238
+
188
239
  /** The row chain of the scope a node is evaluated in, from its ancestors. */
189
240
  function scopeSteps(path: readonly AstNode[], node: AstNode): ReferenceStep[] {
190
241
  const steps: ReferenceStep[] = [];
@@ -198,12 +249,16 @@ function scopeSteps(path: readonly AstNode[], node: AstNode): ReferenceStep[] {
198
249
  depth = 1;
199
250
  steps.push({ expr: a.source, depth });
200
251
  pushFroms(steps, a.from, next, depth);
252
+ // A grouped body's post-group clauses read its groups (0.24): the rows
253
+ // at this depth replaced by one key record per group.
254
+ if (atGroupScope(a, path, i, node)) steps.push({ expr: a.groupBy!, depth });
201
255
  break;
202
256
  }
203
257
  case "subquery": {
204
258
  // The receiver step was pushed by the enclosing `op`; this body's rows
205
259
  // are one deeper than where that receiver was read.
206
260
  pushFroms(steps, a.from, next, depth);
261
+ if (atGroupScope(a, path, i, node)) steps.push({ expr: a.groupBy!, depth });
207
262
  break;
208
263
  }
209
264
  case "op": {
package/src/resolve.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  // given. Untouched nodes are returned by reference, so a cached query keeps the
27
27
  // identity of every block the rewrite did not reach.
28
28
 
29
- import type { Expr, Follow, FollowDestination, OpNode, Query, SelectItem, Subquery, Where } from "./ast.ts";
29
+ import type { Expr, Follow, FollowDestination, GroupBy, OpNode, Query, SelectItem, Subquery, Where } from "./ast.ts";
30
30
  import { OqxError } from "./errors.ts";
31
31
 
32
32
  /** How a resolution failure is reported: the parser raises its positioned parse
@@ -52,8 +52,15 @@ function resolveBody<T extends Query | Subquery>(node: T, fail: Fail): T {
52
52
  // cycle report follows the where's own path (`c → a → b → c`), as pinned.
53
53
  const where = node.where === null ? null : resolveWhere(node.select, node.where, fail);
54
54
  const where2 = where === null ? null : resolveNestedWhere(where, fail);
55
+ // `having` (0.24) is the post-group `where`: rewritten against the select
56
+ // exactly as `where` is (`select n: count() … having n > 1`).
57
+ const having = node.having === null ? null : resolveWhere(node.select, node.having, fail);
58
+ const having2 = having === null ? null : resolveNestedWhere(having, fail);
55
59
  const select = resolveSelect(node.select, fail);
56
60
  const follow = node.follow === null ? null : resolveFollow(node.follow, fail);
61
+ // The keys are read on the rows, not rewritten (a key IS the name the post-group
62
+ // clauses read); their nested blocks resolve against their own select.
63
+ const groupBy = node.groupBy === null ? null : resolveGroupBy(node.groupBy, fail);
57
64
  const from = mapSame(node.from, (e) => resolveExpr(e, fail));
58
65
  const orderBy = node.orderBy === null ? null : mapSame(node.orderBy, (o) => {
59
66
  const expr = resolveExpr(o.expr, fail);
@@ -68,13 +75,22 @@ function resolveBody<T extends Query | Subquery>(node: T, fail: Fail): T {
68
75
  const source = node.kind === "query" ? resolveExpr(node.source, fail) : null;
69
76
  const same = where2 === node.where && select === node.select && follow === node.follow && from === node.from
70
77
  && orderBy === node.orderBy && limit === node.limit && offset === node.offset && frontier === node.frontier
78
+ && groupBy === node.groupBy && having2 === node.having
71
79
  && (node.kind !== "query" || source === node.source);
72
80
  if (same) return node;
73
- const out = { ...node, where: where2, select, follow, frontier, from, orderBy, limit, offset };
81
+ const out = { ...node, where: where2, select, follow, frontier, groupBy, having: having2, from, orderBy, limit, offset };
74
82
  if (node.kind === "query") (out as Query).source = source!;
75
83
  return out;
76
84
  }
77
85
 
86
+ function resolveGroupBy(g: GroupBy, fail: Fail): GroupBy {
87
+ const keys = mapSame(g.keys, (k) => {
88
+ const expr = resolveExpr(k.expr, fail);
89
+ return expr === k.expr ? k : { ...k, expr };
90
+ });
91
+ return keys === g.keys ? g : { ...g, keys };
92
+ }
93
+
78
94
  function resolveOp(op: OpNode, fail: Fail): OpNode {
79
95
  const receiver = resolveExpr(op.receiver, fail);
80
96
  const sub = resolveBody(op.sub, fail);
@@ -145,6 +161,10 @@ function mapExpr(e: Expr, f: (e: Expr) => Expr | null): Expr {
145
161
  const parts = mapSame(e.parts, (p) => (typeof p === "string" ? p : go(p)));
146
162
  return parts === e.parts ? e : { ...e, parts };
147
163
  }
164
+ case "aggregate": {
165
+ const arg = e.arg ? go(e.arg) : null;
166
+ return arg === e.arg ? e : { ...e, arg };
167
+ }
148
168
  case "op": { // the receiver is read in this scope; the block is its own scope
149
169
  const receiver = go(e.receiver);
150
170
  return receiver === e.receiver ? e : { ...e, receiver };
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** The OQX language version (`spec/oqx/VERSION`, `major.minor`) this implementation
2
2
  * conforms to — distinct from the package's own semver, whose `major.minor` it
3
3
  * equals. Stamped on every `toJSON` document. */
4
- export const LANGUAGE_VERSION = "0.23";
4
+ export const LANGUAGE_VERSION = "0.24";
package/src/walk.ts CHANGED
@@ -14,7 +14,8 @@ import { LANGUAGE_VERSION } from "./version.ts";
14
14
  * The root node has no clause (`null`). */
15
15
  export type Clause =
16
16
  | "source" | "from" | "where" | "select" | "orderBy" | "limit" | "offset"
17
- | "follow" | "follow.destination" | "follow.frontier";
17
+ | "follow" | "follow.destination" | "follow.frontier"
18
+ | "groupBy" | "having"; // the `group by` keys and the post-group predicate (0.24)
18
19
 
19
20
  /** Where a visited node sits. `depth` is the scope depth at which the node is
20
21
  * evaluated (0 = the root scope: a top-level `source`, `limit`, `offset`; 1 =
@@ -57,6 +58,10 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
57
58
  // The body's `frontier` (0.20) reads each seed in the row scope, like `where`.
58
59
  { key: "frontier", clause: "follow.frontier", depth: 1 },
59
60
  { key: "follow", clause: "follow", depth: 1 },
61
+ // `group by` keys are read on the rows, `having` on the groups — both at
62
+ // the row depth (the group scope sits where the rows did; 0.24).
63
+ { key: "groupBy", clause: "groupBy", depth: 1 },
64
+ { key: "having", clause: "having", depth: 1 },
60
65
  { key: "orderBy", clause: "orderBy", depth: 1 },
61
66
  { key: "limit", clause: "limit" },
62
67
  { key: "offset", clause: "offset" },
@@ -71,12 +76,16 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
71
76
  // (`childDepth` tells the two apart by the block's position).
72
77
  { key: "frontier", clause: "follow.frontier", depth: -1 },
73
78
  { key: "follow", clause: "follow" },
79
+ { key: "groupBy", clause: "groupBy" },
80
+ { key: "having", clause: "having" },
74
81
  { key: "orderBy", clause: "orderBy" },
75
82
  { key: "limit", clause: "limit" },
76
83
  { key: "offset", clause: "offset" },
77
84
  ],
78
85
  op: [{ key: "receiver" }, { key: "sub", depth: 1 }],
79
86
  follow: [{ key: "destinations", clause: "follow.destination" }],
87
+ groupBy: [{ key: "keys" }],
88
+ key: [{ key: "expr" }],
80
89
  order: [{ key: "expr" }],
81
90
  field: [{ key: "expr" }],
82
91
  collect: [{ key: "op" }],
@@ -99,6 +108,8 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
99
108
  tuple: [{ key: "items" }],
100
109
  // The interpolations; the text parts between them are strings, not nodes.
101
110
  template: [{ key: "parts" }],
111
+ // The argument is read on each of the group's rows (0.24); `null` for `count()`.
112
+ aggregate: [{ key: "arg" }],
102
113
  };
103
114
 
104
115
  /** A child slot's value: a node, a list of nodes (a template's `parts` also