@omgbase/oqx 0.8.0 → 0.9.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/src/parser.ts CHANGED
@@ -20,12 +20,13 @@ import type {
20
20
  Query, Where, Expr, OpNode, Subquery, SelectItem, OrderSpec, Follow, Consumer, RelOp,
21
21
  } from "./ast.ts";
22
22
 
23
- const CONSUMERS = new Set<string>(["collect", "exists", "count", "first", "single"]);
23
+ const CONSUMERS = new Set<string>(["collect", "exists", "none", "count", "first", "single"]);
24
24
  // Contextual clause words lexed as bare idents; an open-ended range must stop
25
25
  // before them rather than consume them as its high bound.
26
26
  const CLAUSE_WORDS = new Set<string>([
27
- "collect", "exists", "count", "first", "single",
27
+ "collect", "exists", "none", "count", "first", "single",
28
28
  "order", "by", "asc", "desc", "follow", "distinct", "frontier", "depth", "in", "values",
29
+ "limit", "offset",
29
30
  ]);
30
31
  const RELOPS = new Set<string>(["==", "!=", "<", "<=", ">", ">="]);
31
32
  const CMP_OPS = new Set<string>(["==", "!=", "<", "<=", ">", ">="]);
@@ -50,6 +51,8 @@ interface BodyClauses {
50
51
  follow: Follow | null;
51
52
  distinct: boolean;
52
53
  values: boolean;
54
+ limit: Expr | null;
55
+ offset: Expr | null;
53
56
  }
54
57
 
55
58
  class Parser {
@@ -91,6 +94,7 @@ class Parser {
91
94
  follow: directive.sub.follow,
92
95
  distinct: directive.distinct ?? false,
93
96
  values: directive.sub.values ?? false,
97
+ ...bounds(directive.sub),
94
98
  };
95
99
  }
96
100
  if (directive) this.fail(`unexpected ${this.tokDesc()} after the top-level directive`);
@@ -111,6 +115,7 @@ class Parser {
111
115
  follow: body.follow,
112
116
  distinct: body.distinct,
113
117
  values: body.values,
118
+ ...bounds(body),
114
119
  };
115
120
  }
116
121
 
@@ -128,6 +133,8 @@ class Parser {
128
133
  let follow: Follow | null = null;
129
134
  let distinct = false;
130
135
  let values = false;
136
+ let limit: Expr | null = null;
137
+ let offset: Expr | null = null;
131
138
  let sawWhere = false, sawSelect = false, sawOrder = false;
132
139
 
133
140
  while (!this.at("eof") && !this.at("rbrace")) {
@@ -155,6 +162,13 @@ class Parser {
155
162
  ({ items: select, values } = this.parseProjection());
156
163
  continue;
157
164
  }
165
+ if (this.atBound()) {
166
+ const word = this.next().value;
167
+ const e = this.parsePostfix();
168
+ if (word === "limit") { if (limit) this.fail("duplicate `limit` clause"); limit = e; }
169
+ else { if (offset) this.fail("duplicate `offset` clause"); offset = e; }
170
+ continue;
171
+ }
158
172
  if (orderByAllowed && this.atOrderBy()) {
159
173
  if (sawOrder) this.fail("duplicate `order by` clause");
160
174
  sawOrder = true;
@@ -174,9 +188,19 @@ class Parser {
174
188
  ({ items: select, values } = this.parseProjection());
175
189
  continue;
176
190
  }
177
- this.fail(`unexpected ${this.tokDesc()} — expected from/where/select${orderByAllowed ? "/order by" : ""}/follow`);
191
+ this.fail(`unexpected ${this.tokDesc()} — expected from/where/select${orderByAllowed ? "/order by" : ""}/limit/offset/follow`);
178
192
  }
179
- return { froms, where, select, orderBy, follow, distinct, values };
193
+ return { froms, where, select, orderBy, follow, distinct, values, limit, offset };
194
+ }
195
+
196
+ // `limit <n>` / `offset <n>` — the word must be followed by something that can
197
+ // be a bound (a number, a binding, or an outer reference), so a field that
198
+ // happens to be called `limit` still projects/filters as a bare name.
199
+ private atBound(): boolean {
200
+ const t = this.peek();
201
+ if (t.type !== "ident" || (t.value !== "limit" && t.value !== "offset")) return false;
202
+ const nx = this.peekAt(1);
203
+ return !!nx && (nx.type === "number" || nx.type === "binding" || nx.type === "caret");
180
204
  }
181
205
 
182
206
  // Decide, by syntactic shape only, whether a leading unkeyworded run is a
@@ -333,7 +357,7 @@ class Parser {
333
357
  const op = this.tryOp();
334
358
  if (op) {
335
359
  if (op.op !== "collect" && op.op !== "first" && op.op !== "single") {
336
- this.fail(`projection '${name}' must use collect/first/single, not ${op.op}`);
360
+ this.fail(`projection '${name}' must use collect/first/single, not ${op.op} (exists/none/count are where-position tests)`);
337
361
  }
338
362
  if (lift) this.fail(`a lift (^${name}) value must be a scalar expression, not ${op.op} { … }`);
339
363
  return { kind: "collect", name, op };
@@ -405,7 +429,7 @@ class Parser {
405
429
  // Validate a consumer op used in where position and attach any `count { … } <op> N`.
406
430
  private finishWhereOp(op: OpNode): OpNode {
407
431
  if (op.op === "first" || op.op === "single") {
408
- this.fail(`${op.op} { … } is a select-position lookup; in where use exists { … } or count { … } <op> N`);
432
+ this.fail(`${op.op} { … } is a select-position lookup; in where use exists { … } / none { … } or count { … } <op> N`);
409
433
  }
410
434
  if (op.op === "collect") {
411
435
  const allLift = op.sub.select.length > 0 && op.sub.select.every((s) => s.kind === "field" && s.lift > 0);
@@ -457,7 +481,7 @@ class Parser {
457
481
  private parseSubquery(): { sub: Subquery; distinct: boolean } {
458
482
  const body = this.parseBody(true);
459
483
  return {
460
- sub: { from: body.froms, where: body.where, select: body.select, orderBy: body.orderBy, follow: body.follow, values: body.values },
484
+ sub: { from: body.froms, where: body.where, select: body.select, orderBy: body.orderBy, follow: body.follow, values: body.values, ...bounds(body) },
461
485
  distinct: body.distinct,
462
486
  };
463
487
  }
@@ -624,3 +648,9 @@ function navKey(e: Expr): string | null {
624
648
  default: return null;
625
649
  }
626
650
  }
651
+
652
+ // The optional `limit`/`offset` fields of a Query/Subquery, present only when set
653
+ // (so a Query built without them is unchanged).
654
+ function bounds(b: { limit?: Expr | null; offset?: Expr | null }): { limit?: Expr; offset?: Expr } {
655
+ return { ...(b.limit ? { limit: b.limit } : {}), ...(b.offset ? { offset: b.offset } : {}) };
656
+ }