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