@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/dist/parser.js
CHANGED
|
@@ -9,12 +9,36 @@
|
|
|
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
|
import { lexTemplate, lexString } from "./lexer.js";
|
|
16
38
|
import { OqxError } from "./errors.js";
|
|
17
39
|
const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "single"]);
|
|
40
|
+
// The fixed clause order of a body (ADR-020). Each clause appears at most once.
|
|
41
|
+
const CLAUSE_ORDER = ["select", "from", "where", "follow", "order by", "limit", "offset"];
|
|
18
42
|
// Contextual clause words lexed as bare idents; an open-ended range must stop
|
|
19
43
|
// before them rather than consume them as its high bound.
|
|
20
44
|
const CLAUSE_WORDS = new Set([
|
|
@@ -26,6 +50,10 @@ const RELOPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
|
26
50
|
const CMP_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
27
51
|
const ADD_OPS = new Set(["+", "-"]);
|
|
28
52
|
const MUL_OPS = new Set(["*", "/", "%"]);
|
|
53
|
+
// `true`/`false`/`null` are literals in every position, so they can never name a
|
|
54
|
+
// receiver (`true exists { … }`, `follow null`).
|
|
55
|
+
const LITERAL_WORDS = new Set(["true", "false", "null"]);
|
|
56
|
+
const TOP_CTX = { top: true, op: null, liftsAllowed: false };
|
|
29
57
|
/** Parse a tagged-template call into a Query. */
|
|
30
58
|
export function parseTemplate(fragments, values) {
|
|
31
59
|
return new Parser(lexTemplate(fragments, values)).parseQuery();
|
|
@@ -58,7 +86,7 @@ class Parser {
|
|
|
58
86
|
// ---- top level ------------------------------------------------------------
|
|
59
87
|
parseQuery() {
|
|
60
88
|
// Directive form: `<receiver> <consumer> { … }` consuming the whole query.
|
|
61
|
-
const directive = this.tryOp();
|
|
89
|
+
const directive = this.tryOp(false);
|
|
62
90
|
if (directive && this.at("eof")) {
|
|
63
91
|
return {
|
|
64
92
|
source: directive.receiver,
|
|
@@ -75,12 +103,12 @@ class Parser {
|
|
|
75
103
|
}
|
|
76
104
|
if (directive)
|
|
77
105
|
this.fail(`unexpected ${this.tokDesc()} after the top-level directive`);
|
|
78
|
-
// Body form:
|
|
79
|
-
const body = this.parseBody(
|
|
106
|
+
// Body form: the fixed clause list; its `from` is the source.
|
|
107
|
+
const body = this.parseBody(TOP_CTX);
|
|
80
108
|
if (!this.at("eof"))
|
|
81
|
-
this.fail(`unexpected ${this.tokDesc()} after the query`);
|
|
109
|
+
this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause`);
|
|
82
110
|
if (body.froms.length === 0) {
|
|
83
|
-
this.fail("a query must
|
|
111
|
+
this.fail("a query must name its source with `from <collection>` (or be `<collection> <consumer> { … }`)");
|
|
84
112
|
}
|
|
85
113
|
return {
|
|
86
114
|
source: body.froms[0],
|
|
@@ -100,7 +128,11 @@ class Parser {
|
|
|
100
128
|
return t.type === "eof" ? "end of query" : `'${t.value || t.type}'`;
|
|
101
129
|
}
|
|
102
130
|
// ---- clause body (shared by top level and consumer blocks) ----------------
|
|
103
|
-
|
|
131
|
+
// An ordered state machine over the fixed clause sequence: `stage` is the
|
|
132
|
+
// index (in CLAUSE_ORDER) of the last clause parsed, so a clause with a lower
|
|
133
|
+
// index is out of order and an equal index is a duplicate. The only
|
|
134
|
+
// keyword-less clause is a leading projection (while `stage` is still -1).
|
|
135
|
+
parseBody(ctx) {
|
|
104
136
|
const froms = [];
|
|
105
137
|
let where = null;
|
|
106
138
|
let select = [];
|
|
@@ -110,83 +142,122 @@ class Parser {
|
|
|
110
142
|
let values = false;
|
|
111
143
|
let limit = null;
|
|
112
144
|
let offset = null;
|
|
113
|
-
let
|
|
145
|
+
let stage = -1;
|
|
146
|
+
const enter = (clause) => {
|
|
147
|
+
const idx = CLAUSE_ORDER.indexOf(clause);
|
|
148
|
+
if (idx === stage)
|
|
149
|
+
this.fail(`duplicate \`${clause}\` clause`);
|
|
150
|
+
if (idx < stage) {
|
|
151
|
+
this.fail(`\`${clause}\` must come before \`${CLAUSE_ORDER[stage]}\` — OQX clause order is ${CLAUSE_ORDER.join(", ")}`);
|
|
152
|
+
}
|
|
153
|
+
stage = idx;
|
|
154
|
+
};
|
|
114
155
|
while (!this.at("eof") && !this.at("rbrace")) {
|
|
115
|
-
if (this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
if (this.at("kw", "select")) {
|
|
157
|
+
enter("select");
|
|
158
|
+
this.next();
|
|
159
|
+
if (this.at("ident", "distinct")) {
|
|
160
|
+
this.next();
|
|
161
|
+
distinct = true;
|
|
162
|
+
}
|
|
163
|
+
({ items: select, values } = this.parseProjection(ctx));
|
|
119
164
|
continue;
|
|
120
165
|
}
|
|
121
166
|
if (this.at("kw", "from")) {
|
|
167
|
+
enter("from");
|
|
122
168
|
this.next();
|
|
123
169
|
froms.push(this.parseValueExpr());
|
|
124
170
|
continue;
|
|
125
171
|
}
|
|
126
172
|
if (this.at("kw", "where")) {
|
|
127
|
-
|
|
128
|
-
this.fail("duplicate `where` clause");
|
|
129
|
-
sawWhere = true;
|
|
173
|
+
enter("where");
|
|
130
174
|
this.next();
|
|
131
175
|
where = this.parseWhere();
|
|
132
176
|
continue;
|
|
133
177
|
}
|
|
134
|
-
if (this.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
sawSelect = true;
|
|
138
|
-
if (this.at("kw")) {
|
|
139
|
-
this.next();
|
|
140
|
-
if (this.at("ident", "distinct")) {
|
|
141
|
-
this.next();
|
|
142
|
-
distinct = true;
|
|
143
|
-
}
|
|
144
|
-
} // consume `select` + optional `distinct`; a leading `^` is part of the item
|
|
145
|
-
({ items: select, values } = this.parseProjection());
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
if (this.atBound()) {
|
|
149
|
-
const word = this.next().value;
|
|
150
|
-
const e = this.parsePostfix();
|
|
151
|
-
if (word === "limit") {
|
|
152
|
-
if (limit)
|
|
153
|
-
this.fail("duplicate `limit` clause");
|
|
154
|
-
limit = e;
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
if (offset)
|
|
158
|
-
this.fail("duplicate `offset` clause");
|
|
159
|
-
offset = e;
|
|
160
|
-
}
|
|
178
|
+
if (this.atFollow()) {
|
|
179
|
+
enter("follow");
|
|
180
|
+
follow = this.parseFollow();
|
|
161
181
|
continue;
|
|
162
182
|
}
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
this.fail("duplicate `order by` clause");
|
|
166
|
-
sawOrder = true;
|
|
183
|
+
if (this.atOrderBy()) {
|
|
184
|
+
enter("order by");
|
|
167
185
|
this.next();
|
|
168
186
|
this.next(); // `order` `by`
|
|
169
187
|
orderBy = this.parseOrderSpecs();
|
|
170
188
|
continue;
|
|
171
189
|
}
|
|
172
|
-
if (this.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
if (this.atBound()) {
|
|
191
|
+
const word = this.peek().value;
|
|
192
|
+
enter(word);
|
|
193
|
+
this.next();
|
|
194
|
+
// A top-level bound is evaluated at the root scope itself, so `^n` there
|
|
195
|
+
// has nothing to read: say so now instead of an "absent" error at eval.
|
|
196
|
+
if (ctx.top && this.at("caret")) {
|
|
197
|
+
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`);
|
|
198
|
+
}
|
|
199
|
+
const e = this.parsePostfix();
|
|
200
|
+
if (word === "limit")
|
|
201
|
+
limit = e;
|
|
202
|
+
else
|
|
203
|
+
offset = e;
|
|
177
204
|
continue;
|
|
178
205
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
(
|
|
206
|
+
// A keyword-less run. Before any clause it is the projection (`select` is
|
|
207
|
+
// the one keyword that may be dropped, and only in first position); after
|
|
208
|
+
// any clause it is an error — a predicate is never implicit.
|
|
209
|
+
if (stage === -1 && (this.at("ident") || this.at("binding") || this.at("caret") || this.canStartValue())) {
|
|
210
|
+
enter("select");
|
|
211
|
+
({ items: select, values } = this.parseProjection(ctx));
|
|
184
212
|
continue;
|
|
185
213
|
}
|
|
186
|
-
this.
|
|
214
|
+
this.failUnexpectedInBody(stage, ctx);
|
|
187
215
|
}
|
|
216
|
+
where = this.inlineAliases(select, where);
|
|
188
217
|
return { froms, where, select, orderBy, follow, distinct, values, limit, offset };
|
|
189
218
|
}
|
|
219
|
+
// The error for a token that starts no clause, phrased for the mistake it most
|
|
220
|
+
// likely is: a bare run right after `from` (the old implicit `where`, or a
|
|
221
|
+
// consumer word where a whole-query directive was meant), or a stray token.
|
|
222
|
+
failUnexpectedInBody(stage, ctx) {
|
|
223
|
+
const t = this.peek();
|
|
224
|
+
const remaining = CLAUSE_ORDER.slice(stage + 1).join("/");
|
|
225
|
+
const last = stage >= 0 ? CLAUSE_ORDER[stage] : null;
|
|
226
|
+
const isWord = t.type === "ident" || t.type === "kw" || t.type === "binding" || t.type === "caret";
|
|
227
|
+
// Punctuation, an operator, or a literal after a complete clause is a stray
|
|
228
|
+
// token, not a misplaced predicate: name where the body ends. (A comma keeps
|
|
229
|
+
// the projection hint below — `name from r, id` almost always meant a
|
|
230
|
+
// projection.)
|
|
231
|
+
if (last !== null && !isWord && t.type !== "comma") {
|
|
232
|
+
if (ctx.top)
|
|
233
|
+
this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause (expected ${remaining} or the end of the query)`);
|
|
234
|
+
this.fail(`unexpected ${this.tokDesc()} in the ${ctx.op} { … } block — expected ${remaining} or '}' to close the block`);
|
|
235
|
+
}
|
|
236
|
+
// Clause words that did not form a clause: say what the clause needs.
|
|
237
|
+
if (last !== null && t.type === "ident") {
|
|
238
|
+
if (t.value === "order")
|
|
239
|
+
this.fail(`unexpected 'order' after \`${last}\` — an ordering is written \`order by <expr> [asc|desc]\``);
|
|
240
|
+
if (t.value === "follow")
|
|
241
|
+
this.fail(`unexpected 'follow' after \`${last}\` — \`follow\` needs a relation: \`follow <relation>\` or \`follow distinct <relation>\``);
|
|
242
|
+
if (t.value === "limit" || t.value === "offset") {
|
|
243
|
+
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\``);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (last === "from" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
247
|
+
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\``);
|
|
248
|
+
}
|
|
249
|
+
if (last === "from") {
|
|
250
|
+
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}`);
|
|
251
|
+
}
|
|
252
|
+
// `{ rel exists { … } }` / `{ rel count { … } >= 2 }`: the leading `rel` was
|
|
253
|
+
// read as the projection, so the consumer word is where the mistake shows.
|
|
254
|
+
if (last === "select" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
255
|
+
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 { … }\`)`);
|
|
256
|
+
}
|
|
257
|
+
if (last === null)
|
|
258
|
+
this.fail(`unexpected ${this.tokDesc()} — expected a projection or ${remaining}`);
|
|
259
|
+
this.fail(`unexpected ${this.tokDesc()} after \`${last}\` — expected ${remaining}`);
|
|
260
|
+
}
|
|
190
261
|
// `limit <n>` / `offset <n>` — the word must be followed by something that can
|
|
191
262
|
// be a bound (a number, a binding, or an outer reference), so a field that
|
|
192
263
|
// happens to be called `limit` still projects/filters as a bare name.
|
|
@@ -197,79 +268,113 @@ class Parser {
|
|
|
197
268
|
const nx = this.peekAt(1);
|
|
198
269
|
return !!nx && (nx.type === "number" || nx.type === "binding" || nx.type === "caret");
|
|
199
270
|
}
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
271
|
+
// ---- alias inlining -------------------------------------------------------
|
|
272
|
+
// `where` may reference the same body's `select` aliases. This is a
|
|
273
|
+
// compile-time rewrite, not a second execution pass: every bare identifier in
|
|
274
|
+
// the where tree that names an alias is replaced by that alias's expression,
|
|
275
|
+
// so the engine and any pushdown planner see an ordinary where over row
|
|
276
|
+
// fields. Rules:
|
|
277
|
+
// • an alias shadows a same-named row field inside `where`;
|
|
278
|
+
// • an alias's own name inside its own expression is the row field
|
|
279
|
+
// (`name: name.upper()` is not recursive), but a chain of aliases that
|
|
280
|
+
// comes back to one being resolved (`a: b, b: a`) is a cycle → error;
|
|
281
|
+
// • an alias whose value is a `collect`/`first`/`single { … }` block may
|
|
282
|
+
// stand alone as a where leaf (a collection in predicate position means
|
|
283
|
+
// non-empty) but not appear inside an expression;
|
|
284
|
+
// • nested blocks (consumer bodies, follow blocks) are their own scopes and
|
|
285
|
+
// are not rewritten against this body's select — each body rewrites
|
|
286
|
+
// against its own.
|
|
287
|
+
inlineAliases(select, where) {
|
|
288
|
+
if (!where || select.length === 0)
|
|
289
|
+
return where;
|
|
290
|
+
const aliases = new Map();
|
|
291
|
+
for (const it of select) {
|
|
292
|
+
if (it.kind === "collect")
|
|
293
|
+
aliases.set(it.name, it);
|
|
294
|
+
else if (it.lift === 0 && it.name !== "")
|
|
295
|
+
aliases.set(it.name, it);
|
|
296
|
+
}
|
|
297
|
+
if (aliases.size === 0)
|
|
298
|
+
return where;
|
|
299
|
+
const resolving = [];
|
|
300
|
+
const subst = (e) => {
|
|
301
|
+
switch (e.kind) {
|
|
302
|
+
case "ident": {
|
|
303
|
+
const a = aliases.get(e.name);
|
|
304
|
+
if (!a)
|
|
305
|
+
return e;
|
|
306
|
+
if (resolving[resolving.length - 1] === e.name)
|
|
307
|
+
return e; // its own name inside its own expression: the row field
|
|
308
|
+
if (resolving.includes(e.name)) {
|
|
309
|
+
const cycle = [...resolving.slice(resolving.indexOf(e.name)), e.name].join(" → ");
|
|
310
|
+
this.fail(`select aliases form a cycle: ${cycle} — an alias used in \`where\` cannot depend on itself`);
|
|
311
|
+
}
|
|
312
|
+
if (a.kind === "collect") {
|
|
313
|
+
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`);
|
|
314
|
+
}
|
|
315
|
+
resolving.push(e.name);
|
|
316
|
+
const out = subst(a.expr);
|
|
317
|
+
resolving.pop();
|
|
318
|
+
return out;
|
|
241
319
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
320
|
+
case "member": return { ...e, recv: subst(e.recv) };
|
|
321
|
+
case "index": return { ...e, recv: subst(e.recv), index: subst(e.index) };
|
|
322
|
+
case "call": return { ...e, recv: e.recv ? subst(e.recv) : null, args: e.args.map(subst) };
|
|
323
|
+
case "unary": return { ...e, expr: subst(e.expr) };
|
|
324
|
+
case "binary":
|
|
325
|
+
case "logical":
|
|
326
|
+
case "in": return { ...e, left: subst(e.left), right: subst(e.right) };
|
|
327
|
+
case "range": return { ...e, lo: e.lo ? subst(e.lo) : null, hi: e.hi ? subst(e.hi) : null };
|
|
328
|
+
default: return e; // lit, binding, outer (`^name` reads an enclosing row, never an alias)
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const walk = (w) => {
|
|
332
|
+
switch (w.kind) {
|
|
333
|
+
case "and": return { kind: "and", parts: w.parts.map(walk) };
|
|
334
|
+
case "or": return { kind: "or", parts: w.parts.map(walk) };
|
|
335
|
+
case "not": return { kind: "not", expr: walk(w.expr) };
|
|
336
|
+
case "scalar": {
|
|
337
|
+
if (w.expr.kind === "ident") {
|
|
338
|
+
const a = aliases.get(w.expr.name);
|
|
339
|
+
if (a?.kind === "collect")
|
|
340
|
+
return a.op; // a collection in predicate position: non-empty
|
|
341
|
+
}
|
|
342
|
+
return { kind: "scalar", expr: subst(w.expr) };
|
|
246
343
|
}
|
|
344
|
+
case "op": return { ...w, receiver: subst(w.receiver) }; // the receiver is read in this scope; the block is its own scope
|
|
247
345
|
}
|
|
248
|
-
}
|
|
249
|
-
return
|
|
346
|
+
};
|
|
347
|
+
return walk(where);
|
|
250
348
|
}
|
|
251
349
|
atOrderBy() {
|
|
252
350
|
const t = this.peek();
|
|
253
351
|
const nx = this.peekAt(1);
|
|
254
352
|
return t.type === "ident" && t.value === "order" && !!nx && nx.type === "ident" && nx.value === "by";
|
|
255
353
|
}
|
|
354
|
+
// `follow` is a clause when a relation (or something that tries to be one —
|
|
355
|
+
// `distinct`, an outer reference) follows; otherwise it is a field name.
|
|
256
356
|
atFollow() {
|
|
257
357
|
if (!this.at("ident", "follow"))
|
|
258
358
|
return false;
|
|
259
359
|
const nx = this.peekAt(1);
|
|
260
|
-
return !!nx && (nx.type === "ident" || nx.type === "binding");
|
|
360
|
+
return !!nx && (nx.type === "ident" || nx.type === "binding" || nx.type === "caret");
|
|
261
361
|
}
|
|
262
362
|
// ---- follow ---------------------------------------------------------------
|
|
263
363
|
parseFollow() {
|
|
264
364
|
this.next(); // `follow`
|
|
265
365
|
let distinct = false;
|
|
366
|
+
// After `follow`, `distinct` is a keyword (a relation literally named
|
|
367
|
+
// `distinct` is not supported); it must be followed by the relation.
|
|
266
368
|
if (this.at("ident", "distinct")) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
distinct
|
|
369
|
+
this.next();
|
|
370
|
+
distinct = true;
|
|
371
|
+
if (!this.at("ident") && !this.at("binding") && !this.at("caret")) {
|
|
372
|
+
this.fail("expected a relation after `follow distinct` (`follow distinct <relation>`)");
|
|
271
373
|
}
|
|
272
374
|
}
|
|
375
|
+
if (this.at("caret")) {
|
|
376
|
+
this.fail("`follow` takes a relation of the current row (`follow <relation>`); an outer reference `^name` is not allowed there");
|
|
377
|
+
}
|
|
273
378
|
const receiver = this.parseReceiver();
|
|
274
379
|
const follow = { receiver, distinct, where: null, frontier: null, depth: null, by: null };
|
|
275
380
|
if (!this.at("lbrace"))
|
|
@@ -325,6 +430,8 @@ class Parser {
|
|
|
325
430
|
const levels = this.parseCarets();
|
|
326
431
|
if (!this.at("ident"))
|
|
327
432
|
this.fail("expected a collection navigation (a property/relation name)");
|
|
433
|
+
if (LITERAL_WORDS.has(this.peek().value))
|
|
434
|
+
this.fail(`\`${this.peek().value}\` is a literal, not a collection`);
|
|
328
435
|
return this.parseNavFrom(this.next(), levels).expr;
|
|
329
436
|
}
|
|
330
437
|
// Consume a run of `^` and return its length (0 when there is none).
|
|
@@ -347,7 +454,7 @@ class Parser {
|
|
|
347
454
|
while (this.at("dot")) {
|
|
348
455
|
this.next();
|
|
349
456
|
if (!this.at("ident"))
|
|
350
|
-
this.fail("expected
|
|
457
|
+
this.fail("expected a property name after '.'");
|
|
351
458
|
name = this.next().value;
|
|
352
459
|
expr = { kind: "member", recv: expr, name };
|
|
353
460
|
}
|
|
@@ -360,11 +467,11 @@ class Parser {
|
|
|
360
467
|
// meaningless. Without `values`, every item needs a key — a bare/dotted
|
|
361
468
|
// navigation supplies its own (the last segment); any other expression must be
|
|
362
469
|
// aliased (`name: expr`).
|
|
363
|
-
parseProjection() {
|
|
364
|
-
const items = [this.parseSelectItem()];
|
|
470
|
+
parseProjection(ctx) {
|
|
471
|
+
const items = [this.parseSelectItem(ctx)];
|
|
365
472
|
while (this.at("comma")) {
|
|
366
473
|
this.next();
|
|
367
|
-
items.push(this.parseSelectItem());
|
|
474
|
+
items.push(this.parseSelectItem(ctx));
|
|
368
475
|
}
|
|
369
476
|
let values = false;
|
|
370
477
|
if (this.at("ident", "values")) {
|
|
@@ -377,20 +484,38 @@ class Parser {
|
|
|
377
484
|
this.fail("a lift (^name: …) cannot be combined with `values`");
|
|
378
485
|
}
|
|
379
486
|
else {
|
|
487
|
+
// Every item needs a distinct key: two items with one name would silently
|
|
488
|
+
// overwrite each other in the record. Lifts are keyed per scope (`^x` and
|
|
489
|
+
// `^^x` bind different rows), so the lift depth is part of the key.
|
|
490
|
+
const seen = new Set();
|
|
380
491
|
for (const it of items) {
|
|
381
492
|
if (it.name === "")
|
|
382
|
-
this.fail("a projection item that is not a plain name needs an alias (`name: expr`)
|
|
493
|
+
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");
|
|
494
|
+
const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
|
|
495
|
+
if (seen.has(key))
|
|
496
|
+
this.fail(`duplicate projection name '${it.name}' — each projected item needs its own name (alias one: \`other: expr\`)`);
|
|
497
|
+
seen.add(key);
|
|
383
498
|
}
|
|
384
499
|
}
|
|
385
500
|
return { items, values };
|
|
386
501
|
}
|
|
387
|
-
parseSelectItem() {
|
|
388
|
-
// Leading `^`s mark a lift; the count is how many scopes out it binds.
|
|
502
|
+
parseSelectItem(ctx) {
|
|
503
|
+
// Leading `^`s mark a lift; the count is how many scopes out it binds. A
|
|
504
|
+
// lift is bound by a `collect { … }` in where position and nowhere else — at
|
|
505
|
+
// the top level, in a select-position block, or in an exists/none/count
|
|
506
|
+
// block it would silently do nothing (or act as a plain field), so it is an
|
|
507
|
+
// error there.
|
|
389
508
|
const lift = this.parseCarets();
|
|
509
|
+
if (!this.at("ident") && !this.canStartValue())
|
|
510
|
+
this.fail("expected a projection name");
|
|
511
|
+
if (lift > 0 && !ctx.liftsAllowed) {
|
|
512
|
+
const what = this.at("ident") ? `^${this.peek().value}` : "^name";
|
|
513
|
+
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}: … }\`)`);
|
|
514
|
+
}
|
|
390
515
|
if (this.at("ident") && this.peekAt(1)?.type === "colon") {
|
|
391
516
|
const name = this.next().value;
|
|
392
517
|
this.next(); // ':'
|
|
393
|
-
const op = this.tryOp();
|
|
518
|
+
const op = this.tryOp(false);
|
|
394
519
|
if (op) {
|
|
395
520
|
if (op.op !== "collect" && op.op !== "first" && op.op !== "single") {
|
|
396
521
|
this.fail(`projection '${name}' must use collect/first/single, not ${op.op} (exists/none/count are where-position tests)`);
|
|
@@ -402,8 +527,6 @@ class Parser {
|
|
|
402
527
|
const expr = this.parseValueExpr();
|
|
403
528
|
return { kind: "field", name, expr, lift };
|
|
404
529
|
}
|
|
405
|
-
if (!this.at("ident") && !this.canStartValue())
|
|
406
|
-
this.fail("expected a projection name");
|
|
407
530
|
// Unaliased item: a bare/dotted navigation keys by its last segment; any
|
|
408
531
|
// other expression is unnamed ("") — legal only under `values` (checked by
|
|
409
532
|
// parseProjection, which sees the whole list).
|
|
@@ -430,7 +553,10 @@ class Parser {
|
|
|
430
553
|
}
|
|
431
554
|
return { expr, desc };
|
|
432
555
|
}
|
|
433
|
-
// ---- where boolean tree: or → and →
|
|
556
|
+
// ---- where boolean tree: or → and → primary --------------------------------
|
|
557
|
+
// `!` is handled in `parseWherePrimary`: it applies to the operand right after
|
|
558
|
+
// it (a consumer test, a parenthesized group, or a scalar primary), never to a
|
|
559
|
+
// whole comparison — `!a == b` is `(!a) == b`, exactly as in value position.
|
|
434
560
|
parseWhere() { return this.parseWhereOr(); }
|
|
435
561
|
parseWhereOr() {
|
|
436
562
|
const left = this.parseWhereAnd();
|
|
@@ -444,37 +570,78 @@ class Parser {
|
|
|
444
570
|
return { kind: "or", parts };
|
|
445
571
|
}
|
|
446
572
|
parseWhereAnd() {
|
|
447
|
-
const left = this.
|
|
573
|
+
const left = this.parseWherePrimary();
|
|
448
574
|
if (!this.atOp("&&"))
|
|
449
575
|
return left;
|
|
450
576
|
const parts = [left];
|
|
451
577
|
while (this.atOp("&&")) {
|
|
452
578
|
this.next();
|
|
453
|
-
parts.push(this.
|
|
579
|
+
parts.push(this.parseWherePrimary());
|
|
454
580
|
}
|
|
455
581
|
return { kind: "and", parts };
|
|
456
582
|
}
|
|
457
|
-
|
|
458
|
-
|
|
583
|
+
// A where operand: `[!…] ( where )`, `[!…] <receiver> <consumer> { … }`, or a
|
|
584
|
+
// scalar leaf (a cmp-level expression, which handles its own `!`).
|
|
585
|
+
//
|
|
586
|
+
// Parentheses group EITHER a predicate or a scalar, decided by what follows
|
|
587
|
+
// the `)`: a comparison, arithmetic, `in`, a range, or `.`-navigation means
|
|
588
|
+
// the group is a scalar operand (`(a + 1) > 2`, `(a || b) == 5`,
|
|
589
|
+
// `(x).size() > 1`); anything else means it is a predicate group
|
|
590
|
+
// (`(a > 1) && b`). A group that contains a consumer test can only be a
|
|
591
|
+
// predicate.
|
|
592
|
+
parseWherePrimary() {
|
|
593
|
+
const start = this.pos;
|
|
594
|
+
let nots = 0;
|
|
595
|
+
while (this.atOp("!")) {
|
|
459
596
|
this.next();
|
|
460
|
-
|
|
597
|
+
nots++;
|
|
461
598
|
}
|
|
462
|
-
return this.parseWherePrimary();
|
|
463
|
-
}
|
|
464
|
-
parseWherePrimary() {
|
|
465
599
|
if (this.at("lparen")) {
|
|
466
600
|
this.next();
|
|
467
|
-
const
|
|
601
|
+
const inner = this.parseWhere();
|
|
468
602
|
if (!this.at("rparen"))
|
|
469
603
|
this.fail("expected ')' to close a grouped where expression");
|
|
470
604
|
this.next();
|
|
471
|
-
|
|
605
|
+
if (this.atScalarContinuation()) {
|
|
606
|
+
let e = this.parsePostfix(this.whereToExpr(inner));
|
|
607
|
+
for (let i = 0; i < nots; i++)
|
|
608
|
+
e = { kind: "unary", op: "!", expr: e };
|
|
609
|
+
return { kind: "scalar", expr: this.parseCmp(e) };
|
|
610
|
+
}
|
|
611
|
+
return wrapNot(inner, nots);
|
|
472
612
|
}
|
|
473
|
-
const op = this.tryOp();
|
|
613
|
+
const op = this.tryOp(true);
|
|
474
614
|
if (op)
|
|
475
|
-
return this.finishWhereOp(op);
|
|
476
|
-
|
|
477
|
-
|
|
615
|
+
return wrapNot(this.finishWhereOp(op), nots);
|
|
616
|
+
// A scalar leaf, re-read from the first `!` so the scalar grammar gives `!`
|
|
617
|
+
// its one precedence (tighter than comparison). A leaf whose whole value is
|
|
618
|
+
// a negation keeps the `not` node shape (`where !active`).
|
|
619
|
+
this.pos = start;
|
|
620
|
+
return scalarLeaf(this.parseCmp());
|
|
621
|
+
}
|
|
622
|
+
// Whether the token after a `)` continues a scalar expression.
|
|
623
|
+
atScalarContinuation() {
|
|
624
|
+
const t = this.peek();
|
|
625
|
+
if (t.type === "op")
|
|
626
|
+
return CMP_OPS.has(t.value) || ADD_OPS.has(t.value) || MUL_OPS.has(t.value);
|
|
627
|
+
return t.type === "dot" || t.type === "range" || (t.type === "ident" && t.value === "in");
|
|
628
|
+
}
|
|
629
|
+
// A parenthesized where-group that turned out to be a scalar operand, as an
|
|
630
|
+
// expression. A consumer test has no scalar value, so it cannot be operated on.
|
|
631
|
+
whereToExpr(w) {
|
|
632
|
+
switch (w.kind) {
|
|
633
|
+
case "scalar": return w.expr;
|
|
634
|
+
case "not": return { kind: "unary", op: "!", expr: this.whereToExpr(w.expr) };
|
|
635
|
+
case "and":
|
|
636
|
+
case "or": {
|
|
637
|
+
const op = w.kind === "and" ? "&&" : "||";
|
|
638
|
+
return w.parts.map((p) => this.whereToExpr(p)).reduce((l, r) => ({ kind: "logical", op, left: l, right: r }));
|
|
639
|
+
}
|
|
640
|
+
case "op": {
|
|
641
|
+
const hint = w.op === "count" ? ` — write \`<relation> count { … } ${this.peek().value} N\` without the parentheses` : "";
|
|
642
|
+
this.fail(`a consumer test (${w.op} { … }) is a predicate, not a value, so it cannot be compared or operated on${hint}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
478
645
|
}
|
|
479
646
|
// Validate a consumer op used in where position and attach any `count { … } <op> N`.
|
|
480
647
|
finishWhereOp(op) {
|
|
@@ -501,7 +668,7 @@ class Parser {
|
|
|
501
668
|
}
|
|
502
669
|
// Detect + parse a postfix consumer op `<receiver> <consumer> { <sub> }`.
|
|
503
670
|
// Returns null (rewinding) when the lookahead is not a consumer op.
|
|
504
|
-
tryOp() {
|
|
671
|
+
tryOp(inWhere) {
|
|
505
672
|
const start = this.pos;
|
|
506
673
|
let receiver;
|
|
507
674
|
if (this.at("binding"))
|
|
@@ -522,6 +689,8 @@ class Parser {
|
|
|
522
689
|
const opThenBrace = after?.type === "lbrace";
|
|
523
690
|
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
524
691
|
if (opThenBrace || opDistinctBrace) {
|
|
692
|
+
if (receiver.kind === "ident" && LITERAL_WORDS.has(receiver.name))
|
|
693
|
+
this.fail(`\`${receiver.name}\` is a literal, not a collection`);
|
|
525
694
|
const op = this.next().value;
|
|
526
695
|
let distinct = false;
|
|
527
696
|
if (this.at("ident", "distinct")) {
|
|
@@ -529,7 +698,7 @@ class Parser {
|
|
|
529
698
|
distinct = true;
|
|
530
699
|
}
|
|
531
700
|
this.next(); // '{'
|
|
532
|
-
const { sub, distinct: bodyDistinct } = this.parseSubquery();
|
|
701
|
+
const { sub, distinct: bodyDistinct } = this.parseSubquery({ top: false, op, liftsAllowed: inWhere && op === "collect" });
|
|
533
702
|
if (!this.at("rbrace"))
|
|
534
703
|
this.fail(`expected '}' to close the ${op} { … } block`);
|
|
535
704
|
this.next();
|
|
@@ -539,8 +708,8 @@ class Parser {
|
|
|
539
708
|
this.pos = start;
|
|
540
709
|
return null;
|
|
541
710
|
}
|
|
542
|
-
parseSubquery() {
|
|
543
|
-
const body = this.parseBody(
|
|
711
|
+
parseSubquery(ctx) {
|
|
712
|
+
const body = this.parseBody(ctx);
|
|
544
713
|
return {
|
|
545
714
|
sub: { from: body.froms, where: body.where, select: body.select, orderBy: body.orderBy, follow: body.follow, values: body.values, ...bounds(body) },
|
|
546
715
|
distinct: body.distinct,
|
|
@@ -566,30 +735,39 @@ class Parser {
|
|
|
566
735
|
return left;
|
|
567
736
|
}
|
|
568
737
|
// Comparison / membership (also the entry point for a where scalar leaf, so a
|
|
569
|
-
// where leaf never swallows the where-tree's && / ||).
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
738
|
+
// where leaf never swallows the where-tree's && / ||). Non-associative: a
|
|
739
|
+
// second comparison in a row is an error, not a silent stray token.
|
|
740
|
+
//
|
|
741
|
+
// Each level from here down takes an optional already-parsed `left` operand,
|
|
742
|
+
// so a parenthesized where-group promoted to a scalar continues into the
|
|
743
|
+
// operator tail without re-reading its tokens.
|
|
744
|
+
parseCmp(left) {
|
|
745
|
+
const lhs = this.parseRange(left);
|
|
746
|
+
const t = this.peek();
|
|
747
|
+
const isCmp = () => (this.peek().type === "op" && CMP_OPS.has(this.peek().value)) || this.at("ident", "in");
|
|
748
|
+
if (!isCmp())
|
|
749
|
+
return lhs;
|
|
750
|
+
const op = this.next().value;
|
|
751
|
+
const rhs = this.parseRange();
|
|
752
|
+
const result = op === "in" ? { kind: "in", left: lhs, right: rhs } : { kind: "binary", op, left: lhs, right: rhs };
|
|
753
|
+
if (isCmp()) {
|
|
754
|
+
this.fail(`comparisons do not chain: \`a ${t.value} b ${this.peek().value} c\` — write two comparisons joined with \`&&\``);
|
|
579
755
|
}
|
|
580
|
-
return
|
|
756
|
+
return result;
|
|
581
757
|
}
|
|
582
758
|
// Range literal: `lo..hi` / `lo...hi` and the open-ended forms `..hi`, `lo..`.
|
|
583
759
|
// Binds looser than arithmetic (so `1+1..2*3` is the range 2..6) but tighter
|
|
584
760
|
// than comparison / `in` (so `n in 1..5` reads as `n in (1..5)`). A leading
|
|
585
761
|
// `..`/`...` opens the low end; a trailing `..`/`...` with no following value
|
|
586
762
|
// opens the high end.
|
|
587
|
-
parseRange() {
|
|
588
|
-
if (this.at("range")) {
|
|
763
|
+
parseRange(left) {
|
|
764
|
+
if (left === undefined && this.at("range")) {
|
|
589
765
|
const exclusiveEnd = this.next().value === "...";
|
|
766
|
+
if (!this.canStartValue())
|
|
767
|
+
this.fail("a range needs at least one bound: `lo..hi`, `lo..`, or `..hi`");
|
|
590
768
|
return { kind: "range", lo: null, hi: this.parseAdd(), exclusiveEnd };
|
|
591
769
|
}
|
|
592
|
-
const lo = this.parseAdd();
|
|
770
|
+
const lo = this.parseAdd(left);
|
|
593
771
|
if (this.at("range")) {
|
|
594
772
|
const exclusiveEnd = this.next().value === "...";
|
|
595
773
|
const hi = this.canStartValue() ? this.parseAdd() : null;
|
|
@@ -611,23 +789,25 @@ class Parser {
|
|
|
611
789
|
return true;
|
|
612
790
|
return t.type === "op" && (t.value === "-" || t.value === "!");
|
|
613
791
|
}
|
|
614
|
-
parseAdd() {
|
|
615
|
-
|
|
792
|
+
parseAdd(left) {
|
|
793
|
+
left = this.parseMul(left);
|
|
616
794
|
while (this.peek().type === "op" && ADD_OPS.has(this.peek().value)) {
|
|
617
795
|
const op = this.next().value;
|
|
618
796
|
left = { kind: "binary", op, left, right: this.parseMul() };
|
|
619
797
|
}
|
|
620
798
|
return left;
|
|
621
799
|
}
|
|
622
|
-
parseMul() {
|
|
623
|
-
|
|
800
|
+
parseMul(left) {
|
|
801
|
+
left = this.parseUnary(left);
|
|
624
802
|
while (this.peek().type === "op" && MUL_OPS.has(this.peek().value)) {
|
|
625
803
|
const op = this.next().value;
|
|
626
804
|
left = { kind: "binary", op, left, right: this.parseUnary() };
|
|
627
805
|
}
|
|
628
806
|
return left;
|
|
629
807
|
}
|
|
630
|
-
parseUnary() {
|
|
808
|
+
parseUnary(left) {
|
|
809
|
+
if (left !== undefined)
|
|
810
|
+
return this.parsePostfix(left);
|
|
631
811
|
if (this.atOp("!")) {
|
|
632
812
|
this.next();
|
|
633
813
|
return { kind: "unary", op: "!", expr: this.parseUnary() };
|
|
@@ -638,8 +818,8 @@ class Parser {
|
|
|
638
818
|
}
|
|
639
819
|
return this.parsePostfix();
|
|
640
820
|
}
|
|
641
|
-
parsePostfix() {
|
|
642
|
-
let expr = this.parsePrimary();
|
|
821
|
+
parsePostfix(left) {
|
|
822
|
+
let expr = left ?? this.parsePrimary();
|
|
643
823
|
for (;;) {
|
|
644
824
|
if (this.at("dot")) {
|
|
645
825
|
this.next();
|
|
@@ -653,8 +833,8 @@ class Parser {
|
|
|
653
833
|
expr = { kind: "member", recv: expr, name };
|
|
654
834
|
}
|
|
655
835
|
}
|
|
656
|
-
else if (this.at("lparen") && expr.kind === "ident") {
|
|
657
|
-
// free function call: name(args)
|
|
836
|
+
else if (this.at("lparen") && left === undefined && expr.kind === "ident") {
|
|
837
|
+
// free function call: name(args) — a bare name read here, not `(f)(x)`
|
|
658
838
|
expr = { kind: "call", recv: null, name: expr.name, args: this.parseArgs() };
|
|
659
839
|
}
|
|
660
840
|
else if (this.at("lbrace")) {
|
|
@@ -736,6 +916,19 @@ function navKey(e) {
|
|
|
736
916
|
default: return null;
|
|
737
917
|
}
|
|
738
918
|
}
|
|
919
|
+
// `!` applied `n` times to a where node.
|
|
920
|
+
function wrapNot(w, n) {
|
|
921
|
+
for (let i = 0; i < n; i++)
|
|
922
|
+
w = { kind: "not", expr: w };
|
|
923
|
+
return w;
|
|
924
|
+
}
|
|
925
|
+
// A scalar where leaf. A leading `!` on the whole leaf becomes a `not` node (so
|
|
926
|
+
// `where !active` keeps its shape); inside a comparison it stays a unary `!`.
|
|
927
|
+
function scalarLeaf(e) {
|
|
928
|
+
if (e.kind === "unary" && e.op === "!")
|
|
929
|
+
return { kind: "not", expr: scalarLeaf(e.expr) };
|
|
930
|
+
return { kind: "scalar", expr: e };
|
|
931
|
+
}
|
|
739
932
|
// The optional `limit`/`offset` fields of a Query/Subquery, present only when set
|
|
740
933
|
// (so a Query built without them is unchanged).
|
|
741
934
|
function bounds(b) {
|