@omgbase/oqx 0.10.2 → 0.13.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/CHANGELOG.md +192 -0
- package/README.md +138 -32
- package/dist/context.d.ts +21 -4
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -9
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +98 -67
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +47 -21
- package/dist/lexer.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +356 -163
- package/dist/parser.js.map +1 -1
- package/dist/regex.d.ts +19 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +0 -0
- package/dist/regex.js.map +1 -0
- package/dist/semantics.d.ts +37 -9
- package/dist/semantics.d.ts.map +1 -1
- package/dist/semantics.js +138 -36
- package/dist/semantics.js.map +1 -1
- package/package.json +1 -1
- package/src/context.ts +37 -10
- package/src/engine.ts +98 -65
- package/src/index.ts +7 -1
- package/src/lexer.ts +44 -14
- package/src/parser.ts +340 -119
- package/src/regex.ts +0 -0
- package/src/semantics.ts +128 -31
package/src/parser.ts
CHANGED
|
@@ -9,9 +9,31 @@
|
|
|
9
9
|
// 2. Whitespace directives (collect/exists/count/first/single) belong to OQX —
|
|
10
10
|
// a consumer is `<receiver> <directive> { <block> }`, never a method.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// Clause order is FIXED (ADR-020). Within one clause body — the top level or a
|
|
13
|
+
// consumer block — each clause appears at most once, in exactly this order:
|
|
14
|
+
//
|
|
15
|
+
// select <projection> from <source> where <predicate> follow <relation> {…}
|
|
16
|
+
// order by … limit N offset N
|
|
17
|
+
//
|
|
18
|
+
// Every clause is optional except that a top-level body needs `from` (the
|
|
19
|
+
// receiver-plus-consumer form `<receiver> <consumer> { block }` supplies the
|
|
20
|
+
// source itself, so its block's `from` is an optional re-projection). Only
|
|
21
|
+
// `select` may drop its keyword, and only when it is the first clause written
|
|
22
|
+
// (`name, age from people`); every other clause always carries its keyword, so
|
|
23
|
+
// a predicate is never implicit — a block filters with `where`. An out-of-order
|
|
24
|
+
// clause is a parse error naming the order. `where` may reference the same
|
|
25
|
+
// body's `select` aliases: after a body is parsed, each bare identifier in its
|
|
26
|
+
// `where` that names an alias is replaced by the alias's expression (a
|
|
27
|
+
// compile-time rewrite — see `inlineAliases`).
|
|
28
|
+
//
|
|
29
|
+
// Principle of least surprise, applied to the grammar: a rule a careful user
|
|
30
|
+
// would not predict is a bug. Hence `!` binds tighter than comparison in every
|
|
31
|
+
// position (`!a == b` is `(!a) == b`, as in C); parentheses in `where` group a
|
|
32
|
+
// predicate OR a scalar, decided by what follows the `)`; a range's open end
|
|
33
|
+
// stops at a clause word; duplicate projection names, a `follow distinct` with
|
|
34
|
+
// no relation, a lift outside a where-position `collect`, and a top-level
|
|
35
|
+
// `limit ^n` (there is no enclosing scope) are parse errors rather than silent
|
|
36
|
+
// misreads.
|
|
15
37
|
|
|
16
38
|
import type { Token, TokType } from "./lexer.ts";
|
|
17
39
|
import { lexTemplate, lexString } from "./lexer.ts";
|
|
@@ -21,6 +43,9 @@ import type {
|
|
|
21
43
|
} from "./ast.ts";
|
|
22
44
|
|
|
23
45
|
const CONSUMERS = new Set<string>(["collect", "exists", "none", "count", "first", "single"]);
|
|
46
|
+
// The fixed clause order of a body (ADR-020). Each clause appears at most once.
|
|
47
|
+
const CLAUSE_ORDER = ["select", "from", "where", "follow", "order by", "limit", "offset"] as const;
|
|
48
|
+
type Clause = (typeof CLAUSE_ORDER)[number];
|
|
24
49
|
// Contextual clause words lexed as bare idents; an open-ended range must stop
|
|
25
50
|
// before them rather than consume them as its high bound.
|
|
26
51
|
const CLAUSE_WORDS = new Set<string>([
|
|
@@ -32,6 +57,20 @@ const RELOPS = new Set<string>(["==", "!=", "<", "<=", ">", ">="]);
|
|
|
32
57
|
const CMP_OPS = new Set<string>(["==", "!=", "<", "<=", ">", ">="]);
|
|
33
58
|
const ADD_OPS = new Set<string>(["+", "-"]);
|
|
34
59
|
const MUL_OPS = new Set<string>(["*", "/", "%"]);
|
|
60
|
+
// `true`/`false`/`null` are literals in every position, so they can never name a
|
|
61
|
+
// receiver (`true exists { … }`, `follow null`).
|
|
62
|
+
const LITERAL_WORDS = new Set<string>(["true", "false", "null"]);
|
|
63
|
+
|
|
64
|
+
// Where a clause body sits, for error messages and position-dependent rules.
|
|
65
|
+
interface BodyCtx {
|
|
66
|
+
/** The top-level body form (a stray token there is "after the query"). */
|
|
67
|
+
top: boolean;
|
|
68
|
+
/** The consumer of the enclosing block, when not top-level. */
|
|
69
|
+
op: Consumer | null;
|
|
70
|
+
/** Whether `^name: expr` lift items are legal: only in a where-position `collect { … }`. */
|
|
71
|
+
liftsAllowed: boolean;
|
|
72
|
+
}
|
|
73
|
+
const TOP_CTX: BodyCtx = { top: true, op: null, liftsAllowed: false };
|
|
35
74
|
|
|
36
75
|
/** Parse a tagged-template call into a Query. */
|
|
37
76
|
export function parseTemplate(fragments: readonly string[], values: number): Query {
|
|
@@ -82,7 +121,7 @@ class Parser {
|
|
|
82
121
|
// ---- top level ------------------------------------------------------------
|
|
83
122
|
parseQuery(): Query {
|
|
84
123
|
// Directive form: `<receiver> <consumer> { … }` consuming the whole query.
|
|
85
|
-
const directive = this.tryOp();
|
|
124
|
+
const directive = this.tryOp(false);
|
|
86
125
|
if (directive && this.at("eof")) {
|
|
87
126
|
return {
|
|
88
127
|
source: directive.receiver,
|
|
@@ -99,11 +138,11 @@ class Parser {
|
|
|
99
138
|
}
|
|
100
139
|
if (directive) this.fail(`unexpected ${this.tokDesc()} after the top-level directive`);
|
|
101
140
|
|
|
102
|
-
// Body form:
|
|
103
|
-
const body = this.parseBody(
|
|
104
|
-
if (!this.at("eof")) this.fail(`unexpected ${this.tokDesc()} after the query`);
|
|
141
|
+
// Body form: the fixed clause list; its `from` is the source.
|
|
142
|
+
const body = this.parseBody(TOP_CTX);
|
|
143
|
+
if (!this.at("eof")) this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause`);
|
|
105
144
|
if (body.froms.length === 0) {
|
|
106
|
-
this.fail("a query must
|
|
145
|
+
this.fail("a query must name its source with `from <collection>` (or be `<collection> <consumer> { … }`)");
|
|
107
146
|
}
|
|
108
147
|
return {
|
|
109
148
|
source: body.froms[0]!,
|
|
@@ -125,7 +164,11 @@ class Parser {
|
|
|
125
164
|
}
|
|
126
165
|
|
|
127
166
|
// ---- clause body (shared by top level and consumer blocks) ----------------
|
|
128
|
-
|
|
167
|
+
// An ordered state machine over the fixed clause sequence: `stage` is the
|
|
168
|
+
// index (in CLAUSE_ORDER) of the last clause parsed, so a clause with a lower
|
|
169
|
+
// index is out of order and an equal index is a duplicate. The only
|
|
170
|
+
// keyword-less clause is a leading projection (while `stage` is still -1).
|
|
171
|
+
private parseBody(ctx: BodyCtx): BodyClauses {
|
|
129
172
|
const froms: Expr[] = [];
|
|
130
173
|
let where: Where | null = null;
|
|
131
174
|
let select: SelectItem[] = [];
|
|
@@ -135,64 +178,114 @@ class Parser {
|
|
|
135
178
|
let values = false;
|
|
136
179
|
let limit: Expr | null = null;
|
|
137
180
|
let offset: Expr | null = null;
|
|
138
|
-
let
|
|
181
|
+
let stage = -1;
|
|
182
|
+
|
|
183
|
+
const enter = (clause: Clause): void => {
|
|
184
|
+
const idx = CLAUSE_ORDER.indexOf(clause);
|
|
185
|
+
if (idx === stage) this.fail(`duplicate \`${clause}\` clause`);
|
|
186
|
+
if (idx < stage) {
|
|
187
|
+
this.fail(`\`${clause}\` must come before \`${CLAUSE_ORDER[stage]}\` — OQX clause order is ${CLAUSE_ORDER.join(", ")}`);
|
|
188
|
+
}
|
|
189
|
+
stage = idx;
|
|
190
|
+
};
|
|
139
191
|
|
|
140
192
|
while (!this.at("eof") && !this.at("rbrace")) {
|
|
141
|
-
if (this.
|
|
142
|
-
|
|
143
|
-
|
|
193
|
+
if (this.at("kw", "select")) {
|
|
194
|
+
enter("select");
|
|
195
|
+
this.next();
|
|
196
|
+
if (this.at("ident", "distinct")) { this.next(); distinct = true; }
|
|
197
|
+
({ items: select, values } = this.parseProjection(ctx));
|
|
144
198
|
continue;
|
|
145
199
|
}
|
|
146
200
|
if (this.at("kw", "from")) {
|
|
201
|
+
enter("from");
|
|
147
202
|
this.next();
|
|
148
203
|
froms.push(this.parseValueExpr());
|
|
149
204
|
continue;
|
|
150
205
|
}
|
|
151
206
|
if (this.at("kw", "where")) {
|
|
152
|
-
|
|
153
|
-
sawWhere = true;
|
|
207
|
+
enter("where");
|
|
154
208
|
this.next();
|
|
155
209
|
where = this.parseWhere();
|
|
156
210
|
continue;
|
|
157
211
|
}
|
|
158
|
-
if (this.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (this.at("kw")) { this.next(); if (this.at("ident", "distinct")) { this.next(); distinct = true; } } // consume `select` + optional `distinct`; a leading `^` is part of the item
|
|
162
|
-
({ items: select, values } = this.parseProjection());
|
|
163
|
-
continue;
|
|
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; }
|
|
212
|
+
if (this.atFollow()) {
|
|
213
|
+
enter("follow");
|
|
214
|
+
follow = this.parseFollow();
|
|
170
215
|
continue;
|
|
171
216
|
}
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
sawOrder = true;
|
|
217
|
+
if (this.atOrderBy()) {
|
|
218
|
+
enter("order by");
|
|
175
219
|
this.next(); this.next(); // `order` `by`
|
|
176
220
|
orderBy = this.parseOrderSpecs();
|
|
177
221
|
continue;
|
|
178
222
|
}
|
|
179
|
-
if (this.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
223
|
+
if (this.atBound()) {
|
|
224
|
+
const word = this.peek().value as "limit" | "offset";
|
|
225
|
+
enter(word);
|
|
226
|
+
this.next();
|
|
227
|
+
// A top-level bound is evaluated at the root scope itself, so `^n` there
|
|
228
|
+
// has nothing to read: say so now instead of an "absent" error at eval.
|
|
229
|
+
if (ctx.top && this.at("caret")) {
|
|
230
|
+
this.fail(`\`${word} ^…\` at the top level has no enclosing scope — a top-level bound is a number literal or a binding; inside a block \`^name\` reads the enclosing row`);
|
|
231
|
+
}
|
|
232
|
+
const e = this.parsePostfix();
|
|
233
|
+
if (word === "limit") limit = e; else offset = e;
|
|
183
234
|
continue;
|
|
184
235
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
236
|
+
// A keyword-less run. Before any clause it is the projection (`select` is
|
|
237
|
+
// the one keyword that may be dropped, and only in first position); after
|
|
238
|
+
// any clause it is an error — a predicate is never implicit.
|
|
239
|
+
if (stage === -1 && (this.at("ident") || this.at("binding") || this.at("caret") || this.canStartValue())) {
|
|
240
|
+
enter("select");
|
|
241
|
+
({ items: select, values } = this.parseProjection(ctx));
|
|
189
242
|
continue;
|
|
190
243
|
}
|
|
191
|
-
this.
|
|
244
|
+
this.failUnexpectedInBody(stage, ctx);
|
|
192
245
|
}
|
|
246
|
+
where = this.inlineAliases(select, where);
|
|
193
247
|
return { froms, where, select, orderBy, follow, distinct, values, limit, offset };
|
|
194
248
|
}
|
|
195
249
|
|
|
250
|
+
// The error for a token that starts no clause, phrased for the mistake it most
|
|
251
|
+
// likely is: a bare run right after `from` (the old implicit `where`, or a
|
|
252
|
+
// consumer word where a whole-query directive was meant), or a stray token.
|
|
253
|
+
private failUnexpectedInBody(stage: number, ctx: BodyCtx): never {
|
|
254
|
+
const t = this.peek();
|
|
255
|
+
const remaining = CLAUSE_ORDER.slice(stage + 1).join("/");
|
|
256
|
+
const last = stage >= 0 ? CLAUSE_ORDER[stage] : null;
|
|
257
|
+
const isWord = t.type === "ident" || t.type === "kw" || t.type === "binding" || t.type === "caret";
|
|
258
|
+
// Punctuation, an operator, or a literal after a complete clause is a stray
|
|
259
|
+
// token, not a misplaced predicate: name where the body ends. (A comma keeps
|
|
260
|
+
// the projection hint below — `name from r, id` almost always meant a
|
|
261
|
+
// projection.)
|
|
262
|
+
if (last !== null && !isWord && t.type !== "comma") {
|
|
263
|
+
if (ctx.top) this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause (expected ${remaining} or the end of the query)`);
|
|
264
|
+
this.fail(`unexpected ${this.tokDesc()} in the ${ctx.op} { … } block — expected ${remaining} or '}' to close the block`);
|
|
265
|
+
}
|
|
266
|
+
// Clause words that did not form a clause: say what the clause needs.
|
|
267
|
+
if (last !== null && t.type === "ident") {
|
|
268
|
+
if (t.value === "order") this.fail(`unexpected 'order' after \`${last}\` — an ordering is written \`order by <expr> [asc|desc]\``);
|
|
269
|
+
if (t.value === "follow") this.fail(`unexpected 'follow' after \`${last}\` — \`follow\` needs a relation: \`follow <relation>\` or \`follow distinct <relation>\``);
|
|
270
|
+
if (t.value === "limit" || t.value === "offset") {
|
|
271
|
+
this.fail(`unexpected '${t.value}' after \`${last}\` — a bound is a non-negative number literal, a binding, or (inside a block) an outer reference \`^name\``);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (last === "from" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
275
|
+
this.fail(`unexpected \`${t.value}\` after \`from\` — a whole-query consumer is written \`<collection> ${t.value} { … }\`; to project a field named ${t.value} write \`select ${t.value} from …\`; a predicate needs \`where\``);
|
|
276
|
+
}
|
|
277
|
+
if (last === "from") {
|
|
278
|
+
this.fail(`unexpected ${this.tokDesc()} after \`from\` — a predicate needs \`where\` (there is no implicit where), and a projection goes before \`from\` (\`select … from …\`); expected ${remaining}`);
|
|
279
|
+
}
|
|
280
|
+
// `{ rel exists { … } }` / `{ rel count { … } >= 2 }`: the leading `rel` was
|
|
281
|
+
// read as the projection, so the consumer word is where the mistake shows.
|
|
282
|
+
if (last === "select" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
283
|
+
this.fail(`unexpected \`${t.value}\` after a projection — a consumer test is a predicate: write \`where <relation> ${t.value} { … }\` — a predicate is never implicit; a nested block in a projection needs a name (\`name: <relation> collect { … }\`)`);
|
|
284
|
+
}
|
|
285
|
+
if (last === null) this.fail(`unexpected ${this.tokDesc()} — expected a projection or ${remaining}`);
|
|
286
|
+
this.fail(`unexpected ${this.tokDesc()} after \`${last}\` — expected ${remaining}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
196
289
|
// `limit <n>` / `offset <n>` — the word must be followed by something that can
|
|
197
290
|
// be a bound (a number, a binding, or an outer reference), so a field that
|
|
198
291
|
// happens to be called `limit` still projects/filters as a bare name.
|
|
@@ -203,39 +296,74 @@ class Parser {
|
|
|
203
296
|
return !!nx && (nx.type === "number" || nx.type === "binding" || nx.type === "caret");
|
|
204
297
|
}
|
|
205
298
|
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
299
|
+
// ---- alias inlining -------------------------------------------------------
|
|
300
|
+
// `where` may reference the same body's `select` aliases. This is a
|
|
301
|
+
// compile-time rewrite, not a second execution pass: every bare identifier in
|
|
302
|
+
// the where tree that names an alias is replaced by that alias's expression,
|
|
303
|
+
// so the engine and any pushdown planner see an ordinary where over row
|
|
304
|
+
// fields. Rules:
|
|
305
|
+
// • an alias shadows a same-named row field inside `where`;
|
|
306
|
+
// • an alias's own name inside its own expression is the row field
|
|
307
|
+
// (`name: name.upper()` is not recursive), but a chain of aliases that
|
|
308
|
+
// comes back to one being resolved (`a: b, b: a`) is a cycle → error;
|
|
309
|
+
// • an alias whose value is a `collect`/`first`/`single { … }` block may
|
|
310
|
+
// stand alone as a where leaf (a collection in predicate position means
|
|
311
|
+
// non-empty) but not appear inside an expression;
|
|
312
|
+
// • nested blocks (consumer bodies, follow blocks) are their own scopes and
|
|
313
|
+
// are not rewritten against this body's select — each body rewrites
|
|
314
|
+
// against its own.
|
|
315
|
+
private inlineAliases(select: SelectItem[], where: Where | null): Where | null {
|
|
316
|
+
if (!where || select.length === 0) return where;
|
|
317
|
+
const aliases = new Map<string, SelectItem>();
|
|
318
|
+
for (const it of select) {
|
|
319
|
+
if (it.kind === "collect") aliases.set(it.name, it);
|
|
320
|
+
else if (it.lift === 0 && it.name !== "") aliases.set(it.name, it);
|
|
321
|
+
}
|
|
322
|
+
if (aliases.size === 0) return where;
|
|
323
|
+
const resolving: string[] = [];
|
|
324
|
+
const subst = (e: Expr): Expr => {
|
|
325
|
+
switch (e.kind) {
|
|
326
|
+
case "ident": {
|
|
327
|
+
const a = aliases.get(e.name);
|
|
328
|
+
if (!a) return e;
|
|
329
|
+
if (resolving[resolving.length - 1] === e.name) return e; // its own name inside its own expression: the row field
|
|
330
|
+
if (resolving.includes(e.name)) {
|
|
331
|
+
const cycle = [...resolving.slice(resolving.indexOf(e.name)), e.name].join(" → ");
|
|
332
|
+
this.fail(`select aliases form a cycle: ${cycle} — an alias used in \`where\` cannot depend on itself`);
|
|
333
|
+
}
|
|
334
|
+
if (a.kind === "collect") {
|
|
335
|
+
this.fail(`select alias '${e.name}' is a ${a.op.op} { … } block — in \`where\` it can only stand alone as a non-empty test, not inside an expression`);
|
|
336
|
+
}
|
|
337
|
+
resolving.push(e.name);
|
|
338
|
+
const out = subst(a.expr);
|
|
339
|
+
resolving.pop();
|
|
340
|
+
return out;
|
|
231
341
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
342
|
+
case "member": return { ...e, recv: subst(e.recv) };
|
|
343
|
+
case "index": return { ...e, recv: subst(e.recv), index: subst(e.index) };
|
|
344
|
+
case "call": return { ...e, recv: e.recv ? subst(e.recv) : null, args: e.args.map(subst) };
|
|
345
|
+
case "unary": return { ...e, expr: subst(e.expr) };
|
|
346
|
+
case "binary": case "logical": case "in": return { ...e, left: subst(e.left), right: subst(e.right) };
|
|
347
|
+
case "range": return { ...e, lo: e.lo ? subst(e.lo) : null, hi: e.hi ? subst(e.hi) : null };
|
|
348
|
+
default: return e; // lit, binding, outer (`^name` reads an enclosing row, never an alias)
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const walk = (w: Where): Where => {
|
|
352
|
+
switch (w.kind) {
|
|
353
|
+
case "and": return { kind: "and", parts: w.parts.map(walk) };
|
|
354
|
+
case "or": return { kind: "or", parts: w.parts.map(walk) };
|
|
355
|
+
case "not": return { kind: "not", expr: walk(w.expr) };
|
|
356
|
+
case "scalar": {
|
|
357
|
+
if (w.expr.kind === "ident") {
|
|
358
|
+
const a = aliases.get(w.expr.name);
|
|
359
|
+
if (a?.kind === "collect") return a.op; // a collection in predicate position: non-empty
|
|
360
|
+
}
|
|
361
|
+
return { kind: "scalar", expr: subst(w.expr) };
|
|
235
362
|
}
|
|
363
|
+
case "op": return { ...w, receiver: subst(w.receiver) }; // the receiver is read in this scope; the block is its own scope
|
|
236
364
|
}
|
|
237
|
-
}
|
|
238
|
-
return
|
|
365
|
+
};
|
|
366
|
+
return walk(where);
|
|
239
367
|
}
|
|
240
368
|
|
|
241
369
|
private atOrderBy(): boolean {
|
|
@@ -244,19 +372,29 @@ class Parser {
|
|
|
244
372
|
return t.type === "ident" && t.value === "order" && !!nx && nx.type === "ident" && nx.value === "by";
|
|
245
373
|
}
|
|
246
374
|
|
|
375
|
+
// `follow` is a clause when a relation (or something that tries to be one —
|
|
376
|
+
// `distinct`, an outer reference) follows; otherwise it is a field name.
|
|
247
377
|
private atFollow(): boolean {
|
|
248
378
|
if (!this.at("ident", "follow")) return false;
|
|
249
379
|
const nx = this.peekAt(1);
|
|
250
|
-
return !!nx && (nx.type === "ident" || nx.type === "binding");
|
|
380
|
+
return !!nx && (nx.type === "ident" || nx.type === "binding" || nx.type === "caret");
|
|
251
381
|
}
|
|
252
382
|
|
|
253
383
|
// ---- follow ---------------------------------------------------------------
|
|
254
384
|
private parseFollow(): Follow {
|
|
255
385
|
this.next(); // `follow`
|
|
256
386
|
let distinct = false;
|
|
387
|
+
// After `follow`, `distinct` is a keyword (a relation literally named
|
|
388
|
+
// `distinct` is not supported); it must be followed by the relation.
|
|
257
389
|
if (this.at("ident", "distinct")) {
|
|
258
|
-
|
|
259
|
-
|
|
390
|
+
this.next();
|
|
391
|
+
distinct = true;
|
|
392
|
+
if (!this.at("ident") && !this.at("binding") && !this.at("caret")) {
|
|
393
|
+
this.fail("expected a relation after `follow distinct` (`follow distinct <relation>`)");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
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");
|
|
260
398
|
}
|
|
261
399
|
const receiver = this.parseReceiver();
|
|
262
400
|
const follow: Follow = { receiver, distinct, where: null, frontier: null, depth: null, by: null };
|
|
@@ -300,6 +438,7 @@ class Parser {
|
|
|
300
438
|
if (this.at("binding")) return { kind: "binding", index: this.next().index! };
|
|
301
439
|
const levels = this.parseCarets();
|
|
302
440
|
if (!this.at("ident")) this.fail("expected a collection navigation (a property/relation name)");
|
|
441
|
+
if (LITERAL_WORDS.has(this.peek().value)) this.fail(`\`${this.peek().value}\` is a literal, not a collection`);
|
|
303
442
|
return this.parseNavFrom(this.next(), levels).expr;
|
|
304
443
|
}
|
|
305
444
|
|
|
@@ -319,7 +458,7 @@ class Parser {
|
|
|
319
458
|
if (levels === 0 && this.at("lparen")) expr = { kind: "call", recv: null, name: head.value, args: this.parseArgs() };
|
|
320
459
|
while (this.at("dot")) {
|
|
321
460
|
this.next();
|
|
322
|
-
if (!this.at("ident")) this.fail("expected
|
|
461
|
+
if (!this.at("ident")) this.fail("expected a property name after '.'");
|
|
323
462
|
name = this.next().value;
|
|
324
463
|
expr = { kind: "member", recv: expr, name };
|
|
325
464
|
}
|
|
@@ -333,9 +472,9 @@ class Parser {
|
|
|
333
472
|
// meaningless. Without `values`, every item needs a key — a bare/dotted
|
|
334
473
|
// navigation supplies its own (the last segment); any other expression must be
|
|
335
474
|
// aliased (`name: expr`).
|
|
336
|
-
private parseProjection(): { items: SelectItem[]; values: boolean } {
|
|
337
|
-
const items = [this.parseSelectItem()];
|
|
338
|
-
while (this.at("comma")) { this.next(); items.push(this.parseSelectItem()); }
|
|
475
|
+
private parseProjection(ctx: BodyCtx): { items: SelectItem[]; values: boolean } {
|
|
476
|
+
const items = [this.parseSelectItem(ctx)];
|
|
477
|
+
while (this.at("comma")) { this.next(); items.push(this.parseSelectItem(ctx)); }
|
|
339
478
|
let values = false;
|
|
340
479
|
if (this.at("ident", "values")) {
|
|
341
480
|
this.next();
|
|
@@ -344,20 +483,36 @@ class Parser {
|
|
|
344
483
|
const only = items[0]!;
|
|
345
484
|
if (only.kind === "field" && only.lift > 0) this.fail("a lift (^name: …) cannot be combined with `values`");
|
|
346
485
|
} else {
|
|
486
|
+
// Every item needs a distinct key: two items with one name would silently
|
|
487
|
+
// overwrite each other in the record. Lifts are keyed per scope (`^x` and
|
|
488
|
+
// `^^x` bind different rows), so the lift depth is part of the key.
|
|
489
|
+
const seen = new Set<string>();
|
|
347
490
|
for (const it of items) {
|
|
348
|
-
if (it.name === "") this.fail("a projection item that is not a plain name needs an alias (`name: expr`)
|
|
491
|
+
if (it.name === "") 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 …` — a predicate is never implicit");
|
|
492
|
+
const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
|
|
493
|
+
if (seen.has(key)) this.fail(`duplicate projection name '${it.name}' — each projected item needs its own name (alias one: \`other: expr\`)`);
|
|
494
|
+
seen.add(key);
|
|
349
495
|
}
|
|
350
496
|
}
|
|
351
497
|
return { items, values };
|
|
352
498
|
}
|
|
353
499
|
|
|
354
|
-
private parseSelectItem(): SelectItem {
|
|
355
|
-
// Leading `^`s mark a lift; the count is how many scopes out it binds.
|
|
500
|
+
private parseSelectItem(ctx: BodyCtx): SelectItem {
|
|
501
|
+
// Leading `^`s mark a lift; the count is how many scopes out it binds. A
|
|
502
|
+
// lift is bound by a `collect { … }` in where position and nowhere else — at
|
|
503
|
+
// the top level, in a select-position block, or in an exists/none/count
|
|
504
|
+
// block it would silently do nothing (or act as a plain field), so it is an
|
|
505
|
+
// error there.
|
|
356
506
|
const lift = this.parseCarets();
|
|
507
|
+
if (!this.at("ident") && !this.canStartValue()) this.fail("expected a projection name");
|
|
508
|
+
if (lift > 0 && !ctx.liftsAllowed) {
|
|
509
|
+
const what = this.at("ident") ? `^${this.peek().value}` : "^name";
|
|
510
|
+
this.fail(`a lift (${what}) binds a value into the enclosing row and is only valid in a \`collect { … }\` in where position (\`where <relation> collect { ${what}: … }\`)`);
|
|
511
|
+
}
|
|
357
512
|
if (this.at("ident") && this.peekAt(1)?.type === "colon") {
|
|
358
513
|
const name = this.next().value;
|
|
359
514
|
this.next(); // ':'
|
|
360
|
-
const op = this.tryOp();
|
|
515
|
+
const op = this.tryOp(false);
|
|
361
516
|
if (op) {
|
|
362
517
|
if (op.op !== "collect" && op.op !== "first" && op.op !== "single") {
|
|
363
518
|
this.fail(`projection '${name}' must use collect/first/single, not ${op.op} (exists/none/count are where-position tests)`);
|
|
@@ -368,7 +523,6 @@ class Parser {
|
|
|
368
523
|
const expr = this.parseValueExpr();
|
|
369
524
|
return { kind: "field", name, expr, lift };
|
|
370
525
|
}
|
|
371
|
-
if (!this.at("ident") && !this.canStartValue()) this.fail("expected a projection name");
|
|
372
526
|
// Unaliased item: a bare/dotted navigation keys by its last segment; any
|
|
373
527
|
// other expression is unnamed ("") — legal only under `values` (checked by
|
|
374
528
|
// parseProjection, which sees the whole list).
|
|
@@ -391,7 +545,10 @@ class Parser {
|
|
|
391
545
|
return { expr, desc };
|
|
392
546
|
}
|
|
393
547
|
|
|
394
|
-
// ---- where boolean tree: or → and →
|
|
548
|
+
// ---- where boolean tree: or → and → primary --------------------------------
|
|
549
|
+
// `!` is handled in `parseWherePrimary`: it applies to the operand right after
|
|
550
|
+
// it (a consumer test, a parenthesized group, or a scalar primary), never to a
|
|
551
|
+
// whole comparison — `!a == b` is `(!a) == b`, exactly as in value position.
|
|
395
552
|
private parseWhere(): Where { return this.parseWhereOr(); }
|
|
396
553
|
|
|
397
554
|
private parseWhereOr(): Where {
|
|
@@ -403,30 +560,69 @@ class Parser {
|
|
|
403
560
|
}
|
|
404
561
|
|
|
405
562
|
private parseWhereAnd(): Where {
|
|
406
|
-
const left = this.
|
|
563
|
+
const left = this.parseWherePrimary();
|
|
407
564
|
if (!this.atOp("&&")) return left;
|
|
408
565
|
const parts = [left];
|
|
409
|
-
while (this.atOp("&&")) { this.next(); parts.push(this.
|
|
566
|
+
while (this.atOp("&&")) { this.next(); parts.push(this.parseWherePrimary()); }
|
|
410
567
|
return { kind: "and", parts };
|
|
411
568
|
}
|
|
412
569
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
570
|
+
// A where operand: `[!…] ( where )`, `[!…] <receiver> <consumer> { … }`, or a
|
|
571
|
+
// scalar leaf (a cmp-level expression, which handles its own `!`).
|
|
572
|
+
//
|
|
573
|
+
// Parentheses group EITHER a predicate or a scalar, decided by what follows
|
|
574
|
+
// the `)`: a comparison, arithmetic, `in`, a range, or `.`-navigation means
|
|
575
|
+
// the group is a scalar operand (`(a + 1) > 2`, `(a || b) == 5`,
|
|
576
|
+
// `(x).size() > 1`); anything else means it is a predicate group
|
|
577
|
+
// (`(a > 1) && b`). A group that contains a consumer test can only be a
|
|
578
|
+
// predicate.
|
|
418
579
|
private parseWherePrimary(): Where {
|
|
580
|
+
const start = this.pos;
|
|
581
|
+
let nots = 0;
|
|
582
|
+
while (this.atOp("!")) { this.next(); nots++; }
|
|
419
583
|
if (this.at("lparen")) {
|
|
420
584
|
this.next();
|
|
421
|
-
const
|
|
585
|
+
const inner = this.parseWhere();
|
|
422
586
|
if (!this.at("rparen")) this.fail("expected ')' to close a grouped where expression");
|
|
423
587
|
this.next();
|
|
424
|
-
|
|
588
|
+
if (this.atScalarContinuation()) {
|
|
589
|
+
let e = this.parsePostfix(this.whereToExpr(inner));
|
|
590
|
+
for (let i = 0; i < nots; i++) e = { kind: "unary", op: "!", expr: e };
|
|
591
|
+
return { kind: "scalar", expr: this.parseCmp(e) };
|
|
592
|
+
}
|
|
593
|
+
return wrapNot(inner, nots);
|
|
594
|
+
}
|
|
595
|
+
const op = this.tryOp(true);
|
|
596
|
+
if (op) return wrapNot(this.finishWhereOp(op), nots);
|
|
597
|
+
// A scalar leaf, re-read from the first `!` so the scalar grammar gives `!`
|
|
598
|
+
// its one precedence (tighter than comparison). A leaf whose whole value is
|
|
599
|
+
// a negation keeps the `not` node shape (`where !active`).
|
|
600
|
+
this.pos = start;
|
|
601
|
+
return scalarLeaf(this.parseCmp());
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Whether the token after a `)` continues a scalar expression.
|
|
605
|
+
private atScalarContinuation(): boolean {
|
|
606
|
+
const t = this.peek();
|
|
607
|
+
if (t.type === "op") return CMP_OPS.has(t.value) || ADD_OPS.has(t.value) || MUL_OPS.has(t.value);
|
|
608
|
+
return t.type === "dot" || t.type === "range" || (t.type === "ident" && t.value === "in");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// A parenthesized where-group that turned out to be a scalar operand, as an
|
|
612
|
+
// expression. A consumer test has no scalar value, so it cannot be operated on.
|
|
613
|
+
private whereToExpr(w: Where): Expr {
|
|
614
|
+
switch (w.kind) {
|
|
615
|
+
case "scalar": return w.expr;
|
|
616
|
+
case "not": return { kind: "unary", op: "!", expr: this.whereToExpr(w.expr) };
|
|
617
|
+
case "and": case "or": {
|
|
618
|
+
const op = w.kind === "and" ? "&&" : "||";
|
|
619
|
+
return w.parts.map((p) => this.whereToExpr(p)).reduce((l, r) => ({ kind: "logical", op, left: l, right: r }));
|
|
620
|
+
}
|
|
621
|
+
case "op": {
|
|
622
|
+
const hint = w.op === "count" ? ` — write \`<relation> count { … } ${this.peek().value} N\` without the parentheses` : "";
|
|
623
|
+
this.fail(`a consumer test (${w.op} { … }) is a predicate, not a value, so it cannot be compared or operated on${hint}`);
|
|
624
|
+
}
|
|
425
625
|
}
|
|
426
|
-
const op = this.tryOp();
|
|
427
|
-
if (op) return this.finishWhereOp(op);
|
|
428
|
-
const expr = this.parseCmp();
|
|
429
|
-
return { kind: "scalar", expr };
|
|
430
626
|
}
|
|
431
627
|
|
|
432
628
|
// Validate a consumer op used in where position and attach any `count { … } <op> N`.
|
|
@@ -451,7 +647,7 @@ class Parser {
|
|
|
451
647
|
|
|
452
648
|
// Detect + parse a postfix consumer op `<receiver> <consumer> { <sub> }`.
|
|
453
649
|
// Returns null (rewinding) when the lookahead is not a consumer op.
|
|
454
|
-
private tryOp(): OpNode | null {
|
|
650
|
+
private tryOp(inWhere: boolean): OpNode | null {
|
|
455
651
|
const start = this.pos;
|
|
456
652
|
let receiver: Expr;
|
|
457
653
|
if (this.at("binding")) receiver = { kind: "binding", index: this.next().index! };
|
|
@@ -467,11 +663,12 @@ class Parser {
|
|
|
467
663
|
const opThenBrace = after?.type === "lbrace";
|
|
468
664
|
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
469
665
|
if (opThenBrace || opDistinctBrace) {
|
|
666
|
+
if (receiver.kind === "ident" && LITERAL_WORDS.has(receiver.name)) this.fail(`\`${receiver.name}\` is a literal, not a collection`);
|
|
470
667
|
const op = this.next().value as Consumer;
|
|
471
668
|
let distinct = false;
|
|
472
669
|
if (this.at("ident", "distinct")) { this.next(); distinct = true; }
|
|
473
670
|
this.next(); // '{'
|
|
474
|
-
const { sub, distinct: bodyDistinct } = this.parseSubquery();
|
|
671
|
+
const { sub, distinct: bodyDistinct } = this.parseSubquery({ top: false, op, liftsAllowed: inWhere && op === "collect" });
|
|
475
672
|
if (!this.at("rbrace")) this.fail(`expected '}' to close the ${op} { … } block`);
|
|
476
673
|
this.next();
|
|
477
674
|
return { kind: "op", receiver, op, sub, distinct: distinct || bodyDistinct };
|
|
@@ -481,8 +678,8 @@ class Parser {
|
|
|
481
678
|
return null;
|
|
482
679
|
}
|
|
483
680
|
|
|
484
|
-
private parseSubquery(): { sub: Subquery; distinct: boolean } {
|
|
485
|
-
const body = this.parseBody(
|
|
681
|
+
private parseSubquery(ctx: BodyCtx): { sub: Subquery; distinct: boolean } {
|
|
682
|
+
const body = this.parseBody(ctx);
|
|
486
683
|
return {
|
|
487
684
|
sub: { from: body.froms, where: body.where, select: body.select, orderBy: body.orderBy, follow: body.follow, values: body.values, ...bounds(body) },
|
|
488
685
|
distinct: body.distinct,
|
|
@@ -506,15 +703,24 @@ class Parser {
|
|
|
506
703
|
}
|
|
507
704
|
|
|
508
705
|
// Comparison / membership (also the entry point for a where scalar leaf, so a
|
|
509
|
-
// where leaf never swallows the where-tree's && / ||).
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
706
|
+
// where leaf never swallows the where-tree's && / ||). Non-associative: a
|
|
707
|
+
// second comparison in a row is an error, not a silent stray token.
|
|
708
|
+
//
|
|
709
|
+
// Each level from here down takes an optional already-parsed `left` operand,
|
|
710
|
+
// so a parenthesized where-group promoted to a scalar continues into the
|
|
711
|
+
// operator tail without re-reading its tokens.
|
|
712
|
+
private parseCmp(left?: Expr): Expr {
|
|
713
|
+
const lhs = this.parseRange(left);
|
|
714
|
+
const t = this.peek();
|
|
715
|
+
const isCmp = (): boolean => (this.peek().type === "op" && CMP_OPS.has(this.peek().value)) || this.at("ident", "in");
|
|
716
|
+
if (!isCmp()) return lhs;
|
|
717
|
+
const op = this.next().value;
|
|
718
|
+
const rhs = this.parseRange();
|
|
719
|
+
const result: Expr = op === "in" ? { kind: "in", left: lhs, right: rhs } : { kind: "binary", op, left: lhs, right: rhs };
|
|
720
|
+
if (isCmp()) {
|
|
721
|
+
this.fail(`comparisons do not chain: \`a ${t.value} b ${this.peek().value} c\` — write two comparisons joined with \`&&\``);
|
|
515
722
|
}
|
|
516
|
-
|
|
517
|
-
return left;
|
|
723
|
+
return result;
|
|
518
724
|
}
|
|
519
725
|
|
|
520
726
|
// Range literal: `lo..hi` / `lo...hi` and the open-ended forms `..hi`, `lo..`.
|
|
@@ -522,12 +728,13 @@ class Parser {
|
|
|
522
728
|
// than comparison / `in` (so `n in 1..5` reads as `n in (1..5)`). A leading
|
|
523
729
|
// `..`/`...` opens the low end; a trailing `..`/`...` with no following value
|
|
524
730
|
// opens the high end.
|
|
525
|
-
private parseRange(): Expr {
|
|
526
|
-
if (this.at("range")) {
|
|
731
|
+
private parseRange(left?: Expr): Expr {
|
|
732
|
+
if (left === undefined && this.at("range")) {
|
|
527
733
|
const exclusiveEnd = this.next().value === "...";
|
|
734
|
+
if (!this.canStartValue()) this.fail("a range needs at least one bound: `lo..hi`, `lo..`, or `..hi`");
|
|
528
735
|
return { kind: "range", lo: null, hi: this.parseAdd(), exclusiveEnd };
|
|
529
736
|
}
|
|
530
|
-
const lo = this.parseAdd();
|
|
737
|
+
const lo = this.parseAdd(left);
|
|
531
738
|
if (this.at("range")) {
|
|
532
739
|
const exclusiveEnd = this.next().value === "...";
|
|
533
740
|
const hi = this.canStartValue() ? this.parseAdd() : null;
|
|
@@ -549,8 +756,8 @@ class Parser {
|
|
|
549
756
|
return t.type === "op" && (t.value === "-" || t.value === "!");
|
|
550
757
|
}
|
|
551
758
|
|
|
552
|
-
private parseAdd(): Expr {
|
|
553
|
-
|
|
759
|
+
private parseAdd(left?: Expr): Expr {
|
|
760
|
+
left = this.parseMul(left);
|
|
554
761
|
while (this.peek().type === "op" && ADD_OPS.has(this.peek().value)) {
|
|
555
762
|
const op = this.next().value;
|
|
556
763
|
left = { kind: "binary", op, left, right: this.parseMul() };
|
|
@@ -558,8 +765,8 @@ class Parser {
|
|
|
558
765
|
return left;
|
|
559
766
|
}
|
|
560
767
|
|
|
561
|
-
private parseMul(): Expr {
|
|
562
|
-
|
|
768
|
+
private parseMul(left?: Expr): Expr {
|
|
769
|
+
left = this.parseUnary(left);
|
|
563
770
|
while (this.peek().type === "op" && MUL_OPS.has(this.peek().value)) {
|
|
564
771
|
const op = this.next().value;
|
|
565
772
|
left = { kind: "binary", op, left, right: this.parseUnary() };
|
|
@@ -567,14 +774,15 @@ class Parser {
|
|
|
567
774
|
return left;
|
|
568
775
|
}
|
|
569
776
|
|
|
570
|
-
private parseUnary(): Expr {
|
|
777
|
+
private parseUnary(left?: Expr): Expr {
|
|
778
|
+
if (left !== undefined) return this.parsePostfix(left);
|
|
571
779
|
if (this.atOp("!")) { this.next(); return { kind: "unary", op: "!", expr: this.parseUnary() }; }
|
|
572
780
|
if (this.atOp("-")) { this.next(); return { kind: "unary", op: "-", expr: this.parseUnary() }; }
|
|
573
781
|
return this.parsePostfix();
|
|
574
782
|
}
|
|
575
783
|
|
|
576
|
-
private parsePostfix(): Expr {
|
|
577
|
-
let expr = this.parsePrimary();
|
|
784
|
+
private parsePostfix(left?: Expr): Expr {
|
|
785
|
+
let expr = left ?? this.parsePrimary();
|
|
578
786
|
for (;;) {
|
|
579
787
|
if (this.at("dot")) {
|
|
580
788
|
this.next();
|
|
@@ -585,8 +793,8 @@ class Parser {
|
|
|
585
793
|
} else {
|
|
586
794
|
expr = { kind: "member", recv: expr, name };
|
|
587
795
|
}
|
|
588
|
-
} else if (this.at("lparen") && expr.kind === "ident") {
|
|
589
|
-
// free function call: name(args)
|
|
796
|
+
} else if (this.at("lparen") && left === undefined && expr.kind === "ident") {
|
|
797
|
+
// free function call: name(args) — a bare name read here, not `(f)(x)`
|
|
590
798
|
expr = { kind: "call", recv: null, name: expr.name, args: this.parseArgs() };
|
|
591
799
|
} else if (this.at("lbrace")) {
|
|
592
800
|
break; // a consumer block boundary — not part of a value expression
|
|
@@ -652,6 +860,19 @@ function navKey(e: Expr): string | null {
|
|
|
652
860
|
}
|
|
653
861
|
}
|
|
654
862
|
|
|
863
|
+
// `!` applied `n` times to a where node.
|
|
864
|
+
function wrapNot(w: Where, n: number): Where {
|
|
865
|
+
for (let i = 0; i < n; i++) w = { kind: "not", expr: w };
|
|
866
|
+
return w;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// A scalar where leaf. A leading `!` on the whole leaf becomes a `not` node (so
|
|
870
|
+
// `where !active` keeps its shape); inside a comparison it stays a unary `!`.
|
|
871
|
+
function scalarLeaf(e: Expr): Where {
|
|
872
|
+
if (e.kind === "unary" && e.op === "!") return { kind: "not", expr: scalarLeaf(e.expr) };
|
|
873
|
+
return { kind: "scalar", expr: e };
|
|
874
|
+
}
|
|
875
|
+
|
|
655
876
|
// The optional `limit`/`offset` fields of a Query/Subquery, present only when set
|
|
656
877
|
// (so a Query built without them is unchanged).
|
|
657
878
|
function bounds(b: { limit?: Expr | null; offset?: Expr | null }): { limit?: Expr; offset?: Expr } {
|