@omgbase/oqx 0.25.0 → 0.26.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 +7 -0
- package/README.md +243 -207
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +38 -35
- package/dist/engine.js.map +1 -1
- package/dist/optimize/analysis.d.ts +3 -2
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +3 -2
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +133 -32
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts +2 -2
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +79 -25
- package/dist/print.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/engine.ts +32 -28
- package/src/optimize/analysis.ts +3 -2
- package/src/parser.ts +121 -30
- package/src/print.ts +81 -28
- package/src/version.ts +1 -1
package/dist/parser.js
CHANGED
|
@@ -24,17 +24,27 @@
|
|
|
24
24
|
// that destination's block, whose body admits two clauses no other block has —
|
|
25
25
|
// `frontier P` and `depth n`, in the clause order after `where`.
|
|
26
26
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// A query names its source BEFORE its body (0.26, the collection-scoped form):
|
|
28
|
+
// `<collection> [<consumer>] [distinct] { body }` — the receiver supplies the
|
|
29
|
+
// rows, so the body never names its source and its `from` is the re-projection
|
|
30
|
+
// (a flatMap). The clause form `select … from S …` is still read as the same
|
|
31
|
+
// tree (its top-level body needs `from`); the printer never writes it. Only
|
|
30
32
|
// `select` may drop its keyword, and only when it is the first clause written
|
|
31
|
-
// (`name, age
|
|
32
|
-
//
|
|
33
|
-
// is syntactically a predicate — not a bare name, a
|
|
34
|
-
// opens a `where`-first body (`people
|
|
35
|
-
//
|
|
33
|
+
// (`people { name, age }`); every other clause always carries its keyword. A
|
|
34
|
+
// body with a receiver may instead lead with a predicate (since 0.17): a
|
|
35
|
+
// leading expression that is syntactically a predicate — not a bare name, a
|
|
36
|
+
// dotted navigation or a lift — opens a `where`-first body (`people { age > 50
|
|
37
|
+
// }`, `people exists { age > 50 }`); at the clause form's top level a leading
|
|
38
|
+
// predicate is still an error, since `where` would precede `from`. An
|
|
36
39
|
// out-of-order clause is a parse error naming the order.
|
|
37
40
|
//
|
|
41
|
+
// Two 0.26 shapes desugar HERE as well: a consumer word with no block is the
|
|
42
|
+
// directive over the empty block (`jobs first` ≡ `jobs first { }`, `jobs count
|
|
43
|
+
// > 1`, `_nodes exists`; a consumer word right after a complete receiver can be
|
|
44
|
+
// nothing else), and a projection-only block over a `collect`/`first`/`single`
|
|
45
|
+
// directive whose block projects nothing is that directive's projection
|
|
46
|
+
// (`jobs[0] { name }` ≡ `jobs first { name offset 0 }`, `mergeProjectionBlock`).
|
|
47
|
+
//
|
|
38
48
|
// Sugar (0.17) desugars HERE, to existing nodes: `x { … }` is `x collect { … }`
|
|
39
49
|
// wherever a value stands (a select item, a where leaf, a follow destination, a
|
|
40
50
|
// call argument — since 0.20 without exception) and `x distinct { … }` is
|
|
@@ -81,6 +91,7 @@ import { lexTemplate, lexString } from "./lexer.js";
|
|
|
81
91
|
import { OqxError } from "./errors.js";
|
|
82
92
|
import { AGGREGATE_NAMES, isAggregateName } from "./ast.js";
|
|
83
93
|
import { resolveSelect, resolveWhere } from "./resolve.js";
|
|
94
|
+
import { visit } from "./walk.js";
|
|
84
95
|
import { printTemplate } from "./print.js";
|
|
85
96
|
const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "single"]);
|
|
86
97
|
// The fixed clause order of a body (ADR-020). Each clause appears at most once.
|
|
@@ -128,6 +139,29 @@ export function parseTemplate(fragments, values) {
|
|
|
128
139
|
export function parseString(src) {
|
|
129
140
|
return new Parser(lexString(src)).parseQuery();
|
|
130
141
|
}
|
|
142
|
+
/** The clauses of an omitted block (`jobs first` — the empty block `{ }`). */
|
|
143
|
+
function emptyBody() {
|
|
144
|
+
return { froms: [], where: null, select: [], orderBy: null, follow: null, frontier: null, depth: null, flatten: [], groupBy: null, having: null, distinct: false, values: false, limit: null, offset: null };
|
|
145
|
+
}
|
|
146
|
+
/** The names a projection would shadow in a `where` of the same body: every
|
|
147
|
+
* item's name except a plain self-projection (`pay: pay`, `pay`), which
|
|
148
|
+
* substitutes a name for itself. */
|
|
149
|
+
function shadowingNames(items) {
|
|
150
|
+
return items
|
|
151
|
+
.filter((it) => it.name !== "" && !(it.kind === "field" && it.lift === 0 && it.expr.kind === "ident" && it.expr.name === it.name))
|
|
152
|
+
.map((it) => it.name);
|
|
153
|
+
}
|
|
154
|
+
/** Whether a bare identifier anywhere under `w` has one of `names` (the alias
|
|
155
|
+
* guard of the projection-block merge; conservative: nested blocks included). */
|
|
156
|
+
function readsAnyOf(w, names) {
|
|
157
|
+
const wanted = new Set(names.filter((n) => n !== ""));
|
|
158
|
+
if (wanted.size === 0)
|
|
159
|
+
return false;
|
|
160
|
+
let found = false;
|
|
161
|
+
visit(w, { enter(node) { if (node.kind === "ident" && wanted.has(node.name))
|
|
162
|
+
found = true; return !found; } });
|
|
163
|
+
return found;
|
|
164
|
+
}
|
|
131
165
|
// A copy of a node with another span (a parenthesized operand takes the span of
|
|
132
166
|
// its parentheses).
|
|
133
167
|
function withSpan(node, span) {
|
|
@@ -145,6 +179,11 @@ class Parser {
|
|
|
145
179
|
* top-level row's clauses, one deeper per block body and per follow `where`.
|
|
146
180
|
* An absolute reference `N^name` reads `depth − N` scopes out. */
|
|
147
181
|
depth = 0;
|
|
182
|
+
/** Whether a consumer word with no block is read as a directive over the
|
|
183
|
+
* empty block (`jobs first` ≡ `jobs first { }`, since 0.26). Off only while
|
|
184
|
+
* the clause form's top-level `from` expression is read, so `from people
|
|
185
|
+
* count` keeps its whole-query hint (a count is never a source). */
|
|
186
|
+
bareDirectives = true;
|
|
148
187
|
constructor(tokens) {
|
|
149
188
|
this.tokens = tokens;
|
|
150
189
|
}
|
|
@@ -188,9 +227,10 @@ class Parser {
|
|
|
188
227
|
const start = this.start();
|
|
189
228
|
// Directive form: `<receiver> <consumer> { … }` consuming the whole query.
|
|
190
229
|
let directive = this.tryOp(false);
|
|
191
|
-
if (directive && !this.at("eof") && (this.at("lbracket") || this.
|
|
230
|
+
if (directive && !this.at("eof") && (this.at("lbracket") || this.atBlock() !== null)) {
|
|
192
231
|
// A longer chain whose outermost node is still a directive
|
|
193
|
-
// (`people first { } first { }` — how `people[p][0]` prints back
|
|
232
|
+
// (`people first { } first { }` — how `people[p][0]` prints back; a block
|
|
233
|
+
// over the directive, `people[0] { name }`, merges into it, 0.26).
|
|
194
234
|
const save = this.pos;
|
|
195
235
|
try {
|
|
196
236
|
const e = this.parsePostfix(directive);
|
|
@@ -231,7 +271,7 @@ class Parser {
|
|
|
231
271
|
if (!this.at("eof"))
|
|
232
272
|
this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause`);
|
|
233
273
|
if (body.froms.length === 0) {
|
|
234
|
-
this.fail("a query must name its source
|
|
274
|
+
this.fail("a query must name its source before its body: `<collection> { … }` (or `<collection> <consumer> { … }`; the clause form `select … from <collection> …` is still read)");
|
|
235
275
|
}
|
|
236
276
|
return {
|
|
237
277
|
kind: "query",
|
|
@@ -316,7 +356,18 @@ class Parser {
|
|
|
316
356
|
if (this.at("kw", "from")) {
|
|
317
357
|
enter("from");
|
|
318
358
|
this.next();
|
|
319
|
-
|
|
359
|
+
// At the clause form's top level a consumer word after the source is the
|
|
360
|
+
// whole-query mistake (`from people count`): kept as the hint below,
|
|
361
|
+
// not read as a braceless directive (a count is never a source).
|
|
362
|
+
const bare = this.bareDirectives;
|
|
363
|
+
if (ctx.top)
|
|
364
|
+
this.bareDirectives = false;
|
|
365
|
+
try {
|
|
366
|
+
froms.push(this.parseValueExpr());
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
this.bareDirectives = bare;
|
|
370
|
+
}
|
|
320
371
|
continue;
|
|
321
372
|
}
|
|
322
373
|
if (this.at("kw", "where")) {
|
|
@@ -1028,7 +1079,7 @@ class Parser {
|
|
|
1028
1079
|
if (it.kind === "collect")
|
|
1029
1080
|
this.fail("a nested block in a projection needs a name (`name: <relation> collect { … }`); as a predicate, a consumer test is written `where <relation> exists { … }`");
|
|
1030
1081
|
if (ctx.top)
|
|
1031
|
-
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 …` after `from` — at the top level a predicate is never implicit (the clause order puts `from` first)");
|
|
1082
|
+
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 `<collection> { <predicate> }`, or write `where …` after `from` — at the top level of the clause form a predicate is never implicit (the clause order puts `from` first)");
|
|
1032
1083
|
this.fail("an item that is not a plain name needs an alias (`name: expr`) or `values`; to filter by it write `where …` after the projection (a block may lead with a predicate only when nothing is projected: `{ age > 18 }`)");
|
|
1033
1084
|
}
|
|
1034
1085
|
const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
|
|
@@ -1058,8 +1109,9 @@ class Parser {
|
|
|
1058
1109
|
this.next(); // ':'
|
|
1059
1110
|
const op = this.tryOp(false);
|
|
1060
1111
|
if (op) {
|
|
1061
|
-
// `name: jobs first { }.pay`: the directive is the head of a longer value
|
|
1062
|
-
|
|
1112
|
+
// `name: jobs first { }.pay`: the directive is the head of a longer value
|
|
1113
|
+
// (a block over it, `jobs[0] { name }`, merges into it — 0.26).
|
|
1114
|
+
if (this.atScalarContinuation() || this.atBlock() !== null)
|
|
1063
1115
|
return this.finishItem(name, this.parseOr(op), lift, start);
|
|
1064
1116
|
if (op.op !== "collect" && op.op !== "first" && op.op !== "single" && op.op !== "count") {
|
|
1065
1117
|
this.fail(`projection '${name}' must use collect/first/single/count, not ${op.op} (exists/none are where-position tests; \`count { … }\` is a value since 0.24)`);
|
|
@@ -1360,14 +1412,19 @@ class Parser {
|
|
|
1360
1412
|
return this.atConsumerBlock();
|
|
1361
1413
|
}
|
|
1362
1414
|
// The cursor is at a consumer word that opens a block — `<op> {` or
|
|
1363
|
-
// `<op> distinct {
|
|
1415
|
+
// `<op> distinct {` — or, since 0.26, at the word alone: a consumer word
|
|
1416
|
+
// right after a complete receiver can be nothing else (OQX never places two
|
|
1417
|
+
// operands side by side), and it reads as the directive over the empty block
|
|
1418
|
+
// (`jobs first` ≡ `jobs first { }`, `_nodes exists`, `jobs count > 1`). The
|
|
1419
|
+
// braceless reading is off only inside the clause form's top-level `from`
|
|
1420
|
+
// (`bareDirectives`). Consumes nothing.
|
|
1364
1421
|
atConsumerBlock() {
|
|
1365
1422
|
if (!this.at("ident") || !CONSUMERS.has(this.peek().value))
|
|
1366
1423
|
return null;
|
|
1367
1424
|
const after = this.peekAt(1);
|
|
1368
1425
|
const opThenBrace = after?.type === "lbrace";
|
|
1369
1426
|
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
1370
|
-
return opThenBrace || opDistinctBrace ? this.peek().value : null;
|
|
1427
|
+
return opThenBrace || opDistinctBrace || this.bareDirectives ? this.peek().value : null;
|
|
1371
1428
|
}
|
|
1372
1429
|
// `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
|
|
1373
1430
|
// at the consumer word, at `distinct`, or at the `{` of the implicit `collect`.
|
|
@@ -1382,22 +1439,35 @@ class Parser {
|
|
|
1382
1439
|
this.next();
|
|
1383
1440
|
distinct = true;
|
|
1384
1441
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
this.
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
this.
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1442
|
+
let body;
|
|
1443
|
+
let subSpan;
|
|
1444
|
+
if (this.at("lbrace")) {
|
|
1445
|
+
const lbrace = this.next().pos; // '{'
|
|
1446
|
+
const defer = this.deferLookup;
|
|
1447
|
+
this.deferLookup = false;
|
|
1448
|
+
// The receiver was read at the enclosing depth; the block's rows are one deeper.
|
|
1449
|
+
body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect", destination }));
|
|
1450
|
+
this.deferLookup = defer;
|
|
1451
|
+
if (!this.at("rbrace"))
|
|
1452
|
+
this.fail(`expected '}' to close the ${op} { … } block`);
|
|
1453
|
+
this.next();
|
|
1454
|
+
// `x collect distinct { group by k }`: the directive's distinct is as
|
|
1455
|
+
// redundant as the body's own (checked in parseBody).
|
|
1456
|
+
if (distinct && body.groupBy)
|
|
1457
|
+
this.failAt(body.groupBy.span[0], DISTINCT_WITH_GROUP);
|
|
1458
|
+
subSpan = this.sp(lbrace);
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
// A consumer word with no block (0.26) is the directive over the empty
|
|
1462
|
+
// block `{ }` — the word was written (only a brace makes a bare
|
|
1463
|
+
// `collect`), so nothing more is consumed; the block spans nothing, at
|
|
1464
|
+
// the end of the word.
|
|
1465
|
+
body = emptyBody();
|
|
1466
|
+
subSpan = [this.end(), this.end()];
|
|
1467
|
+
}
|
|
1398
1468
|
const sub = {
|
|
1399
1469
|
kind: "subquery",
|
|
1400
|
-
span:
|
|
1470
|
+
span: subSpan,
|
|
1401
1471
|
from: body.froms,
|
|
1402
1472
|
where: body.where,
|
|
1403
1473
|
select: body.select,
|
|
@@ -1412,7 +1482,38 @@ class Parser {
|
|
|
1412
1482
|
limit: body.limit,
|
|
1413
1483
|
offset: body.offset,
|
|
1414
1484
|
};
|
|
1415
|
-
return
|
|
1485
|
+
return this.mergeProjectionBlock(receiver, op, distinct || body.distinct, sub, startPos);
|
|
1486
|
+
}
|
|
1487
|
+
// A projection-only block over a `collect`/`first`/`single` directive whose
|
|
1488
|
+
// own block projects nothing IS that directive's projection (0.26):
|
|
1489
|
+
// `jobs[0] { name }` ≡ `jobs first { name offset 0 }`, `jobs[p] { name }` ≡
|
|
1490
|
+
// `jobs first { name where p }`, `jobs[0] { }` ≡ `jobs[0]`. The outer items
|
|
1491
|
+
// are read on the very rows the inner block filtered, at the same depth, so
|
|
1492
|
+
// the merge is exact — except when the outer block carries a clause of its
|
|
1493
|
+
// own (those run AFTER the pick: `jobs[0] { name where pay > 2 }` is the
|
|
1494
|
+
// first job, kept only if it pays), when the inner directive dedups
|
|
1495
|
+
// (`distinct` compares the projection), or when an outer alias would shadow a
|
|
1496
|
+
// bare name the inner `where` reads (`jobs[pay > 2] { pay: employer }`): then
|
|
1497
|
+
// the two directives stay.
|
|
1498
|
+
mergeProjectionBlock(receiver, op, distinct, sub, startPos) {
|
|
1499
|
+
const plain = { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct };
|
|
1500
|
+
if (receiver.kind !== "op" || op !== "collect")
|
|
1501
|
+
return plain;
|
|
1502
|
+
const inner = receiver;
|
|
1503
|
+
if ((inner.op !== "collect" && inner.op !== "first" && inner.op !== "single") || inner.distinct || inner.countCmp !== null)
|
|
1504
|
+
return plain;
|
|
1505
|
+
if (inner.sub.select.length > 0 || inner.sub.values)
|
|
1506
|
+
return plain;
|
|
1507
|
+
const projectionOnly = sub.from.length === 0 && sub.where === null && sub.orderBy === null && sub.follow === null && sub.frontier === null
|
|
1508
|
+
&& sub.depth === null && sub.flatten.length === 0 && sub.groupBy === null && sub.having === null && sub.limit === null && sub.offset === null;
|
|
1509
|
+
if (!projectionOnly)
|
|
1510
|
+
return plain;
|
|
1511
|
+
if (inner.sub.where && readsAnyOf(inner.sub.where, shadowingNames(sub.select)))
|
|
1512
|
+
return plain;
|
|
1513
|
+
return {
|
|
1514
|
+
kind: "op", span: this.sp(startPos), receiver: inner.receiver, op: inner.op, countCmp: null, distinct,
|
|
1515
|
+
sub: { ...inner.sub, select: sub.select, values: sub.values, span: [inner.sub.span[0], sub.span[1]] },
|
|
1516
|
+
};
|
|
1416
1517
|
}
|
|
1417
1518
|
// A tuple `(e1, e2, …)` at the cursor's `(` (0.20): two or more comma-separated
|
|
1418
1519
|
// expressions. A single parenthesized expression is grouping, not a tuple —
|