arrowbase 0.1.1
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 +27 -0
- package/COMPATIBILITY.md +398 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +238 -0
- package/README.md +897 -0
- package/RELEASE_NOTES.md +68 -0
- package/dist/arrow.d.ts +93 -0
- package/dist/arrow.js +5366 -0
- package/dist/arrow.js.map +1 -0
- package/dist/broadcast.d.ts +173 -0
- package/dist/broadcast.js +319 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/collection-DGlKgOGi.d.ts +1932 -0
- package/dist/idb.d.ts +146 -0
- package/dist/idb.js +6193 -0
- package/dist/idb.js.map +1 -0
- package/dist/index.d.ts +1802 -0
- package/dist/index.js +13924 -0
- package/dist/index.js.map +1 -0
- package/dist/query-CzV9E57Y.d.ts +279 -0
- package/dist/react.d.ts +51 -0
- package/dist/react.js +1633 -0
- package/dist/react.js.map +1 -0
- package/dist/tanstack.d.ts +51 -0
- package/dist/tanstack.js +191 -0
- package/dist/tanstack.js.map +1 -0
- package/dist/types-CSoU6uED.d.ts +50 -0
- package/package.json +139 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,1633 @@
|
|
|
1
|
+
import { useMemo, useCallback, useSyncExternalStore, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/react.ts
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var ArrowBaseError = class extends Error {
|
|
7
|
+
constructor(message, code) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "ArrowBaseError";
|
|
11
|
+
}
|
|
12
|
+
code;
|
|
13
|
+
};
|
|
14
|
+
var ValidationError = class extends ArrowBaseError {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message, "E_VALIDATION");
|
|
17
|
+
this.name = "ValidationError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/query/trackRow.ts
|
|
22
|
+
function trackRow(row, deps) {
|
|
23
|
+
return new Proxy(row, {
|
|
24
|
+
get(target, prop, receiver) {
|
|
25
|
+
if (typeof prop === "string") deps.add(prop);
|
|
26
|
+
return Reflect.get(target, prop, receiver);
|
|
27
|
+
},
|
|
28
|
+
// The tracking proxy is intentionally transparent for other traps so
|
|
29
|
+
// predicates that destructure or use `in` still work as expected.
|
|
30
|
+
has(target, prop) {
|
|
31
|
+
if (typeof prop === "string") deps.add(prop);
|
|
32
|
+
return Reflect.has(target, prop);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/query/filter.ts
|
|
38
|
+
function collectColumns(expr, out) {
|
|
39
|
+
switch (expr.kind) {
|
|
40
|
+
case "col":
|
|
41
|
+
out.add(expr.name);
|
|
42
|
+
return;
|
|
43
|
+
case "cmp":
|
|
44
|
+
if (expr.left.kind === "col") out.add(expr.left.name);
|
|
45
|
+
if (expr.right.kind === "col") out.add(expr.right.name);
|
|
46
|
+
return;
|
|
47
|
+
case "and":
|
|
48
|
+
case "or":
|
|
49
|
+
for (const a of expr.args) collectColumns(a, out);
|
|
50
|
+
return;
|
|
51
|
+
case "not":
|
|
52
|
+
collectColumns(expr.arg, out);
|
|
53
|
+
return;
|
|
54
|
+
case "isNull":
|
|
55
|
+
out.add(expr.col.name);
|
|
56
|
+
return;
|
|
57
|
+
case "in":
|
|
58
|
+
out.add(expr.col.name);
|
|
59
|
+
return;
|
|
60
|
+
case "str":
|
|
61
|
+
out.add(expr.col.name);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/schema/types.ts
|
|
67
|
+
var ColumnTypeCode = {
|
|
68
|
+
Bool: 1,
|
|
69
|
+
Int8: 2,
|
|
70
|
+
Int16: 3,
|
|
71
|
+
Int32: 4,
|
|
72
|
+
Int64: 5,
|
|
73
|
+
UInt8: 6,
|
|
74
|
+
UInt16: 7,
|
|
75
|
+
UInt32: 8,
|
|
76
|
+
UInt64: 9,
|
|
77
|
+
Float32: 10,
|
|
78
|
+
Float64: 11,
|
|
79
|
+
Utf8: 12,
|
|
80
|
+
DictUtf8: 13};
|
|
81
|
+
|
|
82
|
+
// src/query/kernel.ts
|
|
83
|
+
function isValid(validity, row) {
|
|
84
|
+
if (!validity) return true;
|
|
85
|
+
const byte = validity[row >>> 3] ?? 0;
|
|
86
|
+
return (byte & 1 << (row & 7)) !== 0;
|
|
87
|
+
}
|
|
88
|
+
var FilterCompileError = class extends Error {
|
|
89
|
+
};
|
|
90
|
+
function compileFilter(expr, scanByName) {
|
|
91
|
+
if (!expr) {
|
|
92
|
+
return { test: () => true, columns: /* @__PURE__ */ new Set() };
|
|
93
|
+
}
|
|
94
|
+
const cols = /* @__PURE__ */ new Set();
|
|
95
|
+
const test = compileExpr(expr, scanByName, cols);
|
|
96
|
+
return { test, columns: cols };
|
|
97
|
+
}
|
|
98
|
+
function compileExpr(expr, scanByName, cols) {
|
|
99
|
+
switch (expr.kind) {
|
|
100
|
+
case "col": {
|
|
101
|
+
const sc = requireCol(expr, scanByName, cols);
|
|
102
|
+
if (sc.typeCode !== ColumnTypeCode.Bool) {
|
|
103
|
+
throw new FilterCompileError(
|
|
104
|
+
`column ref "${expr.name}" in boolean position must be a bool column`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const values = sc.values;
|
|
108
|
+
const validity = sc.validity;
|
|
109
|
+
return (row) => {
|
|
110
|
+
if (!isValid(validity, row)) return false;
|
|
111
|
+
return (values[row] ?? 0) !== 0;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case "and": {
|
|
115
|
+
const children = expr.args.map((a) => compileExpr(a, scanByName, cols));
|
|
116
|
+
if (children.length === 0) return () => true;
|
|
117
|
+
return (row) => {
|
|
118
|
+
for (let i = 0; i < children.length; i++) {
|
|
119
|
+
if (!children[i](row)) return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
case "or": {
|
|
125
|
+
const children = expr.args.map((a) => compileExpr(a, scanByName, cols));
|
|
126
|
+
if (children.length === 0) return () => false;
|
|
127
|
+
return (row) => {
|
|
128
|
+
for (let i = 0; i < children.length; i++) {
|
|
129
|
+
if (children[i](row)) return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case "not": {
|
|
135
|
+
const inner = compileExpr(expr.arg, scanByName, cols);
|
|
136
|
+
return (row) => !inner(row);
|
|
137
|
+
}
|
|
138
|
+
case "isNull": {
|
|
139
|
+
const sc = requireCol(expr.col, scanByName, cols);
|
|
140
|
+
const validity = sc.validity;
|
|
141
|
+
if (!validity) {
|
|
142
|
+
return expr.negated ? () => true : () => false;
|
|
143
|
+
}
|
|
144
|
+
if (expr.negated) return (row) => isValid(validity, row);
|
|
145
|
+
return (row) => !isValid(validity, row);
|
|
146
|
+
}
|
|
147
|
+
case "in": {
|
|
148
|
+
const tests = expr.values.map(
|
|
149
|
+
(value) => compileCmp(
|
|
150
|
+
"eq",
|
|
151
|
+
expr.col,
|
|
152
|
+
{ kind: "lit", value },
|
|
153
|
+
scanByName,
|
|
154
|
+
cols
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
return (row) => {
|
|
158
|
+
for (const test of tests) if (test(row)) return true;
|
|
159
|
+
return false;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
case "cmp":
|
|
163
|
+
return compileCmp(expr.op, expr.left, expr.right, scanByName, cols);
|
|
164
|
+
case "str":
|
|
165
|
+
return compileString(expr, scanByName, cols);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function requireCol(ref, scanByName, cols) {
|
|
169
|
+
const sc = scanByName.get(ref.name);
|
|
170
|
+
if (!sc) throw new FilterCompileError(`unknown column "${ref.name}"`);
|
|
171
|
+
cols.add(ref.name);
|
|
172
|
+
return sc;
|
|
173
|
+
}
|
|
174
|
+
function compileCmp(op, left, right, scanByName, cols) {
|
|
175
|
+
let ref;
|
|
176
|
+
let literal;
|
|
177
|
+
let flipped;
|
|
178
|
+
if (left.kind === "col" && right.kind === "lit") {
|
|
179
|
+
ref = left;
|
|
180
|
+
literal = right.value;
|
|
181
|
+
flipped = false;
|
|
182
|
+
} else if (left.kind === "lit" && right.kind === "col") {
|
|
183
|
+
ref = right;
|
|
184
|
+
literal = left.value;
|
|
185
|
+
flipped = true;
|
|
186
|
+
} else {
|
|
187
|
+
throw new FilterCompileError(
|
|
188
|
+
"column-column and literal-literal comparisons are not supported in filter DSL"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const effective = flipped ? flipCmp(op) : op;
|
|
192
|
+
return buildCmp(effective, requireCol(ref, scanByName, cols), literal);
|
|
193
|
+
}
|
|
194
|
+
function flipCmp(op) {
|
|
195
|
+
switch (op) {
|
|
196
|
+
case "eq":
|
|
197
|
+
case "neq":
|
|
198
|
+
return op;
|
|
199
|
+
case "gt":
|
|
200
|
+
return "lt";
|
|
201
|
+
case "gte":
|
|
202
|
+
return "lte";
|
|
203
|
+
case "lt":
|
|
204
|
+
return "gt";
|
|
205
|
+
case "lte":
|
|
206
|
+
return "gte";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function buildCmp(op, sc, literal) {
|
|
210
|
+
const code = sc.typeCode;
|
|
211
|
+
const validity = sc.validity;
|
|
212
|
+
if (isNumericTypeCode(code)) {
|
|
213
|
+
const values = sc.values;
|
|
214
|
+
if (literal === null) {
|
|
215
|
+
if (op === "eq") {
|
|
216
|
+
if (!validity) return () => false;
|
|
217
|
+
return (row) => !isValid(validity, row);
|
|
218
|
+
}
|
|
219
|
+
if (op === "neq") {
|
|
220
|
+
if (!validity) return () => true;
|
|
221
|
+
return (row) => isValid(validity, row);
|
|
222
|
+
}
|
|
223
|
+
return () => false;
|
|
224
|
+
}
|
|
225
|
+
if (typeof literal !== "number") {
|
|
226
|
+
throw new FilterCompileError(
|
|
227
|
+
`column "${sc.name}" expects numeric literal, got ${typeof literal}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const lit = literal;
|
|
231
|
+
const cmp = numericCmp(op);
|
|
232
|
+
if (!validity) return (row) => cmp(values[row] ?? 0, lit);
|
|
233
|
+
return (row) => isValid(validity, row) && cmp(values[row] ?? 0, lit);
|
|
234
|
+
}
|
|
235
|
+
if (code === ColumnTypeCode.Bool) {
|
|
236
|
+
const values = sc.values;
|
|
237
|
+
if (literal === null) {
|
|
238
|
+
if (op === "eq") {
|
|
239
|
+
if (!validity) return () => false;
|
|
240
|
+
return (row) => !isValid(validity, row);
|
|
241
|
+
}
|
|
242
|
+
if (op === "neq") return validity ? (row) => isValid(validity, row) : () => true;
|
|
243
|
+
return () => false;
|
|
244
|
+
}
|
|
245
|
+
if (typeof literal !== "boolean") {
|
|
246
|
+
throw new FilterCompileError(
|
|
247
|
+
`column "${sc.name}" expects boolean literal, got ${typeof literal}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const litByte = literal ? 1 : 0;
|
|
251
|
+
if (op === "eq") {
|
|
252
|
+
if (!validity) return (row) => (values[row] ?? 0) === litByte;
|
|
253
|
+
return (row) => isValid(validity, row) && (values[row] ?? 0) === litByte;
|
|
254
|
+
}
|
|
255
|
+
if (op === "neq") {
|
|
256
|
+
if (!validity) return (row) => (values[row] ?? 0) !== litByte;
|
|
257
|
+
return (row) => isValid(validity, row) && (values[row] ?? 0) !== litByte;
|
|
258
|
+
}
|
|
259
|
+
throw new FilterCompileError(
|
|
260
|
+
`bool columns only support eq/neq (got ${op})`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (code === ColumnTypeCode.Int64 || code === ColumnTypeCode.UInt64) {
|
|
264
|
+
const values = sc.values;
|
|
265
|
+
if (literal === null) {
|
|
266
|
+
if (op === "eq") return validity ? (row) => !isValid(validity, row) : () => false;
|
|
267
|
+
if (op === "neq") return validity ? (row) => isValid(validity, row) : () => true;
|
|
268
|
+
return () => false;
|
|
269
|
+
}
|
|
270
|
+
if (typeof literal !== "bigint") {
|
|
271
|
+
if (typeof literal === "number" && Number.isInteger(literal)) {
|
|
272
|
+
literal = BigInt(literal);
|
|
273
|
+
} else {
|
|
274
|
+
throw new FilterCompileError(
|
|
275
|
+
`column "${sc.name}" expects bigint literal, got ${typeof literal}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const lit = literal;
|
|
280
|
+
const cmp = bigintCmp(op);
|
|
281
|
+
if (!validity) return (row) => cmp(values[row], lit);
|
|
282
|
+
return (row) => isValid(validity, row) && cmp(values[row], lit);
|
|
283
|
+
}
|
|
284
|
+
if (code === ColumnTypeCode.DictUtf8) {
|
|
285
|
+
const dict = sc.dictionary;
|
|
286
|
+
const codes = sc.values;
|
|
287
|
+
if (literal === null) {
|
|
288
|
+
if (op === "eq") return validity ? (row) => !isValid(validity, row) : () => false;
|
|
289
|
+
if (op === "neq") return validity ? (row) => isValid(validity, row) : () => true;
|
|
290
|
+
return () => false;
|
|
291
|
+
}
|
|
292
|
+
if (typeof literal !== "string") {
|
|
293
|
+
throw new FilterCompileError(
|
|
294
|
+
`column "${sc.name}" expects string literal, got ${typeof literal}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const matchCode = dict.lookup(literal);
|
|
298
|
+
if (op === "eq") {
|
|
299
|
+
if (matchCode === void 0) {
|
|
300
|
+
return () => false;
|
|
301
|
+
}
|
|
302
|
+
if (!validity) return (row) => codes[row] === matchCode;
|
|
303
|
+
return (row) => isValid(validity, row) && codes[row] === matchCode;
|
|
304
|
+
}
|
|
305
|
+
if (op === "neq") {
|
|
306
|
+
if (matchCode === void 0) {
|
|
307
|
+
if (!validity) return () => true;
|
|
308
|
+
return (row) => isValid(validity, row);
|
|
309
|
+
}
|
|
310
|
+
if (!validity) return (row) => codes[row] !== matchCode;
|
|
311
|
+
return (row) => isValid(validity, row) && codes[row] !== matchCode;
|
|
312
|
+
}
|
|
313
|
+
throw new FilterCompileError(
|
|
314
|
+
`dict_utf8 columns only support eq/neq (got ${op})`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (code === ColumnTypeCode.Utf8) {
|
|
318
|
+
const ranges = sc.values;
|
|
319
|
+
const heap = sc.heap;
|
|
320
|
+
if (literal === null) {
|
|
321
|
+
if (op === "eq") return validity ? (row) => !isValid(validity, row) : () => false;
|
|
322
|
+
if (op === "neq") return validity ? (row) => isValid(validity, row) : () => true;
|
|
323
|
+
return () => false;
|
|
324
|
+
}
|
|
325
|
+
if (typeof literal !== "string") {
|
|
326
|
+
throw new FilterCompileError(
|
|
327
|
+
`column "${sc.name}" expects string literal, got ${typeof literal}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const lit = literal;
|
|
331
|
+
const readString = (row) => {
|
|
332
|
+
const start = ranges[row * 2] ?? 0;
|
|
333
|
+
const end = ranges[row * 2 + 1] ?? 0;
|
|
334
|
+
return heap.read(start, end);
|
|
335
|
+
};
|
|
336
|
+
if (op === "eq") {
|
|
337
|
+
if (!validity) return (row) => readString(row) === lit;
|
|
338
|
+
return (row) => isValid(validity, row) && readString(row) === lit;
|
|
339
|
+
}
|
|
340
|
+
if (op === "neq") {
|
|
341
|
+
if (!validity) return (row) => readString(row) !== lit;
|
|
342
|
+
return (row) => isValid(validity, row) && readString(row) !== lit;
|
|
343
|
+
}
|
|
344
|
+
throw new FilterCompileError(
|
|
345
|
+
`utf8 columns only support eq/neq (got ${op})`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
throw new FilterCompileError(
|
|
349
|
+
`filter DSL does not support column type code ${code}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
function compileString(expr, scanByName, cols) {
|
|
353
|
+
const sc = requireCol(expr.col, scanByName, cols);
|
|
354
|
+
const code = sc.typeCode;
|
|
355
|
+
if (code !== ColumnTypeCode.Utf8 && code !== ColumnTypeCode.DictUtf8) {
|
|
356
|
+
throw new FilterCompileError(
|
|
357
|
+
`string predicates require a utf8 or dict_utf8 column; "${sc.name}" is not a string column`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const validity = sc.validity;
|
|
361
|
+
const negated = expr.negated === true;
|
|
362
|
+
const stringPredicate = buildStringPredicate(expr);
|
|
363
|
+
if (code === ColumnTypeCode.DictUtf8) {
|
|
364
|
+
const dict = sc.dictionary;
|
|
365
|
+
const codes = sc.values;
|
|
366
|
+
const matchSet = /* @__PURE__ */ new Set();
|
|
367
|
+
for (const [codeVal, str] of dict.entries()) {
|
|
368
|
+
if (stringPredicate(str)) matchSet.add(codeVal);
|
|
369
|
+
}
|
|
370
|
+
if (matchSet.size === 0) {
|
|
371
|
+
const constResult = negated;
|
|
372
|
+
if (!validity) return () => constResult;
|
|
373
|
+
return (row) => isValid(validity, row) ? constResult : false;
|
|
374
|
+
}
|
|
375
|
+
if (!validity) {
|
|
376
|
+
if (!negated) return (row) => matchSet.has(codes[row]);
|
|
377
|
+
return (row) => !matchSet.has(codes[row]);
|
|
378
|
+
}
|
|
379
|
+
if (!negated) {
|
|
380
|
+
return (row) => isValid(validity, row) && matchSet.has(codes[row]);
|
|
381
|
+
}
|
|
382
|
+
return (row) => isValid(validity, row) && !matchSet.has(codes[row]);
|
|
383
|
+
}
|
|
384
|
+
const ranges = sc.values;
|
|
385
|
+
const heap = sc.heap;
|
|
386
|
+
const readString = (row) => {
|
|
387
|
+
const start = ranges[row * 2] ?? 0;
|
|
388
|
+
const end = ranges[row * 2 + 1] ?? 0;
|
|
389
|
+
return heap.read(start, end);
|
|
390
|
+
};
|
|
391
|
+
if (!validity) {
|
|
392
|
+
if (!negated) return (row) => stringPredicate(readString(row));
|
|
393
|
+
return (row) => !stringPredicate(readString(row));
|
|
394
|
+
}
|
|
395
|
+
if (!negated) {
|
|
396
|
+
return (row) => isValid(validity, row) && stringPredicate(readString(row));
|
|
397
|
+
}
|
|
398
|
+
return (row) => isValid(validity, row) && !stringPredicate(readString(row));
|
|
399
|
+
}
|
|
400
|
+
function buildStringPredicate(expr) {
|
|
401
|
+
switch (expr.op) {
|
|
402
|
+
case "contains":
|
|
403
|
+
return compileSubstring(expr.value, expr.caseInsensitive ?? false, "contains");
|
|
404
|
+
case "startsWith":
|
|
405
|
+
return compileSubstring(expr.value, expr.caseInsensitive ?? false, "startsWith");
|
|
406
|
+
case "endsWith":
|
|
407
|
+
return compileSubstring(expr.value, expr.caseInsensitive ?? false, "endsWith");
|
|
408
|
+
case "like":
|
|
409
|
+
return compileLike(expr.value, expr.caseInsensitive ?? false);
|
|
410
|
+
case "matches":
|
|
411
|
+
return compileRegex(expr.value, expr.flags);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function compileSubstring(needle, caseInsensitive, mode) {
|
|
415
|
+
if (caseInsensitive) {
|
|
416
|
+
const needleL = asciiToLower(needle);
|
|
417
|
+
switch (mode) {
|
|
418
|
+
case "contains":
|
|
419
|
+
return (s) => asciiToLower(s).includes(needleL);
|
|
420
|
+
case "startsWith":
|
|
421
|
+
return (s) => asciiToLower(s).startsWith(needleL);
|
|
422
|
+
case "endsWith":
|
|
423
|
+
return (s) => asciiToLower(s).endsWith(needleL);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
switch (mode) {
|
|
427
|
+
case "contains":
|
|
428
|
+
return (s) => s.includes(needle);
|
|
429
|
+
case "startsWith":
|
|
430
|
+
return (s) => s.startsWith(needle);
|
|
431
|
+
case "endsWith":
|
|
432
|
+
return (s) => s.endsWith(needle);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function compileLike(pattern, caseInsensitive) {
|
|
436
|
+
const simple = parseSimpleLike(pattern);
|
|
437
|
+
if (simple !== void 0) {
|
|
438
|
+
const { literal, leadingWildcard, trailingWildcard } = simple;
|
|
439
|
+
if (leadingWildcard && trailingWildcard) {
|
|
440
|
+
return compileSubstring(literal, caseInsensitive, "contains");
|
|
441
|
+
}
|
|
442
|
+
if (leadingWildcard) return compileSubstring(literal, caseInsensitive, "endsWith");
|
|
443
|
+
if (trailingWildcard) return compileSubstring(literal, caseInsensitive, "startsWith");
|
|
444
|
+
if (caseInsensitive) {
|
|
445
|
+
const expected = asciiToLower(literal);
|
|
446
|
+
return (s) => asciiToLower(s) === expected;
|
|
447
|
+
}
|
|
448
|
+
return (s) => s === literal;
|
|
449
|
+
}
|
|
450
|
+
let src = "^";
|
|
451
|
+
let i = 0;
|
|
452
|
+
while (i < pattern.length) {
|
|
453
|
+
const c = pattern[i];
|
|
454
|
+
if (c === "\\" && i + 1 < pattern.length) {
|
|
455
|
+
const next = pattern[i + 1];
|
|
456
|
+
if (next === "%" || next === "_" || next === "\\") {
|
|
457
|
+
src += escapeRegexChar(next);
|
|
458
|
+
i += 2;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (c === "%") {
|
|
463
|
+
src += ".*";
|
|
464
|
+
} else if (c === "_") {
|
|
465
|
+
src += ".";
|
|
466
|
+
} else {
|
|
467
|
+
src += escapeRegexChar(c);
|
|
468
|
+
}
|
|
469
|
+
i++;
|
|
470
|
+
}
|
|
471
|
+
src += "$";
|
|
472
|
+
const re = new RegExp(src, caseInsensitive ? "is" : "s");
|
|
473
|
+
return (s) => re.test(s);
|
|
474
|
+
}
|
|
475
|
+
function parseSimpleLike(pattern) {
|
|
476
|
+
let literal = "";
|
|
477
|
+
let leadingWildcard = false;
|
|
478
|
+
let pendingWildcard = false;
|
|
479
|
+
let sawLiteral = false;
|
|
480
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
481
|
+
const char = pattern[i];
|
|
482
|
+
if (char === "\\" && i + 1 < pattern.length) {
|
|
483
|
+
const next = pattern[i + 1];
|
|
484
|
+
if (next === "%" || next === "_" || next === "\\") {
|
|
485
|
+
if (pendingWildcard && sawLiteral) return void 0;
|
|
486
|
+
pendingWildcard = false;
|
|
487
|
+
literal += next;
|
|
488
|
+
sawLiteral = true;
|
|
489
|
+
i++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (char === "_") return void 0;
|
|
494
|
+
if (char === "%") {
|
|
495
|
+
if (sawLiteral) pendingWildcard = true;
|
|
496
|
+
else leadingWildcard = true;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (pendingWildcard && sawLiteral) return void 0;
|
|
500
|
+
pendingWildcard = false;
|
|
501
|
+
literal += char;
|
|
502
|
+
sawLiteral = true;
|
|
503
|
+
}
|
|
504
|
+
return { literal, leadingWildcard, trailingWildcard: pendingWildcard };
|
|
505
|
+
}
|
|
506
|
+
function compileRegex(source, flags) {
|
|
507
|
+
let re;
|
|
508
|
+
try {
|
|
509
|
+
re = new RegExp(source, flags);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
throw new FilterCompileError(
|
|
512
|
+
`invalid regex "${source}"${flags ? ` (flags "${flags}")` : ""}: ${err instanceof Error ? err.message : String(err)}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return (s) => re.test(s);
|
|
516
|
+
}
|
|
517
|
+
function escapeRegexChar(c) {
|
|
518
|
+
if ("^$.|?*+()[]{}\\/".includes(c)) return "\\" + c;
|
|
519
|
+
return c;
|
|
520
|
+
}
|
|
521
|
+
function asciiToLower(s) {
|
|
522
|
+
let out = "";
|
|
523
|
+
let changed = false;
|
|
524
|
+
for (let i = 0; i < s.length; i++) {
|
|
525
|
+
const c = s.charCodeAt(i);
|
|
526
|
+
if (c >= 65 && c <= 90) {
|
|
527
|
+
if (!changed) {
|
|
528
|
+
out = s.slice(0, i);
|
|
529
|
+
changed = true;
|
|
530
|
+
}
|
|
531
|
+
out += String.fromCharCode(c + 32);
|
|
532
|
+
} else if (changed) {
|
|
533
|
+
out += s[i];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return changed ? out : s;
|
|
537
|
+
}
|
|
538
|
+
function isNumericTypeCode(code) {
|
|
539
|
+
switch (code) {
|
|
540
|
+
case ColumnTypeCode.Int8:
|
|
541
|
+
case ColumnTypeCode.Int16:
|
|
542
|
+
case ColumnTypeCode.Int32:
|
|
543
|
+
case ColumnTypeCode.UInt8:
|
|
544
|
+
case ColumnTypeCode.UInt16:
|
|
545
|
+
case ColumnTypeCode.UInt32:
|
|
546
|
+
case ColumnTypeCode.Float32:
|
|
547
|
+
case ColumnTypeCode.Float64:
|
|
548
|
+
return true;
|
|
549
|
+
default:
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function numericCmp(op) {
|
|
554
|
+
switch (op) {
|
|
555
|
+
case "eq":
|
|
556
|
+
return (a, b) => a === b;
|
|
557
|
+
case "neq":
|
|
558
|
+
return (a, b) => a !== b;
|
|
559
|
+
case "gt":
|
|
560
|
+
return (a, b) => a > b;
|
|
561
|
+
case "gte":
|
|
562
|
+
return (a, b) => a >= b;
|
|
563
|
+
case "lt":
|
|
564
|
+
return (a, b) => a < b;
|
|
565
|
+
case "lte":
|
|
566
|
+
return (a, b) => a <= b;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function bigintCmp(op) {
|
|
570
|
+
switch (op) {
|
|
571
|
+
case "eq":
|
|
572
|
+
return (a, b) => a === b;
|
|
573
|
+
case "neq":
|
|
574
|
+
return (a, b) => a !== b;
|
|
575
|
+
case "gt":
|
|
576
|
+
return (a, b) => a > b;
|
|
577
|
+
case "gte":
|
|
578
|
+
return (a, b) => a >= b;
|
|
579
|
+
case "lt":
|
|
580
|
+
return (a, b) => a < b;
|
|
581
|
+
case "lte":
|
|
582
|
+
return (a, b) => a <= b;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function scanOrdinals(liveOrdinals, test) {
|
|
586
|
+
const buf = [];
|
|
587
|
+
for (const r of liveOrdinals) {
|
|
588
|
+
if (test(r)) buf.push(r);
|
|
589
|
+
}
|
|
590
|
+
const out = new Uint32Array(buf.length);
|
|
591
|
+
for (let i = 0; i < buf.length; i++) out[i] = buf[i];
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
function sortOrdinals(ordinals, order, scanByName, readCell) {
|
|
595
|
+
if (order.length === 0) return ordinals;
|
|
596
|
+
if (order.length === 1) {
|
|
597
|
+
const spec = order[0];
|
|
598
|
+
const sc = scanByName.get(spec.column);
|
|
599
|
+
if (sc && isNumericTypeCode(sc.typeCode) && !sc.nullable) {
|
|
600
|
+
const values = sc.values;
|
|
601
|
+
const n = ordinals.length;
|
|
602
|
+
const keys = new Float64Array(n);
|
|
603
|
+
for (let i = 0; i < n; i++) keys[i] = values[ordinals[i]] ?? 0;
|
|
604
|
+
const idx = new Uint32Array(n);
|
|
605
|
+
for (let i = 0; i < n; i++) idx[i] = i;
|
|
606
|
+
const asc = spec.dir === "asc";
|
|
607
|
+
const arr2 = Array.from(idx);
|
|
608
|
+
arr2.sort((a, b) => {
|
|
609
|
+
const av = keys[a];
|
|
610
|
+
const bv = keys[b];
|
|
611
|
+
if (av === bv) return a - b;
|
|
612
|
+
return asc ? av < bv ? -1 : 1 : av < bv ? 1 : -1;
|
|
613
|
+
});
|
|
614
|
+
const out2 = new Uint32Array(n);
|
|
615
|
+
for (let i = 0; i < n; i++) out2[i] = ordinals[arr2[i]];
|
|
616
|
+
return out2;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const arr = Array.from(ordinals);
|
|
620
|
+
arr.sort((ra, rb) => {
|
|
621
|
+
for (const spec of order) {
|
|
622
|
+
const av = readCell(ra, spec.column);
|
|
623
|
+
const bv = readCell(rb, spec.column);
|
|
624
|
+
const c = compareScalars(av, bv);
|
|
625
|
+
if (c !== 0) return spec.dir === "asc" ? c : -c;
|
|
626
|
+
}
|
|
627
|
+
return ra - rb;
|
|
628
|
+
});
|
|
629
|
+
const out = new Uint32Array(arr.length);
|
|
630
|
+
for (let i = 0; i < arr.length; i++) out[i] = arr[i];
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
function compareScalars(a, b) {
|
|
634
|
+
if (a === b) return 0;
|
|
635
|
+
if (a === null || a === void 0) return -1;
|
|
636
|
+
if (b === null || b === void 0) return 1;
|
|
637
|
+
if (typeof a === "bigint" && typeof b === "bigint") return a < b ? -1 : a > b ? 1 : 0;
|
|
638
|
+
if (typeof a === "number" && typeof b === "number") return a < b ? -1 : a > b ? 1 : 0;
|
|
639
|
+
if (typeof a === "boolean" && typeof b === "boolean") return (a ? 1 : 0) - (b ? 1 : 0);
|
|
640
|
+
const sa = String(a);
|
|
641
|
+
const sb = String(b);
|
|
642
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
643
|
+
}
|
|
644
|
+
function materialize(collection, ordinals, offset, limit, selection) {
|
|
645
|
+
const end = limit === null ? ordinals.length : Math.min(ordinals.length, offset + limit);
|
|
646
|
+
const n = Math.max(0, end - offset);
|
|
647
|
+
const out = new Array(n);
|
|
648
|
+
const cols = selection ?? collection.columnNames();
|
|
649
|
+
for (let i = 0; i < n; i++) {
|
|
650
|
+
const row = ordinals[offset + i];
|
|
651
|
+
const obj = {};
|
|
652
|
+
for (let c = 0; c < cols.length; c++) {
|
|
653
|
+
const name = cols[c];
|
|
654
|
+
obj[name] = collection._internalReadCell(name, row);
|
|
655
|
+
}
|
|
656
|
+
out[i] = obj;
|
|
657
|
+
}
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/query/fnCompile.ts
|
|
662
|
+
function tryCompileFnToFilterExpr(fn, options = {}) {
|
|
663
|
+
try {
|
|
664
|
+
const source = fn.toString();
|
|
665
|
+
const tokens = tokenize(source);
|
|
666
|
+
const parser = new FnParser(
|
|
667
|
+
tokens,
|
|
668
|
+
source,
|
|
669
|
+
options.knownColumns ?? null,
|
|
670
|
+
options.namespaceAlias ?? null
|
|
671
|
+
);
|
|
672
|
+
return parser.parseTopLevel();
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
var MULTI_OPS = [
|
|
678
|
+
"===",
|
|
679
|
+
"!==",
|
|
680
|
+
"==",
|
|
681
|
+
"!=",
|
|
682
|
+
">=",
|
|
683
|
+
"<=",
|
|
684
|
+
"&&",
|
|
685
|
+
"||",
|
|
686
|
+
"=>"
|
|
687
|
+
];
|
|
688
|
+
var SINGLE_OPS = /* @__PURE__ */ new Set(["!", ">", "<", "-"]);
|
|
689
|
+
var PUNCT = /* @__PURE__ */ new Set(["(", ")", "{", "}", "[", "]", ",", ";", "."]);
|
|
690
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["true", "false", "null", "undefined", "return"]);
|
|
691
|
+
function tokenize(src) {
|
|
692
|
+
const tokens = [];
|
|
693
|
+
let i = 0;
|
|
694
|
+
const n = src.length;
|
|
695
|
+
while (i < n) {
|
|
696
|
+
const ch = src[i];
|
|
697
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
698
|
+
i++;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
702
|
+
while (i < n && src[i] !== "\n") i++;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
706
|
+
i += 2;
|
|
707
|
+
while (i < n && !(src[i] === "*" && src[i + 1] === "/")) i++;
|
|
708
|
+
i += 2;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
712
|
+
const quote = ch;
|
|
713
|
+
const start = i;
|
|
714
|
+
i++;
|
|
715
|
+
let value = "";
|
|
716
|
+
while (i < n && src[i] !== quote) {
|
|
717
|
+
if (src[i] === "\\" && i + 1 < n) {
|
|
718
|
+
const esc = src[i + 1];
|
|
719
|
+
if (esc === "n") value += "\n";
|
|
720
|
+
else if (esc === "t") value += " ";
|
|
721
|
+
else if (esc === "r") value += "\r";
|
|
722
|
+
else if (esc === "\\") value += "\\";
|
|
723
|
+
else if (esc === "'") value += "'";
|
|
724
|
+
else if (esc === '"') value += '"';
|
|
725
|
+
else if (esc === "`") value += "`";
|
|
726
|
+
else if (esc === "0") value += "\0";
|
|
727
|
+
else value += esc;
|
|
728
|
+
i += 2;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
732
|
+
throw new Error("template interpolation unsupported");
|
|
733
|
+
}
|
|
734
|
+
value += src[i];
|
|
735
|
+
i++;
|
|
736
|
+
}
|
|
737
|
+
if (src[i] !== quote) throw new Error("unterminated string");
|
|
738
|
+
i++;
|
|
739
|
+
tokens.push({ kind: "str", text: value, start });
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (ch >= "0" && ch <= "9" || ch === "." && isDigit(src[i + 1])) {
|
|
743
|
+
const start = i;
|
|
744
|
+
while (i < n && (isDigit(src[i]) || src[i] === "." || src[i] === "e" || src[i] === "E" || src[i] === "+" || src[i] === "-")) {
|
|
745
|
+
const c = src[i];
|
|
746
|
+
if ((c === "+" || c === "-") && !(src[i - 1] === "e" || src[i - 1] === "E")) break;
|
|
747
|
+
i++;
|
|
748
|
+
}
|
|
749
|
+
tokens.push({ kind: "num", text: src.slice(start, i), start });
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (isIdentStart(ch)) {
|
|
753
|
+
const start = i;
|
|
754
|
+
while (i < n && isIdentCont(src[i])) i++;
|
|
755
|
+
const text = src.slice(start, i);
|
|
756
|
+
if (KEYWORDS.has(text)) {
|
|
757
|
+
tokens.push({ kind: "keyword", text, start });
|
|
758
|
+
} else {
|
|
759
|
+
tokens.push({ kind: "ident", text, start });
|
|
760
|
+
}
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
let matched = false;
|
|
764
|
+
for (const op of MULTI_OPS) {
|
|
765
|
+
if (src.startsWith(op, i)) {
|
|
766
|
+
tokens.push({ kind: op === "=>" ? "arrow" : "op", text: op, start: i });
|
|
767
|
+
i += op.length;
|
|
768
|
+
matched = true;
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (matched) continue;
|
|
773
|
+
if (SINGLE_OPS.has(ch)) {
|
|
774
|
+
tokens.push({ kind: "op", text: ch, start: i });
|
|
775
|
+
i++;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (PUNCT.has(ch)) {
|
|
779
|
+
tokens.push({ kind: "punct", text: ch, start: i });
|
|
780
|
+
i++;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
throw new Error(`unexpected char '${ch}' at ${i}`);
|
|
784
|
+
}
|
|
785
|
+
tokens.push({ kind: "eof", text: "", start: n });
|
|
786
|
+
return tokens;
|
|
787
|
+
}
|
|
788
|
+
function isDigit(c) {
|
|
789
|
+
return c !== void 0 && c >= "0" && c <= "9";
|
|
790
|
+
}
|
|
791
|
+
function isIdentStart(c) {
|
|
792
|
+
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_" || c === "$";
|
|
793
|
+
}
|
|
794
|
+
function isIdentCont(c) {
|
|
795
|
+
return isIdentStart(c) || c >= "0" && c <= "9";
|
|
796
|
+
}
|
|
797
|
+
var FnParser = class {
|
|
798
|
+
constructor(tokens, _source, knownColumns, namespaceAlias) {
|
|
799
|
+
this.tokens = tokens;
|
|
800
|
+
this.knownColumns = knownColumns;
|
|
801
|
+
this.namespaceAlias = namespaceAlias;
|
|
802
|
+
}
|
|
803
|
+
tokens;
|
|
804
|
+
knownColumns;
|
|
805
|
+
namespaceAlias;
|
|
806
|
+
pos = 0;
|
|
807
|
+
paramName = "";
|
|
808
|
+
peek(off = 0) {
|
|
809
|
+
return this.tokens[this.pos + off] ?? this.tokens[this.tokens.length - 1];
|
|
810
|
+
}
|
|
811
|
+
advance() {
|
|
812
|
+
const t = this.tokens[this.pos];
|
|
813
|
+
this.pos++;
|
|
814
|
+
return t;
|
|
815
|
+
}
|
|
816
|
+
consumeText(text) {
|
|
817
|
+
const t = this.peek();
|
|
818
|
+
if (t.text === text) {
|
|
819
|
+
this.pos++;
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
expectText(text) {
|
|
825
|
+
if (!this.consumeText(text)) {
|
|
826
|
+
throw new Error(`expected '${text}' at token ${this.pos}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Parse the whole arrow function; accept two shapes:
|
|
831
|
+
* `function (row) { return <expr>; }` — legacy fn form
|
|
832
|
+
* `(row) => <expr-or-block>` — arrow form
|
|
833
|
+
*
|
|
834
|
+
* Returns null (via caller) on any unsupported shape.
|
|
835
|
+
*/
|
|
836
|
+
parseTopLevel() {
|
|
837
|
+
try {
|
|
838
|
+
if (this.peek().text === "function") {
|
|
839
|
+
this.advance();
|
|
840
|
+
if (this.peek().kind === "ident") this.advance();
|
|
841
|
+
this.expectText("(");
|
|
842
|
+
if (this.peek().kind !== "ident") return null;
|
|
843
|
+
this.paramName = this.advance().text;
|
|
844
|
+
this.expectText(")");
|
|
845
|
+
this.expectText("{");
|
|
846
|
+
this.expectText("return");
|
|
847
|
+
const expr = this.parseExpr();
|
|
848
|
+
this.consumeText(";");
|
|
849
|
+
this.expectText("}");
|
|
850
|
+
return expr;
|
|
851
|
+
}
|
|
852
|
+
if (this.consumeText("(")) {
|
|
853
|
+
if (this.peek().kind !== "ident") return null;
|
|
854
|
+
this.paramName = this.advance().text;
|
|
855
|
+
this.expectText(")");
|
|
856
|
+
} else {
|
|
857
|
+
if (this.peek().kind !== "ident") return null;
|
|
858
|
+
this.paramName = this.advance().text;
|
|
859
|
+
}
|
|
860
|
+
if (this.peek().kind !== "arrow") return null;
|
|
861
|
+
this.advance();
|
|
862
|
+
if (this.consumeText("{")) {
|
|
863
|
+
this.expectText("return");
|
|
864
|
+
const expr = this.parseExpr();
|
|
865
|
+
this.consumeText(";");
|
|
866
|
+
this.expectText("}");
|
|
867
|
+
return expr;
|
|
868
|
+
}
|
|
869
|
+
return this.parseExpr();
|
|
870
|
+
} catch {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// OrExpr
|
|
875
|
+
parseExpr() {
|
|
876
|
+
let left = this.parseAnd();
|
|
877
|
+
while (this.peek().text === "||") {
|
|
878
|
+
this.advance();
|
|
879
|
+
const right = this.parseAnd();
|
|
880
|
+
left = { kind: "or", args: flatten("or", [left, right]) };
|
|
881
|
+
}
|
|
882
|
+
return left;
|
|
883
|
+
}
|
|
884
|
+
parseAnd() {
|
|
885
|
+
let left = this.parseNot();
|
|
886
|
+
while (this.peek().text === "&&") {
|
|
887
|
+
this.advance();
|
|
888
|
+
const right = this.parseNot();
|
|
889
|
+
left = { kind: "and", args: flatten("and", [left, right]) };
|
|
890
|
+
}
|
|
891
|
+
return left;
|
|
892
|
+
}
|
|
893
|
+
parseNot() {
|
|
894
|
+
if (this.peek().text === "!") {
|
|
895
|
+
this.advance();
|
|
896
|
+
const arg = this.parseNot();
|
|
897
|
+
if (arg.kind === "isNull") {
|
|
898
|
+
return { kind: "isNull", col: arg.col, negated: !arg.negated };
|
|
899
|
+
}
|
|
900
|
+
return { kind: "not", arg };
|
|
901
|
+
}
|
|
902
|
+
return this.parseEquality();
|
|
903
|
+
}
|
|
904
|
+
parseEquality() {
|
|
905
|
+
const left = this.parseRelational();
|
|
906
|
+
const t = this.peek();
|
|
907
|
+
if (t.text === "===" || t.text === "!==" || t.text === "==" || t.text === "!=") {
|
|
908
|
+
this.advance();
|
|
909
|
+
const right = this.parseRelational();
|
|
910
|
+
return this.buildCompare(t.text, left, right);
|
|
911
|
+
}
|
|
912
|
+
return left;
|
|
913
|
+
}
|
|
914
|
+
parseRelational() {
|
|
915
|
+
const left = this.parseUnary();
|
|
916
|
+
const t = this.peek();
|
|
917
|
+
if (t.text === ">" || t.text === ">=" || t.text === "<" || t.text === "<=") {
|
|
918
|
+
this.advance();
|
|
919
|
+
const right = this.parseUnary();
|
|
920
|
+
return this.buildCompare(t.text, left, right);
|
|
921
|
+
}
|
|
922
|
+
return left;
|
|
923
|
+
}
|
|
924
|
+
parseUnary() {
|
|
925
|
+
if (this.peek().text === "-") {
|
|
926
|
+
this.advance();
|
|
927
|
+
const next = this.peek();
|
|
928
|
+
if (next.kind !== "num") throw new Error("unary minus requires numeric literal");
|
|
929
|
+
this.advance();
|
|
930
|
+
return {
|
|
931
|
+
kind: "lit",
|
|
932
|
+
value: -parseFloat(next.text)
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
return this.parseAtom();
|
|
936
|
+
}
|
|
937
|
+
parseAtom() {
|
|
938
|
+
const t = this.peek();
|
|
939
|
+
if (t.kind === "keyword") {
|
|
940
|
+
if (t.text === "true" || t.text === "false") {
|
|
941
|
+
this.advance();
|
|
942
|
+
return { kind: "lit", value: t.text === "true" };
|
|
943
|
+
}
|
|
944
|
+
if (t.text === "null" || t.text === "undefined") {
|
|
945
|
+
this.advance();
|
|
946
|
+
return { kind: "lit", value: null };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (t.kind === "num") {
|
|
950
|
+
this.advance();
|
|
951
|
+
return { kind: "lit", value: parseFloat(t.text) };
|
|
952
|
+
}
|
|
953
|
+
if (t.kind === "str") {
|
|
954
|
+
this.advance();
|
|
955
|
+
return { kind: "lit", value: t.text };
|
|
956
|
+
}
|
|
957
|
+
if (t.kind === "punct" && t.text === "(") {
|
|
958
|
+
this.advance();
|
|
959
|
+
const inner = this.parseExpr();
|
|
960
|
+
this.expectText(")");
|
|
961
|
+
return inner;
|
|
962
|
+
}
|
|
963
|
+
if (t.kind === "ident") {
|
|
964
|
+
if (t.text !== this.paramName) {
|
|
965
|
+
throw new Error(`unsupported identifier '${t.text}'`);
|
|
966
|
+
}
|
|
967
|
+
this.advance();
|
|
968
|
+
const ref = this.parseColumnRef();
|
|
969
|
+
if (ref.kind === "col" && this.peek().text === ".") {
|
|
970
|
+
const strOp = this.parseStringMethodTail(ref);
|
|
971
|
+
if (strOp) return strOp;
|
|
972
|
+
throw new Error("nested field access unsupported");
|
|
973
|
+
}
|
|
974
|
+
return ref;
|
|
975
|
+
}
|
|
976
|
+
throw new Error(`unexpected token '${t.text}'`);
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* After `ref = row.col`, peek a trailing `.method(...)` call. We only
|
|
980
|
+
* recognize a narrow whitelist of String.prototype methods that map
|
|
981
|
+
* cleanly to FilterExpr nodes. Returns null if the tail doesn't look
|
|
982
|
+
* like any supported method call — the caller will then treat the
|
|
983
|
+
* dangling `.` as unsupported nested access.
|
|
984
|
+
*/
|
|
985
|
+
parseStringMethodTail(col) {
|
|
986
|
+
const saved = this.pos;
|
|
987
|
+
if (this.peek().text !== ".") return null;
|
|
988
|
+
this.advance();
|
|
989
|
+
const method = this.peek();
|
|
990
|
+
if (method.kind !== "ident") {
|
|
991
|
+
this.pos = saved;
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
const name = method.text;
|
|
995
|
+
const MAPPING = {
|
|
996
|
+
includes: "contains",
|
|
997
|
+
startsWith: "startsWith",
|
|
998
|
+
endsWith: "endsWith"
|
|
999
|
+
};
|
|
1000
|
+
const mapped = MAPPING[name];
|
|
1001
|
+
if (!mapped) {
|
|
1002
|
+
this.pos = saved;
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
this.advance();
|
|
1006
|
+
if (this.peek().text !== "(") {
|
|
1007
|
+
this.pos = saved;
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
this.advance();
|
|
1011
|
+
const arg = this.peek();
|
|
1012
|
+
if (arg.kind !== "str") {
|
|
1013
|
+
this.pos = saved;
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
this.advance();
|
|
1017
|
+
if (this.peek().text === ",") {
|
|
1018
|
+
this.pos = saved;
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
if (this.peek().text !== ")") {
|
|
1022
|
+
this.pos = saved;
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
this.advance();
|
|
1026
|
+
return {
|
|
1027
|
+
kind: "str",
|
|
1028
|
+
op: mapped,
|
|
1029
|
+
col,
|
|
1030
|
+
value: arg.text
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
parseColumnRef() {
|
|
1034
|
+
const t = this.peek();
|
|
1035
|
+
let colName;
|
|
1036
|
+
if (t.kind === "punct" && t.text === ".") {
|
|
1037
|
+
this.advance();
|
|
1038
|
+
const id = this.advance();
|
|
1039
|
+
if (id.kind !== "ident") throw new Error("expected identifier after .");
|
|
1040
|
+
colName = id.text;
|
|
1041
|
+
} else if (t.kind === "punct" && t.text === "[") {
|
|
1042
|
+
this.advance();
|
|
1043
|
+
const lit = this.advance();
|
|
1044
|
+
if (lit.kind !== "str") throw new Error("computed access must be string literal");
|
|
1045
|
+
colName = lit.text;
|
|
1046
|
+
this.expectText("]");
|
|
1047
|
+
} else {
|
|
1048
|
+
throw new Error("bare param unsupported");
|
|
1049
|
+
}
|
|
1050
|
+
if (this.namespaceAlias !== null) {
|
|
1051
|
+
if (colName !== this.namespaceAlias) throw new Error(`unknown namespace '${colName}'`);
|
|
1052
|
+
this.expectText(".");
|
|
1053
|
+
const field = this.advance();
|
|
1054
|
+
if (field.kind !== "ident") throw new Error("expected column after namespace alias");
|
|
1055
|
+
colName = field.text;
|
|
1056
|
+
}
|
|
1057
|
+
const next = this.peek();
|
|
1058
|
+
if (next.text === "[") {
|
|
1059
|
+
throw new Error("nested index access unsupported");
|
|
1060
|
+
}
|
|
1061
|
+
if (next.text === ".") ;
|
|
1062
|
+
if (this.knownColumns && !this.knownColumns.has(colName)) {
|
|
1063
|
+
throw new Error(`unknown column '${colName}'`);
|
|
1064
|
+
}
|
|
1065
|
+
return { kind: "col", name: colName };
|
|
1066
|
+
}
|
|
1067
|
+
// Build a cmp node; rewrite `col ==/!== null` into isNull / isNotNull.
|
|
1068
|
+
buildCompare(op, left, right) {
|
|
1069
|
+
const normOp = op === "==" ? "===" : op === "!=" ? "!==" : op;
|
|
1070
|
+
const nullEq = detectNullEquality(left, right);
|
|
1071
|
+
if (nullEq) {
|
|
1072
|
+
return {
|
|
1073
|
+
kind: "isNull",
|
|
1074
|
+
col: nullEq.col,
|
|
1075
|
+
negated: normOp === "!==" || normOp === "!="
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
const cmpOp = OP_TO_CMP[normOp];
|
|
1079
|
+
if (!cmpOp) throw new Error(`bad op '${op}'`);
|
|
1080
|
+
const leftOp = asOperand(left);
|
|
1081
|
+
const rightOp = asOperand(right);
|
|
1082
|
+
if (!leftOp || !rightOp) throw new Error("operand not a column or literal");
|
|
1083
|
+
if (leftOp.kind === "col" && rightOp.kind === "col") {
|
|
1084
|
+
throw new Error("col vs col unsupported");
|
|
1085
|
+
}
|
|
1086
|
+
if (leftOp.kind === "lit" && rightOp.kind === "lit") {
|
|
1087
|
+
throw new Error("lit vs lit unsupported");
|
|
1088
|
+
}
|
|
1089
|
+
if (leftOp.kind === "lit") {
|
|
1090
|
+
return {
|
|
1091
|
+
kind: "cmp",
|
|
1092
|
+
op: flipOp(cmpOp),
|
|
1093
|
+
left: rightOp,
|
|
1094
|
+
right: leftOp
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
return { kind: "cmp", op: cmpOp, left: leftOp, right: rightOp };
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
var OP_TO_CMP = {
|
|
1101
|
+
"===": "eq",
|
|
1102
|
+
"!==": "neq",
|
|
1103
|
+
">": "gt",
|
|
1104
|
+
">=": "gte",
|
|
1105
|
+
"<": "lt",
|
|
1106
|
+
"<=": "lte"
|
|
1107
|
+
};
|
|
1108
|
+
function flipOp(op) {
|
|
1109
|
+
switch (op) {
|
|
1110
|
+
case "gt":
|
|
1111
|
+
return "lt";
|
|
1112
|
+
case "gte":
|
|
1113
|
+
return "lte";
|
|
1114
|
+
case "lt":
|
|
1115
|
+
return "gt";
|
|
1116
|
+
case "lte":
|
|
1117
|
+
return "gte";
|
|
1118
|
+
default:
|
|
1119
|
+
return op;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function asOperand(node) {
|
|
1123
|
+
if (node.kind === "col") return node;
|
|
1124
|
+
const raw = node;
|
|
1125
|
+
if (raw.kind === "lit") return raw;
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
function detectNullEquality(left, right) {
|
|
1129
|
+
const isNullLit = (n) => {
|
|
1130
|
+
const raw = n;
|
|
1131
|
+
return raw.kind === "lit" && raw.value === null;
|
|
1132
|
+
};
|
|
1133
|
+
if (left.kind === "col" && isNullLit(right)) return { col: left };
|
|
1134
|
+
if (right.kind === "col" && isNullLit(left)) return { col: right };
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
function flatten(kind, args) {
|
|
1138
|
+
const out = [];
|
|
1139
|
+
for (const a of args) {
|
|
1140
|
+
if (a.kind === kind) out.push(...a.args);
|
|
1141
|
+
else out.push(a);
|
|
1142
|
+
}
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
var Query = class _Query {
|
|
1146
|
+
collection;
|
|
1147
|
+
predicate;
|
|
1148
|
+
/** Column-op filter DSL expression (vectorizable). */
|
|
1149
|
+
filterExpr;
|
|
1150
|
+
selection;
|
|
1151
|
+
order;
|
|
1152
|
+
limitN;
|
|
1153
|
+
offsetN;
|
|
1154
|
+
/** Explicit deps override. If null, deps are inferred. */
|
|
1155
|
+
explicitDeps;
|
|
1156
|
+
constructor(collection, predicate = null, selection = null, order = [], limitN = null, offsetN = 0, explicitDeps = null, filterExpr = null) {
|
|
1157
|
+
this.collection = collection;
|
|
1158
|
+
this.predicate = predicate;
|
|
1159
|
+
this.filterExpr = filterExpr;
|
|
1160
|
+
this.selection = selection;
|
|
1161
|
+
this.order = order;
|
|
1162
|
+
this.limitN = limitN;
|
|
1163
|
+
this.offsetN = offsetN;
|
|
1164
|
+
this.explicitDeps = explicitDeps;
|
|
1165
|
+
}
|
|
1166
|
+
where(predicate) {
|
|
1167
|
+
const next = this.predicate ? (row) => this.predicate(row) && predicate(row) : predicate;
|
|
1168
|
+
const lowered = tryCompileFnToFilterExpr(
|
|
1169
|
+
predicate,
|
|
1170
|
+
{ knownColumns: new Set(this.collection.schema.columnsByName.keys()) }
|
|
1171
|
+
);
|
|
1172
|
+
const composedExpr = lowered === null ? this.filterExpr : this.filterExpr ? { kind: "and", args: [this.filterExpr, lowered] } : lowered;
|
|
1173
|
+
const keepFn = lowered === null;
|
|
1174
|
+
return new _Query(
|
|
1175
|
+
this.collection,
|
|
1176
|
+
keepFn ? next : this.predicate,
|
|
1177
|
+
this.selection,
|
|
1178
|
+
this.order,
|
|
1179
|
+
this.limitN,
|
|
1180
|
+
this.offsetN,
|
|
1181
|
+
this.explicitDeps,
|
|
1182
|
+
composedExpr
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Add a vectorizable column-op filter. Composes with prior `filter()`
|
|
1187
|
+
* calls via AND. Anything that fits the `FilterExpr` DSL takes the
|
|
1188
|
+
* vectorized fast path in `QueryExecutor`; combine with `.where(fn)`
|
|
1189
|
+
* when you need an escape hatch for logic outside the DSL — the fast
|
|
1190
|
+
* path still runs first for the DSL portion.
|
|
1191
|
+
*/
|
|
1192
|
+
filter(expr) {
|
|
1193
|
+
const next = this.filterExpr ? { kind: "and", args: [this.filterExpr, expr] } : expr;
|
|
1194
|
+
return new _Query(
|
|
1195
|
+
this.collection,
|
|
1196
|
+
this.predicate,
|
|
1197
|
+
this.selection,
|
|
1198
|
+
this.order,
|
|
1199
|
+
this.limitN,
|
|
1200
|
+
this.offsetN,
|
|
1201
|
+
this.explicitDeps,
|
|
1202
|
+
next
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
select(columns) {
|
|
1206
|
+
for (const c of columns) this.requireKnownColumn(c);
|
|
1207
|
+
return new _Query(
|
|
1208
|
+
this.collection,
|
|
1209
|
+
this.predicate,
|
|
1210
|
+
[...columns],
|
|
1211
|
+
this.order,
|
|
1212
|
+
this.limitN,
|
|
1213
|
+
this.offsetN,
|
|
1214
|
+
this.explicitDeps,
|
|
1215
|
+
this.filterExpr
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
orderBy(column, dir = "asc") {
|
|
1219
|
+
this.requireKnownColumn(column);
|
|
1220
|
+
return new _Query(
|
|
1221
|
+
this.collection,
|
|
1222
|
+
this.predicate,
|
|
1223
|
+
this.selection,
|
|
1224
|
+
[...this.order, { column, dir }],
|
|
1225
|
+
this.limitN,
|
|
1226
|
+
this.offsetN,
|
|
1227
|
+
this.explicitDeps,
|
|
1228
|
+
this.filterExpr
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
limit(n) {
|
|
1232
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1233
|
+
throw new ValidationError(`limit must be a non-negative integer, got ${n}`);
|
|
1234
|
+
}
|
|
1235
|
+
return new _Query(
|
|
1236
|
+
this.collection,
|
|
1237
|
+
this.predicate,
|
|
1238
|
+
this.selection,
|
|
1239
|
+
this.order,
|
|
1240
|
+
n,
|
|
1241
|
+
this.offsetN,
|
|
1242
|
+
this.explicitDeps,
|
|
1243
|
+
this.filterExpr
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
offset(n) {
|
|
1247
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1248
|
+
throw new ValidationError(`offset must be a non-negative integer, got ${n}`);
|
|
1249
|
+
}
|
|
1250
|
+
return new _Query(
|
|
1251
|
+
this.collection,
|
|
1252
|
+
this.predicate,
|
|
1253
|
+
this.selection,
|
|
1254
|
+
this.order,
|
|
1255
|
+
this.limitN,
|
|
1256
|
+
n,
|
|
1257
|
+
this.explicitDeps,
|
|
1258
|
+
this.filterExpr
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Equijoin against another collection. Returns a `JoinQuery` that
|
|
1263
|
+
* exposes the post-join builder surface (where / select / orderBy /
|
|
1264
|
+
* limit / offset).
|
|
1265
|
+
*
|
|
1266
|
+
* ```ts
|
|
1267
|
+
* query(orders)
|
|
1268
|
+
* .filter(f('amount').gt(100))
|
|
1269
|
+
* .join(users, { on: { left: 'userId', right: '__id' }, as: 'user' })
|
|
1270
|
+
* .orderBy('user.name')
|
|
1271
|
+
* .select(['id', 'amount', 'user.name']);
|
|
1272
|
+
* ```
|
|
1273
|
+
*
|
|
1274
|
+
* Implementation: to avoid a static cycle between query.ts and
|
|
1275
|
+
* join.ts, join.ts registers its factory via `registerJoinFactory`
|
|
1276
|
+
* at module-load time. Importing `arrowbase` (or
|
|
1277
|
+
* `arrowbase/query/join`) before calling `.join()` is sufficient.
|
|
1278
|
+
*/
|
|
1279
|
+
join(right, spec) {
|
|
1280
|
+
{
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
`Query.join: join module not loaded. Import it (via \`import 'arrowbase'\` or \`import { join } from 'arrowbase'\`) before use.`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Explicit dep override. Use this when:
|
|
1288
|
+
* - your predicate reads columns conditionally and you want stable deps
|
|
1289
|
+
* - you want to force reactivity on a superset of columns
|
|
1290
|
+
*/
|
|
1291
|
+
deps(columns) {
|
|
1292
|
+
for (const c of columns) this.requireKnownColumn(c);
|
|
1293
|
+
return new _Query(
|
|
1294
|
+
this.collection,
|
|
1295
|
+
this.predicate,
|
|
1296
|
+
this.selection,
|
|
1297
|
+
this.order,
|
|
1298
|
+
this.limitN,
|
|
1299
|
+
this.offsetN,
|
|
1300
|
+
new Set(columns),
|
|
1301
|
+
this.filterExpr
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
/** Columns that must be materialized for this query (deps + filter + selection + order). */
|
|
1305
|
+
requiredColumns(inferredDeps) {
|
|
1306
|
+
const out = /* @__PURE__ */ new Set();
|
|
1307
|
+
const src = this.explicitDeps ?? inferredDeps;
|
|
1308
|
+
for (const c of src) out.add(c);
|
|
1309
|
+
if (this.filterExpr) collectColumns(this.filterExpr, out);
|
|
1310
|
+
if (this.selection) for (const c of this.selection) out.add(c);
|
|
1311
|
+
for (const o of this.order) out.add(o.column);
|
|
1312
|
+
return out;
|
|
1313
|
+
}
|
|
1314
|
+
// -----------------------------------------------------------------
|
|
1315
|
+
requireKnownColumn(name) {
|
|
1316
|
+
if (!this.collection.schema.columnsByName.has(name)) {
|
|
1317
|
+
throw new ValidationError(
|
|
1318
|
+
`unknown column "${name}" on collection "${this.collection.schema.name}"`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
var QueryExecutor = class {
|
|
1324
|
+
constructor(query) {
|
|
1325
|
+
this.query = query;
|
|
1326
|
+
if (query.explicitDeps) {
|
|
1327
|
+
for (const c of query.explicitDeps) this.deps.add(c);
|
|
1328
|
+
}
|
|
1329
|
+
if (query.filterExpr) {
|
|
1330
|
+
collectColumns(query.filterExpr, this.deps);
|
|
1331
|
+
}
|
|
1332
|
+
this.unsubscribePassive = query.collection.subscribe((event) => {
|
|
1333
|
+
if (event.op !== "update") this.staleFromInsertOrDelete = true;
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
query;
|
|
1337
|
+
deps = /* @__PURE__ */ new Set();
|
|
1338
|
+
cachedSnapshot = null;
|
|
1339
|
+
/** Per-column version seen at the time `cachedSnapshot` was produced. */
|
|
1340
|
+
cachedVersions = /* @__PURE__ */ new Map();
|
|
1341
|
+
/**
|
|
1342
|
+
* When the predicate trips (throws) during the tracking dry-run, we
|
|
1343
|
+
* fall back to `deps = all`. We still try inference again after the
|
|
1344
|
+
* predicate changes, but a single Query instance keeps this bit sticky.
|
|
1345
|
+
*/
|
|
1346
|
+
depsAreAll = false;
|
|
1347
|
+
/**
|
|
1348
|
+
* Inserts, deletes, and rollbacks invalidate any cached snapshot because
|
|
1349
|
+
* they may change the live row set even when no observed value column
|
|
1350
|
+
* moves (soft-delete notwithstanding). We flip this flag from a passive
|
|
1351
|
+
* change listener and clear it on recompute.
|
|
1352
|
+
*/
|
|
1353
|
+
staleFromInsertOrDelete = false;
|
|
1354
|
+
unsubscribePassive;
|
|
1355
|
+
/** Release the passive change listener. */
|
|
1356
|
+
dispose() {
|
|
1357
|
+
this.unsubscribePassive();
|
|
1358
|
+
}
|
|
1359
|
+
/** True if the cached snapshot is still valid given current column versions. */
|
|
1360
|
+
cacheIsFresh() {
|
|
1361
|
+
if (!this.cachedSnapshot) return false;
|
|
1362
|
+
if (this.staleFromInsertOrDelete) return false;
|
|
1363
|
+
const c = this.query.collection;
|
|
1364
|
+
for (const name of this.observedColumns()) {
|
|
1365
|
+
if (!c.schema.columnsByName.has(name)) continue;
|
|
1366
|
+
const prev = this.cachedVersions.get(name);
|
|
1367
|
+
if (prev === void 0) return false;
|
|
1368
|
+
if (c.columnVersion(name) !== prev) return false;
|
|
1369
|
+
}
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Columns this query observes for invalidation purposes. Includes:
|
|
1374
|
+
* - inferred or explicit deps (from predicate / .deps())
|
|
1375
|
+
* - selection columns (stale projection otherwise)
|
|
1376
|
+
* - order-by columns (stale sort otherwise)
|
|
1377
|
+
*/
|
|
1378
|
+
observedColumns() {
|
|
1379
|
+
if (this.depsAreAll) return this.query.collection.columnNames();
|
|
1380
|
+
const union = /* @__PURE__ */ new Set();
|
|
1381
|
+
const base = this.query.explicitDeps ?? this.deps;
|
|
1382
|
+
for (const c of base) union.add(c);
|
|
1383
|
+
if (this.query.filterExpr) collectColumns(this.query.filterExpr, union);
|
|
1384
|
+
if (this.query.selection) for (const c of this.query.selection) union.add(c);
|
|
1385
|
+
for (const o of this.query.order) union.add(o.column);
|
|
1386
|
+
return union;
|
|
1387
|
+
}
|
|
1388
|
+
/** Lazily compile the filterExpr. Cached for executor lifetime. */
|
|
1389
|
+
compiledFilter = null;
|
|
1390
|
+
compiledFilterExpr = null;
|
|
1391
|
+
compiledFilterError = null;
|
|
1392
|
+
getCompiledFilter() {
|
|
1393
|
+
const expr = this.query.filterExpr;
|
|
1394
|
+
if (!expr) return null;
|
|
1395
|
+
if (this.compiledFilterExpr === expr && this.compiledFilter) {
|
|
1396
|
+
return this.compiledFilter;
|
|
1397
|
+
}
|
|
1398
|
+
if (this.compiledFilterError) throw this.compiledFilterError;
|
|
1399
|
+
const c = this.query.collection;
|
|
1400
|
+
const scanByName = /* @__PURE__ */ new Map();
|
|
1401
|
+
const scanCols = c._internalScanColumns();
|
|
1402
|
+
for (const sc of scanCols) scanByName.set(sc.name, sc);
|
|
1403
|
+
try {
|
|
1404
|
+
this.compiledFilter = compileFilter(expr, scanByName);
|
|
1405
|
+
this.compiledFilterExpr = expr;
|
|
1406
|
+
return this.compiledFilter;
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
if (err instanceof FilterCompileError) {
|
|
1409
|
+
this.compiledFilterError = err;
|
|
1410
|
+
}
|
|
1411
|
+
throw err;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
/** Recompute the snapshot, updating cache. */
|
|
1415
|
+
recompute() {
|
|
1416
|
+
const q = this.query;
|
|
1417
|
+
const c = q.collection;
|
|
1418
|
+
const pred = q.predicate;
|
|
1419
|
+
const filterExpr = q.filterExpr;
|
|
1420
|
+
let ordinals;
|
|
1421
|
+
let needsFnPredicate = pred !== null;
|
|
1422
|
+
if (filterExpr) {
|
|
1423
|
+
const compiled = this.getCompiledFilter();
|
|
1424
|
+
ordinals = scanOrdinals(c._internalLiveOrdinals(), compiled.test);
|
|
1425
|
+
} else if (!pred) {
|
|
1426
|
+
ordinals = scanOrdinals(c._internalLiveOrdinals(), () => true);
|
|
1427
|
+
} else {
|
|
1428
|
+
ordinals = this.legacyScan();
|
|
1429
|
+
needsFnPredicate = false;
|
|
1430
|
+
}
|
|
1431
|
+
if (needsFnPredicate) {
|
|
1432
|
+
ordinals = this.applyFnPredicate(ordinals);
|
|
1433
|
+
}
|
|
1434
|
+
const scanByName = /* @__PURE__ */ new Map();
|
|
1435
|
+
for (const sc of c._internalScanColumns()) scanByName.set(sc.name, sc);
|
|
1436
|
+
const sorted = sortOrdinals(
|
|
1437
|
+
ordinals,
|
|
1438
|
+
q.order,
|
|
1439
|
+
scanByName,
|
|
1440
|
+
(row, col) => c._internalReadCell(col, row)
|
|
1441
|
+
);
|
|
1442
|
+
const out = materialize(c, sorted, q.offsetN, q.limitN, q.selection);
|
|
1443
|
+
this.cachedVersions.clear();
|
|
1444
|
+
for (const name of this.observedColumns()) {
|
|
1445
|
+
if (!c.schema.columnsByName.has(name)) continue;
|
|
1446
|
+
this.cachedVersions.set(name, c.columnVersion(name));
|
|
1447
|
+
}
|
|
1448
|
+
this.staleFromInsertOrDelete = false;
|
|
1449
|
+
const snap = {
|
|
1450
|
+
rows: out,
|
|
1451
|
+
version: c.globalVersion
|
|
1452
|
+
};
|
|
1453
|
+
this.cachedSnapshot = snap;
|
|
1454
|
+
return snap;
|
|
1455
|
+
}
|
|
1456
|
+
/** Legacy scan: materialize + run fn predicate with dep tracking. Returns ordinals of survivors. */
|
|
1457
|
+
legacyScan() {
|
|
1458
|
+
const q = this.query;
|
|
1459
|
+
const c = q.collection;
|
|
1460
|
+
const pred = q.predicate;
|
|
1461
|
+
const matched = [];
|
|
1462
|
+
let idx = 0;
|
|
1463
|
+
for (const row of c.rows()) {
|
|
1464
|
+
const rowOrdinal = this.getOrdinalFor(idx++, row);
|
|
1465
|
+
try {
|
|
1466
|
+
const tracked = q.explicitDeps || this.depsAreAll ? row : trackRow(row, this.deps);
|
|
1467
|
+
if (pred(tracked)) matched.push(rowOrdinal);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
this.depsAreAll = true;
|
|
1470
|
+
try {
|
|
1471
|
+
if (pred(row)) matched.push(rowOrdinal);
|
|
1472
|
+
} catch {
|
|
1473
|
+
throw err;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return toU32(matched);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Map the sequential index returned by `rows()` to the live row
|
|
1481
|
+
* ordinal. `rows()` yields in the same order as `_internalLiveOrdinals()`,
|
|
1482
|
+
* so we lazily keep a parallel ordinal buffer.
|
|
1483
|
+
*/
|
|
1484
|
+
cachedOrdinalBuffer = null;
|
|
1485
|
+
cachedOrdinalVersion = -1;
|
|
1486
|
+
getOrdinalFor(idx, _row) {
|
|
1487
|
+
const c = this.query.collection;
|
|
1488
|
+
if (this.cachedOrdinalVersion !== c.globalVersion || !this.cachedOrdinalBuffer) {
|
|
1489
|
+
const arr = [];
|
|
1490
|
+
for (const r of c._internalLiveOrdinals()) arr.push(r);
|
|
1491
|
+
this.cachedOrdinalBuffer = toU32(arr);
|
|
1492
|
+
this.cachedOrdinalVersion = c.globalVersion;
|
|
1493
|
+
}
|
|
1494
|
+
return this.cachedOrdinalBuffer[idx] ?? -1;
|
|
1495
|
+
}
|
|
1496
|
+
/** Apply the fn predicate to a candidate ordinal set (post-kernel filter). */
|
|
1497
|
+
applyFnPredicate(candidates) {
|
|
1498
|
+
const q = this.query;
|
|
1499
|
+
q.collection;
|
|
1500
|
+
const pred = q.predicate;
|
|
1501
|
+
const kept = [];
|
|
1502
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
1503
|
+
const ord = candidates[i];
|
|
1504
|
+
const row = this.materializeFullRow(ord);
|
|
1505
|
+
try {
|
|
1506
|
+
const tracked = q.explicitDeps || this.depsAreAll ? row : trackRow(row, this.deps);
|
|
1507
|
+
if (pred(tracked)) kept.push(ord);
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
this.depsAreAll = true;
|
|
1510
|
+
try {
|
|
1511
|
+
if (pred(row)) kept.push(ord);
|
|
1512
|
+
} catch {
|
|
1513
|
+
throw err;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return toU32(kept);
|
|
1518
|
+
}
|
|
1519
|
+
materializeFullRow(ord) {
|
|
1520
|
+
const c = this.query.collection;
|
|
1521
|
+
const out = {};
|
|
1522
|
+
for (const name of c.columnNames()) out[name] = c._internalReadCell(name, ord);
|
|
1523
|
+
return out;
|
|
1524
|
+
}
|
|
1525
|
+
/** Return a snapshot, reusing the cached one if all observed columns are unchanged. */
|
|
1526
|
+
snapshot() {
|
|
1527
|
+
if (this.cacheIsFresh()) return this.cachedSnapshot;
|
|
1528
|
+
return this.recompute();
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Subscribe to reactive updates. `listener` fires once with the initial
|
|
1532
|
+
* snapshot and then again after every mutation that touches an observed
|
|
1533
|
+
* column. Returns an unsubscribe function.
|
|
1534
|
+
*/
|
|
1535
|
+
subscribe(listener) {
|
|
1536
|
+
listener(this.snapshot());
|
|
1537
|
+
const unsub = this.query.collection.subscribe((event) => {
|
|
1538
|
+
if (!this.shouldReact(event)) return;
|
|
1539
|
+
listener(this.snapshot());
|
|
1540
|
+
});
|
|
1541
|
+
return unsub;
|
|
1542
|
+
}
|
|
1543
|
+
/** Visible for tests: introspect current inferred deps. */
|
|
1544
|
+
get inferredDeps() {
|
|
1545
|
+
return this.deps;
|
|
1546
|
+
}
|
|
1547
|
+
get isDepsAll() {
|
|
1548
|
+
return this.depsAreAll;
|
|
1549
|
+
}
|
|
1550
|
+
shouldReact(event) {
|
|
1551
|
+
if (event.columns === "all" || this.depsAreAll) return true;
|
|
1552
|
+
if (!this.cachedSnapshot) return true;
|
|
1553
|
+
if (event.op === "insert" || event.op === "delete" || event.op === "rollback" || event.op === "compact") {
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
const observed = new Set(this.observedColumns());
|
|
1557
|
+
for (const c of event.columns) {
|
|
1558
|
+
if (observed.has(c)) return true;
|
|
1559
|
+
}
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
function run(q) {
|
|
1564
|
+
return new QueryExecutor(q);
|
|
1565
|
+
}
|
|
1566
|
+
function toU32(arr) {
|
|
1567
|
+
const out = new Uint32Array(arr.length);
|
|
1568
|
+
for (let i = 0; i < arr.length; i++) out[i] = arr[i];
|
|
1569
|
+
return out;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/react.ts
|
|
1573
|
+
function useLiveQuery(query, options = {}) {
|
|
1574
|
+
const executor = useExecutorForQuery(query);
|
|
1575
|
+
return useExecutorSnapshot(executor, options);
|
|
1576
|
+
}
|
|
1577
|
+
function useLiveQueryResult(collection, build, deps = [], options = {}) {
|
|
1578
|
+
const query = useMemo(
|
|
1579
|
+
() => build(new Query(collection)),
|
|
1580
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1581
|
+
[collection, ...deps]
|
|
1582
|
+
);
|
|
1583
|
+
const executor = useExecutorForQuery(query);
|
|
1584
|
+
return useExecutorSnapshot(executor, options);
|
|
1585
|
+
}
|
|
1586
|
+
function useCollectionVersion(collection) {
|
|
1587
|
+
const subscribe = useCallback(
|
|
1588
|
+
(onChange) => collection.subscribe(() => onChange()),
|
|
1589
|
+
[collection]
|
|
1590
|
+
);
|
|
1591
|
+
const getSnapshot = useCallback(() => collection.globalVersion, [collection]);
|
|
1592
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
1593
|
+
}
|
|
1594
|
+
function useExecutorForQuery(query) {
|
|
1595
|
+
const ref = useRef(null);
|
|
1596
|
+
if (ref.current === null || ref.current.query !== query) {
|
|
1597
|
+
if (ref.current) ref.current.executor.dispose();
|
|
1598
|
+
ref.current = { query, executor: run(query) };
|
|
1599
|
+
}
|
|
1600
|
+
return ref.current.executor;
|
|
1601
|
+
}
|
|
1602
|
+
function useExecutorSnapshot(executor, options) {
|
|
1603
|
+
const subscribe = useCallback(
|
|
1604
|
+
(onChange) => {
|
|
1605
|
+
let seenInitial = false;
|
|
1606
|
+
return executor.subscribe(() => {
|
|
1607
|
+
if (!seenInitial) {
|
|
1608
|
+
seenInitial = true;
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
onChange();
|
|
1612
|
+
});
|
|
1613
|
+
},
|
|
1614
|
+
[executor]
|
|
1615
|
+
);
|
|
1616
|
+
const getSnapshot = useCallback(() => executor.snapshot(), [executor]);
|
|
1617
|
+
const getServerSnapshot = useCallback(
|
|
1618
|
+
() => options.ssrSnapshot ?? EMPTY_SNAPSHOT_TYPED(),
|
|
1619
|
+
[options.ssrSnapshot]
|
|
1620
|
+
);
|
|
1621
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
1622
|
+
}
|
|
1623
|
+
var EMPTY_SNAPSHOT = Object.freeze({
|
|
1624
|
+
rows: Object.freeze([]),
|
|
1625
|
+
version: 0
|
|
1626
|
+
});
|
|
1627
|
+
function EMPTY_SNAPSHOT_TYPED() {
|
|
1628
|
+
return EMPTY_SNAPSHOT;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
export { useCollectionVersion, useLiveQuery, useLiveQueryResult };
|
|
1632
|
+
//# sourceMappingURL=react.js.map
|
|
1633
|
+
//# sourceMappingURL=react.js.map
|