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/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