@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/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -676,7 +676,6 @@ export class InMemoryEngine implements Engine {
|
|
|
676
676
|
}
|
|
677
677
|
|
|
678
678
|
private evalWhereOp(op: OpNode, scope: Scope): boolean {
|
|
679
|
-
if (op.sub.follow) throw new OqxError("`follow` is only valid on a select-position collect { … }, not a where op", "eval");
|
|
680
679
|
const plan = this.planOf(op, scope);
|
|
681
680
|
if (!plan.invariant) return this.evalWhereOpPlanned(op, plan, scope);
|
|
682
681
|
return this.memoized(op, scope, () => this.evalWhereOpPlanned(op, plan, scope));
|
|
@@ -687,7 +686,12 @@ export class InMemoryEngine implements Engine {
|
|
|
687
686
|
if (op.op === "exists" || op.op === "none" || op.op === "count") {
|
|
688
687
|
const acc = this.access(op, plan, scope);
|
|
689
688
|
let n: number;
|
|
690
|
-
|
|
689
|
+
// A block with a `follow` (legal in every body since 0.26) yields its
|
|
690
|
+
// walk's occurrences: no cardinality answer, no first-match stop — the
|
|
691
|
+
// walk is run and counted like any other rows.
|
|
692
|
+
if (op.sub.follow) {
|
|
693
|
+
n = this.opRows(op, scope, bound, acc).length;
|
|
694
|
+
} else if (plan.fromCardinality && acc.where === null) {
|
|
691
695
|
// Nothing left to evaluate per row: the accessed rows ARE the matched
|
|
692
696
|
// rows, and the consumer only needs their count after the bound.
|
|
693
697
|
n = boundedCount(acc.rows.length, bound);
|
|
@@ -721,14 +725,28 @@ export class InMemoryEngine implements Engine {
|
|
|
721
725
|
return this.opRows(op, scope, bound).length > 0;
|
|
722
726
|
}
|
|
723
727
|
|
|
724
|
-
// The rows a consumer op reduces
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
728
|
+
// The rows a consumer op reduces — in every position (a where-position test,
|
|
729
|
+
// a select-position directive, a destination block): matched (or, with a
|
|
730
|
+
// `follow`, the walk's occurrences over the matched seeds) → flattened →
|
|
731
|
+
// grouped → ordered → distinct → bounded. `destination` marks a follow
|
|
732
|
+
// destination block: its `frontier` is the destination's (read by the outer
|
|
733
|
+
// walk), never a seed frontier for a `follow` of its own.
|
|
734
|
+
private opRows(op: OpNode, scope: Scope, bound: Bound, acc?: Access, destination = false): Scope[] {
|
|
735
|
+
const sub = op.sub;
|
|
736
|
+
let scopes: Scope[];
|
|
737
|
+
if (sub.follow) {
|
|
738
|
+
// The seeds are the matched rows themselves (`followWalk` enters them); a
|
|
739
|
+
// follow block is never correlated, so `where` here is the whole predicate.
|
|
740
|
+
const { rows, where } = acc ?? this.access(op, this.planOf(op, scope), scope);
|
|
741
|
+
const seeds = where ? rows.filter((r) => this.evalWhere(where, this.enter(r, scope, { lifts: {} }))) : rows;
|
|
742
|
+
scopes = this.followWalk(this.keyed(seeds, sub, scope), sub.follow, destination ? null : sub.frontier, scope).map((o) => this.enter(o.row, scope, { meta: o.meta }));
|
|
743
|
+
} else scopes = this.matchRows(op, scope, acc);
|
|
744
|
+
if (sub.flatten.length > 0) scopes = this.flattenScopes(scopes, sub);
|
|
745
|
+
if (sub.groupBy) scopes = this.groupScopes(scopes, sub, scope);
|
|
746
|
+
// An occurrence's `$key` is the walk's identity (`keyed` gave the seeds theirs).
|
|
747
|
+
if (!sub.follow) this.applyKey(scopes, sub);
|
|
748
|
+
this.sortScopes(scopes, sub.orderBy);
|
|
749
|
+
if (op.distinct) scopes = this.dedupByProjection(scopes, sub);
|
|
732
750
|
return sliceBound(scopes, bound);
|
|
733
751
|
}
|
|
734
752
|
|
|
@@ -928,24 +946,10 @@ export class InMemoryEngine implements Engine {
|
|
|
928
946
|
private evalCollectScopes(op: OpNode, scope: Scope, plan: BlockPlan = this.planOf(op, scope), destination = false): { scopes: Scope[]; single: boolean } {
|
|
929
947
|
const sub = op.sub;
|
|
930
948
|
const bound = this.boundOf(sub, scope);
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
// follow block is never correlated, so `where` here is the whole predicate.
|
|
936
|
-
const { rows, where } = this.access(op, plan, scope);
|
|
937
|
-
const seeds = where ? rows.filter((r) => this.evalWhere(where, this.enter(r, scope, { lifts: {} }))) : rows;
|
|
938
|
-
scopes = this.followWalk(this.keyed(seeds, sub, scope), sub.follow, destination ? null : sub.frontier, scope).map((o) => this.enter(o.row, scope, { meta: o.meta }));
|
|
939
|
-
if (sub.flatten.length > 0) scopes = this.flattenScopes(scopes, sub);
|
|
940
|
-
if (sub.groupBy) scopes = this.groupScopes(scopes, sub, scope);
|
|
941
|
-
this.sortScopes(scopes, sub.orderBy);
|
|
942
|
-
if (op.distinct) scopes = this.dedupByProjection(scopes, sub);
|
|
943
|
-
scopes = sliceBound(scopes, bound);
|
|
944
|
-
} else {
|
|
945
|
-
const acc = this.access(op, plan, scope);
|
|
946
|
-
single = acc.single;
|
|
947
|
-
scopes = this.opRows(op, scope, bound, acc);
|
|
948
|
-
}
|
|
949
|
+
const acc = this.access(op, plan, scope);
|
|
950
|
+
// A walk is always plural (SEMANTICS §20).
|
|
951
|
+
const single = sub.follow ? false : acc.single;
|
|
952
|
+
const scopes = this.opRows(op, scope, bound, acc, destination);
|
|
949
953
|
switch (op.op) {
|
|
950
954
|
case "collect": case "count": return { scopes, single };
|
|
951
955
|
case "first": return { scopes: scopes.slice(0, 1), single };
|
package/src/optimize/analysis.ts
CHANGED
|
@@ -135,8 +135,9 @@ export function whereRaiseFree(w: Where, ctx: RuleContext): boolean {
|
|
|
135
135
|
|
|
136
136
|
/** A where-position directive that can neither raise nor bind: `exists`,
|
|
137
137
|
* `none` or `count` (never `single`, which raises on several rows; `collect`
|
|
138
|
-
* binds lifts; `first`/`single` project), without `follow` (
|
|
139
|
-
*
|
|
138
|
+
* binds lifts; `first`/`single` project), without `follow` (a walk's
|
|
139
|
+
* destinations may raise, and a walk is never a cardinality answer), without a
|
|
140
|
+
* bound (an invalid value raises), without `distinct`
|
|
140
141
|
* (dedup projects, and a projected range raises), whose receiver, `from`,
|
|
141
142
|
* `where` and `order by` (evaluated by `count`) are raise-free and which lifts
|
|
142
143
|
* nothing anywhere. */
|
package/src/parser.ts
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
|
|
@@ -87,6 +97,7 @@ import type {
|
|
|
87
97
|
} from "./ast.ts";
|
|
88
98
|
import { AGGREGATE_NAMES, isAggregateName } from "./ast.ts";
|
|
89
99
|
import { resolveSelect, resolveWhere } from "./resolve.ts";
|
|
100
|
+
import { visit } from "./walk.ts";
|
|
90
101
|
import { printTemplate } from "./print.ts";
|
|
91
102
|
|
|
92
103
|
const CONSUMERS = new Set<string>(["collect", "exists", "none", "count", "first", "single"]);
|
|
@@ -170,6 +181,30 @@ interface BodyClauses {
|
|
|
170
181
|
offset: Expr | null;
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
/** The clauses of an omitted block (`jobs first` — the empty block `{ }`). */
|
|
185
|
+
function emptyBody(): BodyClauses {
|
|
186
|
+
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 };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The names a projection would shadow in a `where` of the same body: every
|
|
190
|
+
* item's name except a plain self-projection (`pay: pay`, `pay`), which
|
|
191
|
+
* substitutes a name for itself. */
|
|
192
|
+
function shadowingNames(items: SelectItem[]): string[] {
|
|
193
|
+
return items
|
|
194
|
+
.filter((it) => it.name !== "" && !(it.kind === "field" && it.lift === 0 && it.expr.kind === "ident" && it.expr.name === it.name))
|
|
195
|
+
.map((it) => it.name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Whether a bare identifier anywhere under `w` has one of `names` (the alias
|
|
199
|
+
* guard of the projection-block merge; conservative: nested blocks included). */
|
|
200
|
+
function readsAnyOf(w: Where, names: string[]): boolean {
|
|
201
|
+
const wanted = new Set(names.filter((n) => n !== ""));
|
|
202
|
+
if (wanted.size === 0) return false;
|
|
203
|
+
let found = false;
|
|
204
|
+
visit(w, { enter(node) { if (node.kind === "ident" && wanted.has(node.name)) found = true; return !found; } });
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
|
|
173
208
|
// A copy of a node with another span (a parenthesized operand takes the span of
|
|
174
209
|
// its parentheses).
|
|
175
210
|
function withSpan<T extends { span: Span }>(node: T, span: Span): T {
|
|
@@ -188,6 +223,11 @@ class Parser {
|
|
|
188
223
|
* top-level row's clauses, one deeper per block body and per follow `where`.
|
|
189
224
|
* An absolute reference `N^name` reads `depth − N` scopes out. */
|
|
190
225
|
private depth = 0;
|
|
226
|
+
/** Whether a consumer word with no block is read as a directive over the
|
|
227
|
+
* empty block (`jobs first` ≡ `jobs first { }`, since 0.26). Off only while
|
|
228
|
+
* the clause form's top-level `from` expression is read, so `from people
|
|
229
|
+
* count` keeps its whole-query hint (a count is never a source). */
|
|
230
|
+
private bareDirectives = true;
|
|
191
231
|
|
|
192
232
|
constructor(tokens: Token[]) {
|
|
193
233
|
this.tokens = tokens;
|
|
@@ -230,9 +270,10 @@ class Parser {
|
|
|
230
270
|
const start = this.start();
|
|
231
271
|
// Directive form: `<receiver> <consumer> { … }` consuming the whole query.
|
|
232
272
|
let directive = this.tryOp(false);
|
|
233
|
-
if (directive && !this.at("eof") && (this.at("lbracket") || this.
|
|
273
|
+
if (directive && !this.at("eof") && (this.at("lbracket") || this.atBlock() !== null)) {
|
|
234
274
|
// A longer chain whose outermost node is still a directive
|
|
235
|
-
// (`people first { } first { }` — how `people[p][0]` prints back
|
|
275
|
+
// (`people first { } first { }` — how `people[p][0]` prints back; a block
|
|
276
|
+
// over the directive, `people[0] { name }`, merges into it, 0.26).
|
|
236
277
|
const save = this.pos;
|
|
237
278
|
try {
|
|
238
279
|
const e = this.parsePostfix(directive);
|
|
@@ -264,7 +305,7 @@ class Parser {
|
|
|
264
305
|
const body = this.parseBody(TOP_CTX);
|
|
265
306
|
if (!this.at("eof")) this.fail(`unexpected ${this.tokDesc()} after the query — nothing may follow the last clause`);
|
|
266
307
|
if (body.froms.length === 0) {
|
|
267
|
-
this.fail("a query must name its source
|
|
308
|
+
this.fail("a query must name its source before its body: `<collection> { … }` (or `<collection> <consumer> { … }`; the clause form `select … from <collection> …` is still read)");
|
|
268
309
|
}
|
|
269
310
|
return {
|
|
270
311
|
kind: "query",
|
|
@@ -350,7 +391,12 @@ class Parser {
|
|
|
350
391
|
if (this.at("kw", "from")) {
|
|
351
392
|
enter("from");
|
|
352
393
|
this.next();
|
|
353
|
-
|
|
394
|
+
// At the clause form's top level a consumer word after the source is the
|
|
395
|
+
// whole-query mistake (`from people count`): kept as the hint below,
|
|
396
|
+
// not read as a braceless directive (a count is never a source).
|
|
397
|
+
const bare = this.bareDirectives;
|
|
398
|
+
if (ctx.top) this.bareDirectives = false;
|
|
399
|
+
try { froms.push(this.parseValueExpr()); } finally { this.bareDirectives = bare; }
|
|
354
400
|
continue;
|
|
355
401
|
}
|
|
356
402
|
if (this.at("kw", "where")) {
|
|
@@ -980,7 +1026,7 @@ class Parser {
|
|
|
980
1026
|
for (const it of items) {
|
|
981
1027
|
if (it.name === "") {
|
|
982
1028
|
if (it.kind === "collect") 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 { … }`");
|
|
983
|
-
if (ctx.top) 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)");
|
|
1029
|
+
if (ctx.top) 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)");
|
|
984
1030
|
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 }`)");
|
|
985
1031
|
}
|
|
986
1032
|
const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
|
|
@@ -1009,8 +1055,9 @@ class Parser {
|
|
|
1009
1055
|
this.next(); // ':'
|
|
1010
1056
|
const op = this.tryOp(false);
|
|
1011
1057
|
if (op) {
|
|
1012
|
-
// `name: jobs first { }.pay`: the directive is the head of a longer value
|
|
1013
|
-
|
|
1058
|
+
// `name: jobs first { }.pay`: the directive is the head of a longer value
|
|
1059
|
+
// (a block over it, `jobs[0] { name }`, merges into it — 0.26).
|
|
1060
|
+
if (this.atScalarContinuation() || this.atBlock() !== null) return this.finishItem(name, this.parseOr(op), lift, start);
|
|
1014
1061
|
if (op.op !== "collect" && op.op !== "first" && op.op !== "single" && op.op !== "count") {
|
|
1015
1062
|
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)`);
|
|
1016
1063
|
}
|
|
@@ -1267,13 +1314,18 @@ class Parser {
|
|
|
1267
1314
|
}
|
|
1268
1315
|
|
|
1269
1316
|
// The cursor is at a consumer word that opens a block — `<op> {` or
|
|
1270
|
-
// `<op> distinct {
|
|
1317
|
+
// `<op> distinct {` — or, since 0.26, at the word alone: a consumer word
|
|
1318
|
+
// right after a complete receiver can be nothing else (OQX never places two
|
|
1319
|
+
// operands side by side), and it reads as the directive over the empty block
|
|
1320
|
+
// (`jobs first` ≡ `jobs first { }`, `_nodes exists`, `jobs count > 1`). The
|
|
1321
|
+
// braceless reading is off only inside the clause form's top-level `from`
|
|
1322
|
+
// (`bareDirectives`). Consumes nothing.
|
|
1271
1323
|
private atConsumerBlock(): Consumer | null {
|
|
1272
1324
|
if (!this.at("ident") || !CONSUMERS.has(this.peek().value)) return null;
|
|
1273
1325
|
const after = this.peekAt(1);
|
|
1274
1326
|
const opThenBrace = after?.type === "lbrace";
|
|
1275
1327
|
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
1276
|
-
return opThenBrace || opDistinctBrace ? (this.peek().value as Consumer) : null;
|
|
1328
|
+
return opThenBrace || opDistinctBrace || this.bareDirectives ? (this.peek().value as Consumer) : null;
|
|
1277
1329
|
}
|
|
1278
1330
|
|
|
1279
1331
|
// `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
|
|
@@ -1285,20 +1337,32 @@ class Parser {
|
|
|
1285
1337
|
if (this.at("ident") && CONSUMERS.has(this.peek().value)) this.next(); // the consumer word (absent for `x { … }`)
|
|
1286
1338
|
let distinct = false;
|
|
1287
1339
|
if (this.at("ident", "distinct")) { this.next(); distinct = true; }
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
this.
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1340
|
+
let body: BodyClauses;
|
|
1341
|
+
let subSpan: Span;
|
|
1342
|
+
if (this.at("lbrace")) {
|
|
1343
|
+
const lbrace = this.next().pos; // '{'
|
|
1344
|
+
const defer = this.deferLookup;
|
|
1345
|
+
this.deferLookup = false;
|
|
1346
|
+
// The receiver was read at the enclosing depth; the block's rows are one deeper.
|
|
1347
|
+
body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect", destination }));
|
|
1348
|
+
this.deferLookup = defer;
|
|
1349
|
+
if (!this.at("rbrace")) this.fail(`expected '}' to close the ${op} { … } block`);
|
|
1350
|
+
this.next();
|
|
1351
|
+
// `x collect distinct { group by k }`: the directive's distinct is as
|
|
1352
|
+
// redundant as the body's own (checked in parseBody).
|
|
1353
|
+
if (distinct && body.groupBy) this.failAt(body.groupBy.span[0], DISTINCT_WITH_GROUP);
|
|
1354
|
+
subSpan = this.sp(lbrace);
|
|
1355
|
+
} else {
|
|
1356
|
+
// A consumer word with no block (0.26) is the directive over the empty
|
|
1357
|
+
// block `{ }` — the word was written (only a brace makes a bare
|
|
1358
|
+
// `collect`), so nothing more is consumed; the block spans nothing, at
|
|
1359
|
+
// the end of the word.
|
|
1360
|
+
body = emptyBody();
|
|
1361
|
+
subSpan = [this.end(), this.end()];
|
|
1362
|
+
}
|
|
1299
1363
|
const sub: Subquery = {
|
|
1300
1364
|
kind: "subquery",
|
|
1301
|
-
span:
|
|
1365
|
+
span: subSpan,
|
|
1302
1366
|
from: body.froms,
|
|
1303
1367
|
where: body.where,
|
|
1304
1368
|
select: body.select,
|
|
@@ -1313,7 +1377,34 @@ class Parser {
|
|
|
1313
1377
|
limit: body.limit,
|
|
1314
1378
|
offset: body.offset,
|
|
1315
1379
|
};
|
|
1316
|
-
return
|
|
1380
|
+
return this.mergeProjectionBlock(receiver, op, distinct || body.distinct, sub, startPos);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// A projection-only block over a `collect`/`first`/`single` directive whose
|
|
1384
|
+
// own block projects nothing IS that directive's projection (0.26):
|
|
1385
|
+
// `jobs[0] { name }` ≡ `jobs first { name offset 0 }`, `jobs[p] { name }` ≡
|
|
1386
|
+
// `jobs first { name where p }`, `jobs[0] { }` ≡ `jobs[0]`. The outer items
|
|
1387
|
+
// are read on the very rows the inner block filtered, at the same depth, so
|
|
1388
|
+
// the merge is exact — except when the outer block carries a clause of its
|
|
1389
|
+
// own (those run AFTER the pick: `jobs[0] { name where pay > 2 }` is the
|
|
1390
|
+
// first job, kept only if it pays), when the inner directive dedups
|
|
1391
|
+
// (`distinct` compares the projection), or when an outer alias would shadow a
|
|
1392
|
+
// bare name the inner `where` reads (`jobs[pay > 2] { pay: employer }`): then
|
|
1393
|
+
// the two directives stay.
|
|
1394
|
+
private mergeProjectionBlock(receiver: Expr, op: Consumer, distinct: boolean, sub: Subquery, startPos: number): OpNode {
|
|
1395
|
+
const plain: OpNode = { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct };
|
|
1396
|
+
if (receiver.kind !== "op" || op !== "collect") return plain;
|
|
1397
|
+
const inner = receiver;
|
|
1398
|
+
if ((inner.op !== "collect" && inner.op !== "first" && inner.op !== "single") || inner.distinct || inner.countCmp !== null) return plain;
|
|
1399
|
+
if (inner.sub.select.length > 0 || inner.sub.values) return plain;
|
|
1400
|
+
const projectionOnly = sub.from.length === 0 && sub.where === null && sub.orderBy === null && sub.follow === null && sub.frontier === null
|
|
1401
|
+
&& sub.depth === null && sub.flatten.length === 0 && sub.groupBy === null && sub.having === null && sub.limit === null && sub.offset === null;
|
|
1402
|
+
if (!projectionOnly) return plain;
|
|
1403
|
+
if (inner.sub.where && readsAnyOf(inner.sub.where, shadowingNames(sub.select))) return plain;
|
|
1404
|
+
return {
|
|
1405
|
+
kind: "op", span: this.sp(startPos), receiver: inner.receiver, op: inner.op, countCmp: null, distinct,
|
|
1406
|
+
sub: { ...inner.sub, select: sub.select, values: sub.values, span: [inner.sub.span[0], sub.span[1]] },
|
|
1407
|
+
};
|
|
1317
1408
|
}
|
|
1318
1409
|
|
|
1319
1410
|
// A tuple `(e1, e2, …)` at the cursor's `(` (0.20): two or more comma-separated
|
package/src/print.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// The canonical printer (spec/oqx/AST.md §6): the one source text of a tree.
|
|
2
2
|
// Single spaces, double-quoted strings with GRAMMAR §1 escapes, the minimal
|
|
3
|
-
// parentheses the precedence table (GRAMMAR §4) needs,
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// parentheses the precedence table (GRAMMAR §4) needs, the collection-scoped
|
|
4
|
+
// form `<source> { … }` for every query whose source can head a block (since
|
|
5
|
+
// 0.26; `collect` is the bare brace, every other consumer its word), the
|
|
6
|
+
// projection leading its body without `select`, the connectives as the words
|
|
7
|
+
// `and` / `or` (since 0.18; `!` stays `!`), every clause in its canonical
|
|
8
|
+
// position (a destination block's `frontier` and `depth` after its `where`,
|
|
9
|
+
// since 0.20). The law both spec runners enforce over every fixture query:
|
|
8
10
|
//
|
|
9
11
|
// strip(parse(print(parse(q)))) ≡ strip(parse(q))
|
|
10
12
|
//
|
|
@@ -19,8 +21,8 @@ import { OqxError } from "./errors.ts";
|
|
|
19
21
|
* stands for the binding `indices[i]` of the printed tree, so a caller re-runs it
|
|
20
22
|
* as `parseTemplate(strings, count)` with the values permuted by `indices`. In a
|
|
21
23
|
* tree straight from `parseTemplate`, `indices` is `0..count-1` unless the
|
|
22
|
-
* canonical
|
|
23
|
-
*
|
|
24
|
+
* canonical form moved a binding past another (`select x: ${1} from ${0}` prints
|
|
25
|
+
* as `${0} { x: ${1} }`). */
|
|
24
26
|
export interface Template {
|
|
25
27
|
strings: string[];
|
|
26
28
|
count: number;
|
|
@@ -96,26 +98,62 @@ function render(n: AstNode): Out {
|
|
|
96
98
|
// ---- query / bodies ---------------------------------------------------------
|
|
97
99
|
|
|
98
100
|
function query(q: Query): Out {
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
// The collection-scoped form (since 0.26): `<source> [<consumer>] [distinct]
|
|
102
|
+
// { body }` — the source heads the body, `collect` is the bare brace (the
|
|
103
|
+
// directive word is written after an `op` source, where a bare brace is not
|
|
104
|
+
// read as a block), `distinct` stays on the head.
|
|
105
|
+
if (canHeadBlock(q.source)) {
|
|
106
|
+
return cat(expr(q.source), directive(q.consumer, q.distinct, q.source, q));
|
|
107
|
+
}
|
|
108
|
+
// A source that cannot head a block — a literal, a template, a range,
|
|
109
|
+
// arithmetic, a logical or `in` operand, a unary, an aggregate: every one an
|
|
110
|
+
// eval error or a degenerate query — keeps the clause form `select … from S …`
|
|
111
|
+
// (a `collect`; another consumer over such a source is unparseable either way).
|
|
112
|
+
// `select` is always written — except for a leading bare item literally named
|
|
113
|
+
// `distinct`, which the keyword would swallow as the modifier.
|
|
114
|
+
const first = q.select[0];
|
|
115
|
+
const bareDistinct = !q.distinct && first?.kind === "field" && first.name === "distinct"
|
|
116
|
+
&& first.expr.kind === "ident" && first.expr.name === "distinct";
|
|
117
|
+
const clauses: Out[] = [];
|
|
118
|
+
if (q.select.length > 0) {
|
|
119
|
+
const items = projection(q.select, q.values);
|
|
120
|
+
clauses.push(bareDistinct ? items : cat(`select ${q.distinct ? "distinct " : ""}`, items));
|
|
121
|
+
}
|
|
122
|
+
clauses.push(cat("from ", expr(q.source)));
|
|
123
|
+
clauses.push(...tail(q));
|
|
124
|
+
return join(clauses, " ");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Whether `e { … }` parses with `e` as the receiver (GRAMMAR §5 and the postfix
|
|
128
|
+
// chain of §4): a tuple, or a chain — navigation, calls, `!`, directives — whose
|
|
129
|
+
// head is a bare name, an outer reference, a binding or a free call. A literal
|
|
130
|
+
// or template head is "not a collection"; an operator (`-x`, `a + b`, `a..b`,
|
|
131
|
+
// `a in b`, `a or b`) or an aggregate cannot head a receiver.
|
|
132
|
+
function canHeadBlock(e: Expr): boolean {
|
|
133
|
+
switch (e.kind) {
|
|
134
|
+
case "ident": case "outer": case "binding": case "tuple": return true;
|
|
135
|
+
case "call": return e.recv === null || chainHead(e.recv);
|
|
136
|
+
case "member": return chainHead(e.recv);
|
|
137
|
+
case "required": return chainHead(e.expr);
|
|
138
|
+
case "op": return chainHead(e.receiver);
|
|
139
|
+
default: return false;
|
|
116
140
|
}
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The head of a postfix chain that may take a block: as `canHeadBlock`, but a
|
|
144
|
+
// tuple is a receiver only on its own (`(a, b).x { … }` is not read as one).
|
|
145
|
+
function chainHead(e: Expr): boolean {
|
|
146
|
+
return e.kind !== "tuple" && canHeadBlock(e);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The text between a receiver and its block: ` { ` for the bare-brace `collect`
|
|
150
|
+
// (since 0.26 the canonical spelling), ` distinct { ` for a distinct collect,
|
|
151
|
+
// ` <op> [distinct] { ` for the other consumers — and for a `collect` over an
|
|
152
|
+
// `op` receiver, where a bare brace right after a written directive is not
|
|
153
|
+
// read as a block (`jobs first { offset 0 } collect { … }`).
|
|
154
|
+
function directiveWord(consumer: OpNode["op"], distinct: boolean, receiver: Expr): string {
|
|
155
|
+
if (consumer === "collect" && receiver.kind !== "op") return distinct ? " distinct " : " ";
|
|
156
|
+
return ` ${consumer}${distinct ? " distinct" : ""} `;
|
|
119
157
|
}
|
|
120
158
|
|
|
121
159
|
type Body = Pick<Subquery, "select" | "values" | "from" | "where" | "follow" | "flatten" | "groupBy" | "having" | "orderBy" | "limit" | "offset">
|
|
@@ -174,12 +212,27 @@ function order(o: OrderSpec): Out {
|
|
|
174
212
|
function op(o: OpNode): Out {
|
|
175
213
|
return cat(
|
|
176
214
|
receiver(o.receiver),
|
|
177
|
-
|
|
178
|
-
block(o.sub),
|
|
215
|
+
directive(o.op, o.distinct, o.receiver, o.sub),
|
|
179
216
|
o.countCmp ? ` ${o.countCmp.op} ${number(o.countCmp.value)}` : "",
|
|
180
217
|
);
|
|
181
218
|
}
|
|
182
219
|
|
|
220
|
+
// A directive's word and block after its receiver: the bare brace for
|
|
221
|
+
// `collect`, the word for every other consumer — and, when the word is written
|
|
222
|
+
// and the block is empty, the word alone (`jobs first`, `_nodes exists`, `jobs
|
|
223
|
+
// count distinct`; since 0.26 an omitted block is the empty block).
|
|
224
|
+
function directive(consumer: OpNode["op"], distinct: boolean, receiver: Expr, b: Body): Out {
|
|
225
|
+
const word = directiveWord(consumer, distinct, receiver);
|
|
226
|
+
if (word !== " " && word !== " distinct " && emptyBody(b)) return [word.trimEnd()];
|
|
227
|
+
return cat(word, braces(body(b)));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function emptyBody(b: Body): boolean {
|
|
231
|
+
return b.select.length === 0 && !b.values && b.from.length === 0 && b.where === null && (b.frontier ?? null) === null && (b.depth ?? null) === null
|
|
232
|
+
&& b.follow === null && b.flatten.length === 0 && b.groupBy === null && b.having === null && (b.orderBy === null || b.orderBy.length === 0)
|
|
233
|
+
&& b.limit === null && b.offset === null;
|
|
234
|
+
}
|
|
235
|
+
|
|
183
236
|
function follow(f: Follow): Out {
|
|
184
237
|
const dests = join(f.destinations.map((d) => (d.kind === "op" ? op(d) : expr(d))), ", ");
|
|
185
238
|
return cat(`follow${f.distinct ? " distinct" : ""} `, dests);
|
package/src/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** The OQX language version (`spec/oqx/VERSION`, `major.minor`) this implementation
|
|
2
2
|
* conforms to — distinct from the package's own semver, whose `major.minor` it
|
|
3
3
|
* equals. Stamped on every `toJSON` document. */
|
|
4
|
-
export const LANGUAGE_VERSION = "0.
|
|
4
|
+
export const LANGUAGE_VERSION = "0.26";
|