@omgbase/oqx 0.15.0 → 0.18.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 +36 -0
- package/README.md +170 -17
- package/dist/ast.d.ts +76 -22
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +21 -6
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +37 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +54 -0
- package/dist/build.js.map +1 -0
- package/dist/context.d.ts +13 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -3
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +1 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +52 -23
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -12
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +15 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +104 -51
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +5 -3
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +2 -2
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +3 -3
- package/dist/optimize/ir.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +616 -251
- package/dist/parser.js.map +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +4 -2
- package/dist/plan.js.map +1 -1
- package/dist/print.d.ts +17 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +266 -0
- package/dist/print.js.map +1 -0
- package/dist/resolve.d.ts +23 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +330 -0
- package/dist/resolve.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/walk.d.ts +54 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +139 -0
- package/dist/walk.js.map +1 -0
- package/package.json +1 -1
- package/src/ast.ts +104 -35
- package/src/build.ts +72 -0
- package/src/context.ts +16 -3
- package/src/engine.ts +55 -26
- package/src/errors.ts +3 -2
- package/src/index.ts +38 -13
- package/src/lexer.ts +102 -51
- package/src/optimize/analysis.ts +4 -3
- package/src/optimize/ir.ts +4 -4
- package/src/parser.ts +573 -225
- package/src/plan.ts +3 -2
- package/src/print.ts +274 -0
- package/src/resolve.ts +305 -0
- package/src/version.ts +4 -0
- package/src/walk.ts +180 -0
package/src/lexer.ts
CHANGED
|
@@ -11,18 +11,27 @@
|
|
|
11
11
|
// host-bindings design note calls for. (A consequence: `${x}` inside a string
|
|
12
12
|
// literal does not interpolate — that fragment would be an unterminated string —
|
|
13
13
|
// which is exactly the desired "interpolation is a value, never source text".)
|
|
14
|
+
//
|
|
15
|
+
// Positions (`Token.pos` / `Token.end`, the `span` of every AST node, and the
|
|
16
|
+
// offsets quoted in error messages) count Unicode CODE POINTS over the raw
|
|
17
|
+
// source (`rawSource` for a template, where a binding occupies its `${n}`
|
|
18
|
+
// marker) — not UTF-16 code units — so the two implementations agree on every
|
|
19
|
+
// offset (spec/oqx/AST.md §3). `toUtf16` converts a span for a UTF-16 editor.
|
|
14
20
|
|
|
15
21
|
import { OqxError } from "./errors.ts";
|
|
22
|
+
import type { Span } from "./ast.ts";
|
|
16
23
|
|
|
17
24
|
export type TokType =
|
|
18
25
|
| "ident"
|
|
19
|
-
| "kw" // from | where | select
|
|
26
|
+
| "kw" // from | where | select | is | not | and | or
|
|
20
27
|
| "string"
|
|
21
28
|
| "number"
|
|
22
29
|
| "lparen"
|
|
23
30
|
| "rparen"
|
|
24
31
|
| "lbrace"
|
|
25
32
|
| "rbrace"
|
|
33
|
+
| "lbracket" // [ — a bracket lookup (since 0.17)
|
|
34
|
+
| "rbracket"
|
|
26
35
|
| "comma"
|
|
27
36
|
| "colon"
|
|
28
37
|
| "caret" // ^ — one-scope lift marker
|
|
@@ -34,20 +43,33 @@ export type TokType =
|
|
|
34
43
|
|
|
35
44
|
export interface Token {
|
|
36
45
|
type: TokType;
|
|
46
|
+
/** The source text of the token, except: a `string` token carries its decoded
|
|
47
|
+
* value (quotes stripped, escapes resolved); a `binding` carries the display
|
|
48
|
+
* marker `${N}`; `eof` carries `""`. */
|
|
37
49
|
value: string;
|
|
50
|
+
/** Code-point offset of the token's first character. */
|
|
38
51
|
pos: number;
|
|
52
|
+
/** Code-point offset just past the token's last character (`[pos, end)`). */
|
|
53
|
+
end: number;
|
|
39
54
|
index?: number; // binding tokens only
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
const KEYWORDS = new Set(["from", "where", "select"]);
|
|
57
|
+
const KEYWORDS = new Set(["from", "where", "select", "is", "not", "and", "or"]);
|
|
43
58
|
|
|
44
59
|
// Multi-char operators, longest first (the scanner tries these before singles).
|
|
45
60
|
const MULTI_OPS = ["==", "!=", "<=", ">=", "&&", "||"];
|
|
46
61
|
const SINGLE_OPS = new Set(["<", ">", "!", "+", "-", "*", "/", "%"]);
|
|
47
62
|
|
|
48
|
-
const isIdentStart = (c: string): boolean =>
|
|
49
|
-
const isIdentPart = (c: string): boolean =>
|
|
50
|
-
const isDigit = (c: string): boolean => c >= "0" && c <= "9";
|
|
63
|
+
const isIdentStart = (c: string): boolean => /^[A-Za-z_$]$/.test(c);
|
|
64
|
+
const isIdentPart = (c: string): boolean => /^[A-Za-z0-9_$]$/.test(c);
|
|
65
|
+
const isDigit = (c: string): boolean => c.length === 1 && c >= "0" && c <= "9";
|
|
66
|
+
|
|
67
|
+
/** The length of a string in Unicode code points. */
|
|
68
|
+
export function codePointLength(s: string): number {
|
|
69
|
+
let n = 0;
|
|
70
|
+
for (const _ of s) n++;
|
|
71
|
+
return n;
|
|
72
|
+
}
|
|
51
73
|
|
|
52
74
|
/** Lex a tagged-template call: the cooked string fragments and the count of
|
|
53
75
|
* interpolated values. Emits a single flat token stream with `binding` tokens
|
|
@@ -57,18 +79,18 @@ export function lexTemplate(fragments: readonly string[], values: number): Token
|
|
|
57
79
|
let base = 0; // running offset across fragments + rendered `${…}` markers
|
|
58
80
|
for (let f = 0; f < fragments.length; f++) {
|
|
59
81
|
lexFragment(fragments[f]!, base, tokens);
|
|
60
|
-
base += fragments[f]
|
|
82
|
+
base += codePointLength(fragments[f]!);
|
|
61
83
|
if (f < fragments.length - 1) {
|
|
62
84
|
// account for the value's rendered width in the display source (see rawSource)
|
|
63
85
|
const marker = `\${${f}}`;
|
|
64
|
-
tokens.push({ type: "binding", value: marker, pos: base, index: f });
|
|
86
|
+
tokens.push({ type: "binding", value: marker, pos: base, end: base + marker.length, index: f });
|
|
65
87
|
base += marker.length;
|
|
66
88
|
}
|
|
67
89
|
}
|
|
68
90
|
if (values !== fragments.length - 1) {
|
|
69
91
|
throw new OqxError(`template arity mismatch: ${fragments.length} fragments, ${values} values`, "lex");
|
|
70
92
|
}
|
|
71
|
-
tokens.push({ type: "eof", value: "", pos: base });
|
|
93
|
+
tokens.push({ type: "eof", value: "", pos: base, end: base });
|
|
72
94
|
return tokens;
|
|
73
95
|
}
|
|
74
96
|
|
|
@@ -76,36 +98,43 @@ export function lexTemplate(fragments: readonly string[], values: number): Token
|
|
|
76
98
|
export function lexString(src: string): Token[] {
|
|
77
99
|
const tokens: Token[] = [];
|
|
78
100
|
lexFragment(src, 0, tokens);
|
|
79
|
-
|
|
101
|
+
const n = codePointLength(src);
|
|
102
|
+
tokens.push({ type: "eof", value: "", pos: n, end: n });
|
|
80
103
|
return tokens;
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
function lexFragment(src: string, base: number, out: Token[]): void {
|
|
107
|
+
// One entry per code point, so every index below is a code-point offset.
|
|
108
|
+
const cps = Array.from(src);
|
|
84
109
|
let i = 0;
|
|
85
|
-
const n =
|
|
86
|
-
const
|
|
87
|
-
|
|
110
|
+
const n = cps.length;
|
|
111
|
+
const at = (j: number): string => cps[j] ?? "";
|
|
112
|
+
const text = (from: number, to: number): string => cps.slice(from, to).join("");
|
|
113
|
+
const push = (type: TokType, value: string, start: number, end: number): void => {
|
|
114
|
+
out.push({ type, value, pos: base + start, end: base + end });
|
|
88
115
|
};
|
|
89
116
|
|
|
90
117
|
while (i < n) {
|
|
91
|
-
const c =
|
|
118
|
+
const c = cps[i]!;
|
|
92
119
|
|
|
93
120
|
if (c === " " || c === "\t" || c === "\n" || c === "\r") { i++; continue; }
|
|
94
121
|
|
|
95
|
-
if (c === "(") { push("lparen", c, i); i++; continue; }
|
|
96
|
-
if (c === ")") { push("rparen", c, i); i++; continue; }
|
|
97
|
-
if (c === "{") { push("lbrace", c, i); i++; continue; }
|
|
98
|
-
if (c === "}") { push("rbrace", c, i); i++; continue; }
|
|
99
|
-
if (c === "
|
|
100
|
-
if (c === "
|
|
101
|
-
if (c === "
|
|
122
|
+
if (c === "(") { push("lparen", c, i, i + 1); i++; continue; }
|
|
123
|
+
if (c === ")") { push("rparen", c, i, i + 1); i++; continue; }
|
|
124
|
+
if (c === "{") { push("lbrace", c, i, i + 1); i++; continue; }
|
|
125
|
+
if (c === "}") { push("rbrace", c, i, i + 1); i++; continue; }
|
|
126
|
+
if (c === "[") { push("lbracket", c, i, i + 1); i++; continue; }
|
|
127
|
+
if (c === "]") { push("rbracket", c, i, i + 1); i++; continue; }
|
|
128
|
+
if (c === ",") { push("comma", c, i, i + 1); i++; continue; }
|
|
129
|
+
if (c === ":") { push("colon", c, i, i + 1); i++; continue; }
|
|
130
|
+
if (c === "^") { push("caret", c, i, i + 1); i++; continue; }
|
|
102
131
|
|
|
103
132
|
// range operator — `...` (exclusive end) or `..` (inclusive), longest first.
|
|
104
133
|
// Scanned before the dot rule so `a..b` never looks like member navigation,
|
|
105
134
|
// and before the number rule so the bounds lex as separate numbers.
|
|
106
|
-
if (c === "." &&
|
|
107
|
-
if (
|
|
108
|
-
push("range", "..", i); i += 2; continue;
|
|
135
|
+
if (c === "." && at(i + 1) === ".") {
|
|
136
|
+
if (at(i + 2) === ".") { push("range", "...", i, i + 3); i += 3; continue; }
|
|
137
|
+
push("range", "..", i, i + 2); i += 2; continue;
|
|
109
138
|
}
|
|
110
139
|
|
|
111
140
|
// `.` followed by a digit is neither navigation (a property name cannot start
|
|
@@ -113,12 +142,12 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
113
142
|
// malformed number, reported as such rather than surfacing as a confusing
|
|
114
143
|
// parse error downstream. Any other `.` is member navigation.
|
|
115
144
|
if (c === ".") {
|
|
116
|
-
if (isDigit(
|
|
117
|
-
const end = scanNumberTail(
|
|
118
|
-
const lit =
|
|
145
|
+
if (isDigit(at(i + 1))) {
|
|
146
|
+
const end = scanNumberTail(cps, i + 1, base);
|
|
147
|
+
const lit = text(i, end);
|
|
119
148
|
throw new OqxError(`malformed number ${JSON.stringify(lit)} at ${base + i} — a number starts with a digit (write 0${lit}), and a property name cannot be a digit (there is no index access)`, "lex");
|
|
120
149
|
}
|
|
121
|
-
push("dot", c, i); i++; continue;
|
|
150
|
+
push("dot", c, i, i + 1); i++; continue;
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
// string literal — decode into its VALUE (quotes stripped, escapes resolved).
|
|
@@ -127,18 +156,18 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
127
156
|
const start = i;
|
|
128
157
|
i++;
|
|
129
158
|
let sval = "";
|
|
130
|
-
while (i < n &&
|
|
131
|
-
if (
|
|
159
|
+
while (i < n && cps[i] !== quote) {
|
|
160
|
+
if (cps[i] === "\\") {
|
|
132
161
|
i++;
|
|
133
|
-
sval += unescape(
|
|
162
|
+
sval += unescape(cps[i]);
|
|
134
163
|
} else {
|
|
135
|
-
sval +=
|
|
164
|
+
sval += cps[i]!;
|
|
136
165
|
}
|
|
137
166
|
i++;
|
|
138
167
|
}
|
|
139
168
|
if (i >= n) throw new OqxError(`unterminated string literal at ${base + start}`, "lex");
|
|
140
169
|
i++; // closing quote
|
|
141
|
-
|
|
170
|
+
push("string", sval, start, i);
|
|
142
171
|
continue;
|
|
143
172
|
}
|
|
144
173
|
|
|
@@ -149,23 +178,23 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
149
178
|
// NaN or a dangling dot.
|
|
150
179
|
if (isDigit(c)) {
|
|
151
180
|
const start = i;
|
|
152
|
-
i = scanNumberTail(
|
|
153
|
-
push("number",
|
|
181
|
+
i = scanNumberTail(cps, i, base);
|
|
182
|
+
push("number", text(start, i), start, i);
|
|
154
183
|
continue;
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
// operators (multi-char first)
|
|
158
|
-
const two =
|
|
159
|
-
if (MULTI_OPS.includes(two)) { push("op", two, i); i += 2; continue; }
|
|
160
|
-
if (SINGLE_OPS.has(c)) { push("op", c, i); i++; continue; }
|
|
187
|
+
const two = c + at(i + 1);
|
|
188
|
+
if (MULTI_OPS.includes(two)) { push("op", two, i, i + 2); i += 2; continue; }
|
|
189
|
+
if (SINGLE_OPS.has(c)) { push("op", c, i, i + 1); i++; continue; }
|
|
161
190
|
|
|
162
191
|
// identifier / keyword
|
|
163
192
|
if (isIdentStart(c)) {
|
|
164
193
|
const start = i;
|
|
165
194
|
i++;
|
|
166
|
-
while (i < n && isIdentPart(
|
|
167
|
-
const word =
|
|
168
|
-
push(KEYWORDS.has(word) ? "kw" : "ident", word, start);
|
|
195
|
+
while (i < n && isIdentPart(cps[i]!)) i++;
|
|
196
|
+
const word = text(start, i);
|
|
197
|
+
push(KEYWORDS.has(word) ? "kw" : "ident", word, start, i);
|
|
169
198
|
continue;
|
|
170
199
|
}
|
|
171
200
|
|
|
@@ -176,24 +205,25 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
176
205
|
// Scan a number whose first digit is at `i`; return the index just past it.
|
|
177
206
|
// Throws the malformed-number lex error for a trailing decimal point or an
|
|
178
207
|
// exponent without digits.
|
|
179
|
-
function scanNumberTail(
|
|
208
|
+
function scanNumberTail(cps: readonly string[], i: number, base = 0): number {
|
|
180
209
|
const start = i;
|
|
181
|
-
const n =
|
|
210
|
+
const n = cps.length;
|
|
211
|
+
const at = (j: number): string => cps[j] ?? "";
|
|
182
212
|
const fail = (end: number, why: string): never => {
|
|
183
|
-
throw new OqxError(`malformed number ${JSON.stringify(
|
|
213
|
+
throw new OqxError(`malformed number ${JSON.stringify(cps.slice(start, end).join(""))} at ${base + start} — ${why}`, "lex");
|
|
184
214
|
};
|
|
185
|
-
while (i < n && isDigit(
|
|
186
|
-
if (
|
|
187
|
-
if (!isDigit(
|
|
215
|
+
while (i < n && isDigit(cps[i]!)) i++;
|
|
216
|
+
if (at(i) === "." && at(i + 1) !== ".") {
|
|
217
|
+
if (!isDigit(at(i + 1))) fail(i + 1, "a decimal point needs a digit after it (write 1.0, not 1.)");
|
|
188
218
|
i++;
|
|
189
|
-
while (i < n && isDigit(
|
|
219
|
+
while (i < n && isDigit(cps[i]!)) i++;
|
|
190
220
|
}
|
|
191
|
-
if (
|
|
221
|
+
if (at(i) === "e" || at(i) === "E") {
|
|
192
222
|
let j = i + 1;
|
|
193
|
-
if (
|
|
194
|
-
if (!isDigit(
|
|
223
|
+
if (at(j) === "+" || at(j) === "-") j++;
|
|
224
|
+
if (!isDigit(at(j))) fail(j, "an exponent needs at least one digit (write 1e5)");
|
|
195
225
|
i = j;
|
|
196
|
-
while (i < n && isDigit(
|
|
226
|
+
while (i < n && isDigit(cps[i]!)) i++;
|
|
197
227
|
}
|
|
198
228
|
return i;
|
|
199
229
|
}
|
|
@@ -214,3 +244,24 @@ function unescape(c: string | undefined): string {
|
|
|
214
244
|
export function rawSource(fragments: readonly string[]): string {
|
|
215
245
|
return fragments.map((s, i) => (i < fragments.length - 1 ? `${s}\${${i}}` : s)).join("");
|
|
216
246
|
}
|
|
247
|
+
|
|
248
|
+
/** Convert a code-point span (as every AST node carries) to UTF-16 code-unit
|
|
249
|
+
* offsets over `source` — the units a JavaScript string, a DOM range or an
|
|
250
|
+
* editor API counts in. `source` is the raw source the span was measured over
|
|
251
|
+
* (for a template, `rawSource(strings)`). Offsets past the end clamp. */
|
|
252
|
+
export function toUtf16(span: Span, source: string): Span {
|
|
253
|
+
const [start, end] = span;
|
|
254
|
+
let cp = 0;
|
|
255
|
+
let unit = 0;
|
|
256
|
+
let startUnit = -1;
|
|
257
|
+
let endUnit = -1;
|
|
258
|
+
for (const ch of source) {
|
|
259
|
+
if (cp === start) startUnit = unit;
|
|
260
|
+
if (cp === end) endUnit = unit;
|
|
261
|
+
cp++;
|
|
262
|
+
unit += ch.length;
|
|
263
|
+
}
|
|
264
|
+
if (startUnit < 0) startUnit = unit;
|
|
265
|
+
if (endUnit < 0) endUnit = unit;
|
|
266
|
+
return [startUnit, endUnit];
|
|
267
|
+
}
|
package/src/optimize/analysis.ts
CHANGED
|
@@ -29,9 +29,9 @@ export function exprReadsScopeIn(e: Expr, at: number, upTo: number): boolean {
|
|
|
29
29
|
case "ident": return at >= 1 && at <= upTo;
|
|
30
30
|
case "outer": { const d = at - e.levels; return d >= 1 && d <= upTo; }
|
|
31
31
|
case "member": return exprReadsScopeIn(e.recv, at, upTo);
|
|
32
|
-
case "index": return exprReadsScopeIn(e.recv, at, upTo) || exprReadsScopeIn(e.index, at, upTo);
|
|
33
32
|
case "call": return (e.recv !== null && exprReadsScopeIn(e.recv, at, upTo)) || e.args.some((a) => exprReadsScopeIn(a, at, upTo));
|
|
34
|
-
case "unary": return exprReadsScopeIn(e.expr, at, upTo);
|
|
33
|
+
case "unary": case "required": return exprReadsScopeIn(e.expr, at, upTo);
|
|
34
|
+
case "op": return opReadsScopeIn(e, at, upTo);
|
|
35
35
|
case "binary": case "logical": case "in": return exprReadsScopeIn(e.left, at, upTo) || exprReadsScopeIn(e.right, at, upTo);
|
|
36
36
|
case "range": return (e.lo !== null && exprReadsScopeIn(e.lo, at, upTo)) || (e.hi !== null && exprReadsScopeIn(e.hi, at, upTo));
|
|
37
37
|
}
|
|
@@ -94,8 +94,9 @@ export function exprRaiseFree(e: Expr, ctx: RuleContext): boolean {
|
|
|
94
94
|
case "lit": case "ident": case "outer": return true;
|
|
95
95
|
case "binding": return e.index < ctx.bindingCount;
|
|
96
96
|
case "call": return false;
|
|
97
|
+
case "required": return false; // raises when absent
|
|
98
|
+
case "op": return opRaiseFree(e, ctx); // a value-position directive projects: never raise-free
|
|
97
99
|
case "member": return exprRaiseFree(e.recv, ctx);
|
|
98
|
-
case "index": return exprRaiseFree(e.recv, ctx) && exprRaiseFree(e.index, ctx);
|
|
99
100
|
case "unary": return exprRaiseFree(e.expr, ctx);
|
|
100
101
|
case "binary": case "logical": case "in": return exprRaiseFree(e.left, ctx) && exprRaiseFree(e.right, ctx);
|
|
101
102
|
case "range": return (e.lo === null || exprRaiseFree(e.lo, ctx)) && (e.hi === null || exprRaiseFree(e.hi, ctx));
|
package/src/optimize/ir.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface BlockPlan {
|
|
|
43
43
|
readonly projection: { readonly select: readonly SelectItem[]; readonly values: boolean };
|
|
44
44
|
readonly orderBy: readonly OrderSpec[] | null;
|
|
45
45
|
readonly distinct: boolean;
|
|
46
|
-
readonly bound: { readonly limit: Expr |
|
|
46
|
+
readonly bound: { readonly limit: Expr | null; readonly offset: Expr | null };
|
|
47
47
|
readonly follow: Follow | null;
|
|
48
48
|
/** Equalities answered by an index probe on the receiver (in conjunct order). */
|
|
49
49
|
readonly correlated: readonly Correlation[];
|
|
@@ -85,9 +85,9 @@ export function logicalBlock(node: OpNode, depth: number): BlockPlan {
|
|
|
85
85
|
receiver: node.receiver,
|
|
86
86
|
from: sub.from,
|
|
87
87
|
where: sub.where,
|
|
88
|
-
projection: { select: sub.select, values: sub.values
|
|
88
|
+
projection: { select: sub.select, values: sub.values },
|
|
89
89
|
orderBy: sub.orderBy,
|
|
90
|
-
distinct: node.distinct
|
|
90
|
+
distinct: node.distinct,
|
|
91
91
|
bound: { limit: sub.limit, offset: sub.offset },
|
|
92
92
|
follow: sub.follow,
|
|
93
93
|
correlated: [],
|
|
@@ -100,7 +100,7 @@ export function logicalBlock(node: OpNode, depth: number): BlockPlan {
|
|
|
100
100
|
|
|
101
101
|
/** Rebuild a conjunction from its remaining parts. */
|
|
102
102
|
export function conjunction(parts: readonly Where[]): Where | null {
|
|
103
|
-
return parts.length === 0 ? null : parts.length === 1 ? parts[0]! : { kind: "and", parts: [...parts] };
|
|
103
|
+
return parts.length === 0 ? null : parts.length === 1 ? parts[0]! : { kind: "and", span: [0, 0], parts: [...parts] };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/** The top-level `&&` conjuncts of a where tree (a non-`and` tree is one). */
|