@visorcraft/mongreldb-kit 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/constraints.d.ts +35 -0
  2. package/dist/constraints.d.ts.map +1 -0
  3. package/dist/constraints.js +404 -0
  4. package/dist/constraints.js.map +1 -0
  5. package/dist/db.d.ts +135 -0
  6. package/dist/db.d.ts.map +1 -0
  7. package/dist/db.js +495 -0
  8. package/dist/db.js.map +1 -0
  9. package/dist/defaults.d.ts +26 -0
  10. package/dist/defaults.d.ts.map +1 -0
  11. package/dist/defaults.js +56 -0
  12. package/dist/defaults.js.map +1 -0
  13. package/dist/errors.d.ts +54 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +104 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +13 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/internalTables.d.ts +63 -0
  22. package/dist/internalTables.d.ts.map +1 -0
  23. package/dist/internalTables.js +60 -0
  24. package/dist/internalTables.js.map +1 -0
  25. package/dist/keys.d.ts +7 -0
  26. package/dist/keys.d.ts.map +1 -0
  27. package/dist/keys.js +84 -0
  28. package/dist/keys.js.map +1 -0
  29. package/dist/migrate.d.ts +132 -0
  30. package/dist/migrate.d.ts.map +1 -0
  31. package/dist/migrate.js +1004 -0
  32. package/dist/migrate.js.map +1 -0
  33. package/dist/packing.d.ts +12 -0
  34. package/dist/packing.d.ts.map +1 -0
  35. package/dist/packing.js +137 -0
  36. package/dist/packing.js.map +1 -0
  37. package/dist/query.d.ts +423 -0
  38. package/dist/query.d.ts.map +1 -0
  39. package/dist/query.js +1645 -0
  40. package/dist/query.js.map +1 -0
  41. package/dist/remote.d.ts +29 -0
  42. package/dist/remote.d.ts.map +1 -0
  43. package/dist/remote.js +42 -0
  44. package/dist/remote.js.map +1 -0
  45. package/dist/rows.d.ts +5 -0
  46. package/dist/rows.d.ts.map +1 -0
  47. package/dist/rows.js +38 -0
  48. package/dist/rows.js.map +1 -0
  49. package/dist/schema.d.ts +91 -0
  50. package/dist/schema.d.ts.map +1 -0
  51. package/dist/schema.js +206 -0
  52. package/dist/schema.js.map +1 -0
  53. package/dist/tsv.d.ts +4 -0
  54. package/dist/tsv.d.ts.map +1 -0
  55. package/dist/tsv.js +102 -0
  56. package/dist/tsv.js.map +1 -0
  57. package/dist/types.d.ts +99 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +2 -0
  60. package/dist/types.js.map +1 -0
  61. package/dist/validation.d.ts +3 -0
  62. package/dist/validation.d.ts.map +1 -0
  63. package/dist/validation.js +98 -0
  64. package/dist/validation.js.map +1 -0
  65. package/package.json +50 -0
package/dist/query.js ADDED
@@ -0,0 +1,1645 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { tableFromIPC } from 'apache-arrow';
3
+ import { KitDatabase, runSyncTxn } from './db.js';
4
+ import { applyDefaults } from './defaults.js';
5
+ import { validateRow } from './validation.js';
6
+ import { toCells, stageUniqueGuards, stagePkGuard, deleteUniqueGuards, deletePkGuard, enforceForeignKeys, planDelete, pkValueFromRow, pkValuesEqual, findByPk, isReferencedTable } from './constraints.js';
7
+ import { KitError } from './errors.js';
8
+ import { encodedPk } from './keys.js';
9
+ import { packRows, packRowIds } from './packing.js';
10
+ import { rowFromRowJs } from './rows.js';
11
+ function joinValuesEqual(a, b) {
12
+ if (a === null || a === undefined || b === null || b === undefined)
13
+ return a === b;
14
+ if (typeof a === 'bigint' || typeof b === 'bigint')
15
+ return BigInt(a) === BigInt(b);
16
+ return a === b;
17
+ }
18
+ /**
19
+ * A declarative join predicate equating `leftTable.leftColumn` with
20
+ * `rightTable.rightColumn`. Behaves like the closure form, but the builder can
21
+ * introspect the equality to fetch the right table by an index probe over the
22
+ * distinct left keys instead of a full scan. Prefer this over a hand-written
23
+ * closure for FK joins.
24
+ */
25
+ export function joinEq(leftTable, leftColumn, rightTable, rightColumn) {
26
+ const pred = (row) => joinValuesEqual(row[leftTable.name]?.[leftColumn.name], row[rightTable.name]?.[rightColumn.name]);
27
+ pred.__eqKey = {
28
+ leftTable: leftTable.name,
29
+ leftColumn,
30
+ rightTable: rightTable.name,
31
+ rightColumn
32
+ };
33
+ return pred;
34
+ }
35
+ const I64_MIN = -9223372036854775808n;
36
+ const I64_MAX = 9223372036854775807n;
37
+ /**
38
+ * Validate that `column` is a real column spec. Guards against the footgun
39
+ * where a column whose name shadows a table property (e.g. `name`) is accessed
40
+ * as `table.name` and yields the table name string instead of the column.
41
+ */
42
+ function asColumn(column) {
43
+ if (!column ||
44
+ typeof column !== 'object' ||
45
+ typeof column.storageType !== 'string' ||
46
+ typeof column.id !== 'number') {
47
+ const got = typeof column === 'string' ? `the string "${column}"` : String(column);
48
+ throw new KitError(`Expected a column, received ${got}. If your column name shadows a table ` +
49
+ `property (e.g. "name"), access it with table.column('<name>').`);
50
+ }
51
+ return column;
52
+ }
53
+ export function eq(column, value) {
54
+ return { kind: 'eq', column: asColumn(column), value };
55
+ }
56
+ export function ne(column, value) {
57
+ return { kind: 'ne', column: asColumn(column), value };
58
+ }
59
+ export function gt(column, value) {
60
+ return { kind: 'gt', column: asColumn(column), value };
61
+ }
62
+ export function gte(column, value) {
63
+ return { kind: 'gte', column: asColumn(column), value };
64
+ }
65
+ export function lt(column, value) {
66
+ return { kind: 'lt', column: asColumn(column), value };
67
+ }
68
+ export function lte(column, value) {
69
+ return { kind: 'lte', column: asColumn(column), value };
70
+ }
71
+ export function isNull(column) {
72
+ return { kind: 'null', column: asColumn(column), not: false };
73
+ }
74
+ export function isNotNull(column) {
75
+ return { kind: 'null', column: asColumn(column), not: true };
76
+ }
77
+ export function inList(column, values) {
78
+ return { kind: 'in', column: asColumn(column), values };
79
+ }
80
+ export function and(...predicates) {
81
+ return { kind: 'and', predicates };
82
+ }
83
+ export function or(...predicates) {
84
+ return { kind: 'or', predicates };
85
+ }
86
+ export function asc(column) {
87
+ return { column: asColumn(column), direction: 'asc' };
88
+ }
89
+ export function desc(column) {
90
+ return { column: asColumn(column), direction: 'desc' };
91
+ }
92
+ /** Negates a predicate (logical NOT). */
93
+ export function not(predicate) {
94
+ return { kind: 'not', predicate };
95
+ }
96
+ /** `column NOT IN (values)`. */
97
+ export function notInList(column, values) {
98
+ return { kind: 'notIn', column: asColumn(column), values };
99
+ }
100
+ /**
101
+ * SQL `LIKE` against a text column. `%` matches any run of characters and `_`
102
+ * matches a single character; all other characters are literal. Case-sensitive.
103
+ */
104
+ export function like(column, pattern) {
105
+ return { kind: 'like', column: asColumn(column), pattern };
106
+ }
107
+ /** Case-sensitive substring match: `column LIKE '%substr%'` with no wildcards. */
108
+ export function contains(column, substr) {
109
+ return { kind: 'contains', column: asColumn(column), substr };
110
+ }
111
+ /** `column IN (subquery)`. The subquery must select exactly one column. */
112
+ export function inSubquery(column, subquery) {
113
+ return { kind: 'inSub', column, subquery };
114
+ }
115
+ /**
116
+ * `EXISTS (subquery)`. The subquery is uncorrelated: it is evaluated once and
117
+ * gates the whole outer scan.
118
+ * // ponytail: no correlated-subquery support; correlation would require
119
+ * // re-binding the outer row into the subquery per candidate.
120
+ */
121
+ export function exists(subquery) {
122
+ return { kind: 'exists', subquery, negate: false };
123
+ }
124
+ /** `NOT EXISTS (subquery)`. Uncorrelated, like {@link exists}. */
125
+ export function notExists(subquery) {
126
+ return { kind: 'exists', subquery, negate: true };
127
+ }
128
+ /** Aggregate descriptor: `COUNT(*)` for a group. */
129
+ export function count() {
130
+ return { fn: 'count' };
131
+ }
132
+ /** Aggregate descriptor: `COUNT(column)` — non-null values in a group. */
133
+ export function countColumn(column) {
134
+ return { fn: 'count', column };
135
+ }
136
+ /** Aggregate descriptor: `COUNT(DISTINCT column)` — unique non-null values. */
137
+ export function countDistinct(column) {
138
+ return { fn: 'count', column, distinct: true };
139
+ }
140
+ /** Aggregate descriptor: `SUM(column)` for a group. */
141
+ export function sum(column) {
142
+ return { fn: 'sum', column };
143
+ }
144
+ /** Aggregate descriptor: `MIN(column)` for a group. */
145
+ export function min(column) {
146
+ return { fn: 'min', column };
147
+ }
148
+ /** Aggregate descriptor: `MAX(column)` for a group. */
149
+ export function max(column) {
150
+ return { fn: 'max', column };
151
+ }
152
+ /** Aggregate descriptor: `AVG(column)` for a group (always a float). */
153
+ export function avg(column) {
154
+ return { fn: 'avg', column };
155
+ }
156
+ function isIndexed(table, columnName) {
157
+ if (table.primaryKey.includes(columnName))
158
+ return true;
159
+ if (table.indexes.some((idx) => idx.columns.includes(columnName)))
160
+ return true;
161
+ if (table.foreignKeys.some((fk) => fk.columns.includes(columnName)))
162
+ return true;
163
+ return false;
164
+ }
165
+ function isBitmapTextColumn(column) {
166
+ return (column.storageType === 'text' ||
167
+ column.storageType === 'timestamp' ||
168
+ column.storageType === 'date' ||
169
+ column.storageType === 'json');
170
+ }
171
+ function makeEqCondition(table, column, value) {
172
+ if (value === null || value === undefined)
173
+ return null;
174
+ if (column.storageType === 'int64') {
175
+ if (typeof value !== 'bigint')
176
+ return null;
177
+ return {
178
+ kind: 2 /* ConditionKind.RangeInt */,
179
+ columnId: column.id,
180
+ int64Lo: value,
181
+ int64Hi: value
182
+ };
183
+ }
184
+ if (column.storageType === 'float64') {
185
+ if (typeof value !== 'number' || Number.isNaN(value))
186
+ return null;
187
+ return {
188
+ kind: 3 /* ConditionKind.RangeF64 */,
189
+ columnId: column.id,
190
+ float64Lo: value,
191
+ float64Hi: value
192
+ };
193
+ }
194
+ if (isIndexed(table, column.name) && isBitmapTextColumn(column)) {
195
+ return {
196
+ kind: 1 /* ConditionKind.BitmapEq */,
197
+ columnId: column.id,
198
+ text: String(value)
199
+ };
200
+ }
201
+ return null;
202
+ }
203
+ function makeInCondition(table, column, values) {
204
+ if (!isIndexed(table, column.name) || !isBitmapTextColumn(column))
205
+ return null;
206
+ if (values.length === 0 || values.some((v) => v === null || v === undefined))
207
+ return null;
208
+ return {
209
+ kind: 7 /* ConditionKind.BitmapIn */,
210
+ columnId: column.id,
211
+ values: [...new Set(values.map((v) => String(v)))]
212
+ };
213
+ }
214
+ /** Build an `FmContains` condition when `column` has an FM index; else `null`
215
+ * (the caller falls back to an in-memory substring check). The engine returns a
216
+ * superset, so the caller keeps the predicate as a residual. */
217
+ function makeContainsCondition(table, column, substr) {
218
+ const hasFm = table.indexes.some((idx) => idx.kind === 'fm' && idx.columns.includes(column.name));
219
+ if (!hasFm || column.storageType !== 'text')
220
+ return null;
221
+ return { kind: 4 /* ConditionKind.FmContains */, columnId: column.id, text: substr };
222
+ }
223
+ /** Build an `FmContainsAll` condition of a LIKE pattern's literal runs (a
224
+ * superset the caller re-checks). Requires an FM index; escaped patterns and
225
+ * pure-wildcard patterns fall back to an in-memory match. */
226
+ function makeLikeCondition(table, column, pattern) {
227
+ const hasFm = table.indexes.some((idx) => idx.kind === 'fm' && idx.columns.includes(column.name));
228
+ if (!hasFm || column.storageType !== 'text' || pattern.includes('\\'))
229
+ return null;
230
+ const segments = pattern.split(/[%_]/).filter((s) => s.length > 0);
231
+ if (segments.length === 0)
232
+ return null;
233
+ return { kind: 10 /* ConditionKind.FmContainsAll */, columnId: column.id, values: segments };
234
+ }
235
+ function makeRangeCondition(column, op, value) {
236
+ if (column.storageType === 'int64') {
237
+ if (typeof value !== 'bigint')
238
+ return null;
239
+ const v = value;
240
+ let lo = I64_MIN;
241
+ let hi = I64_MAX;
242
+ switch (op) {
243
+ case 'gt': {
244
+ if (v < I64_MIN)
245
+ break;
246
+ if (v >= I64_MAX)
247
+ return { alwaysFalse: true };
248
+ lo = v + 1n;
249
+ break;
250
+ }
251
+ case 'gte':
252
+ if (v <= I64_MIN)
253
+ break;
254
+ if (v > I64_MAX)
255
+ return { alwaysFalse: true };
256
+ lo = v;
257
+ break;
258
+ case 'lt': {
259
+ if (v <= I64_MIN)
260
+ return { alwaysFalse: true };
261
+ if (v > I64_MAX)
262
+ break;
263
+ hi = v - 1n;
264
+ break;
265
+ }
266
+ case 'lte':
267
+ if (v < I64_MIN)
268
+ return { alwaysFalse: true };
269
+ if (v >= I64_MAX)
270
+ break;
271
+ hi = v;
272
+ break;
273
+ }
274
+ return {
275
+ condition: {
276
+ kind: 2 /* ConditionKind.RangeInt */,
277
+ columnId: column.id,
278
+ int64Lo: lo,
279
+ int64Hi: hi
280
+ }
281
+ };
282
+ }
283
+ if (column.storageType === 'float64') {
284
+ if (typeof value !== 'number' || Number.isNaN(value))
285
+ return { alwaysFalse: true };
286
+ const v = value;
287
+ let lo = -Infinity;
288
+ let hi = Infinity;
289
+ switch (op) {
290
+ case 'gt':
291
+ case 'gte':
292
+ lo = v;
293
+ break;
294
+ case 'lt':
295
+ case 'lte':
296
+ hi = v;
297
+ break;
298
+ }
299
+ return {
300
+ condition: {
301
+ kind: 3 /* ConditionKind.RangeF64 */,
302
+ columnId: column.id,
303
+ float64Lo: lo,
304
+ float64Hi: hi
305
+ },
306
+ residual: op === 'gt' || op === 'lt' ? { kind: op, column, value } : undefined
307
+ };
308
+ }
309
+ return null;
310
+ }
311
+ function makeRangeFilter(op, column, value) {
312
+ return (row) => {
313
+ const actual = row[column.name];
314
+ if (actual === null || actual === undefined)
315
+ return false;
316
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
317
+ const a = actual;
318
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
+ const v = value;
320
+ switch (op) {
321
+ case 'gt':
322
+ return a > v;
323
+ case 'gte':
324
+ return a >= v;
325
+ case 'lt':
326
+ return a < v;
327
+ case 'lte':
328
+ return a <= v;
329
+ }
330
+ };
331
+ }
332
+ function fullScanRows(db, table) {
333
+ return queryNativeRows(db, table, []);
334
+ }
335
+ function queryNativeRows(db, table, conditions) {
336
+ return db
337
+ .table(table.name)
338
+ .query(conditions)
339
+ .map((rowJs) => ({ rowId: rowJs.rowId, row: rowFromRowJs(table, rowJs) }));
340
+ }
341
+ /**
342
+ * Fetch the right-side rows for a join clause. For an FK-equality clause built
343
+ * with {@link joinEq} whose right column is probe-able, fetch only the rows
344
+ * matching the distinct left keys — one indexed query per key, unioned — instead
345
+ * of scanning the whole table. Falls back to a full scan otherwise. The clause
346
+ * predicate is still re-checked per combined row, so the result is identical.
347
+ */
348
+ function joinRightRows(db, clause, leftRows) {
349
+ const key = clause.kind !== 'cross'
350
+ ? clause.on?.__eqKey
351
+ : undefined;
352
+ if (key && key.rightTable === clause.table.name) {
353
+ const seen = new Set();
354
+ const values = [];
355
+ for (const combo of leftRows) {
356
+ const v = combo[key.leftTable]?.[key.leftColumn.name];
357
+ if (v !== null && v !== undefined) {
358
+ const k = String(v);
359
+ if (!seen.has(k)) {
360
+ seen.add(k);
361
+ values.push(v);
362
+ }
363
+ }
364
+ }
365
+ const rows = [];
366
+ const rowSeen = new Set();
367
+ let probed = true;
368
+ for (const v of values) {
369
+ const cond = makeEqCondition(clause.table, key.rightColumn, v);
370
+ if (!cond) {
371
+ probed = false;
372
+ break;
373
+ }
374
+ for (const m of queryNativeRows(db, clause.table, [cond])) {
375
+ if (!rowSeen.has(m.rowId)) {
376
+ rowSeen.add(m.rowId);
377
+ rows.push(m.row);
378
+ }
379
+ }
380
+ }
381
+ if (probed)
382
+ return rows;
383
+ }
384
+ return fullScanRows(db, clause.table).map((m) => m.row);
385
+ }
386
+ function andResidual(predicates) {
387
+ const residuals = predicates.filter((p) => p !== undefined);
388
+ if (residuals.length === 0)
389
+ return undefined;
390
+ if (residuals.length === 1)
391
+ return residuals[0];
392
+ return { kind: 'and', predicates: residuals };
393
+ }
394
+ function residualPlan(predicate) {
395
+ return { conditions: [], residual: predicate };
396
+ }
397
+ const CONDITION_LABELS = {
398
+ [0 /* ConditionKind.Pk */]: 'Pk',
399
+ [6 /* ConditionKind.PkInt64 */]: 'PkInt64',
400
+ [1 /* ConditionKind.BitmapEq */]: 'BitmapEq',
401
+ [7 /* ConditionKind.BitmapIn */]: 'BitmapIn',
402
+ [2 /* ConditionKind.RangeInt */]: 'RangeInt',
403
+ [3 /* ConditionKind.RangeF64 */]: 'RangeF64',
404
+ [4 /* ConditionKind.FmContains */]: 'FmContains',
405
+ [10 /* ConditionKind.FmContainsAll */]: 'FmContainsAll',
406
+ [8 /* ConditionKind.IsNull */]: 'IsNull',
407
+ [9 /* ConditionKind.IsNotNull */]: 'IsNotNull',
408
+ [5 /* ConditionKind.Ann */]: 'Ann',
409
+ [11 /* ConditionKind.SparseMatch */]: 'SparseMatch'
410
+ };
411
+ function compilePredicate(table, predicate) {
412
+ switch (predicate.kind) {
413
+ case 'eq': {
414
+ const condition = makeEqCondition(table, predicate.column, predicate.value);
415
+ return condition ? { conditions: [condition] } : residualPlan(predicate);
416
+ }
417
+ case 'gt':
418
+ case 'gte':
419
+ case 'lt':
420
+ case 'lte': {
421
+ const condition = makeRangeCondition(predicate.column, predicate.kind, predicate.value);
422
+ if (!condition)
423
+ return residualPlan(predicate);
424
+ if ('alwaysFalse' in condition)
425
+ return { conditions: [], alwaysFalse: true };
426
+ return {
427
+ conditions: [condition.condition],
428
+ residual: condition.residual
429
+ };
430
+ }
431
+ case 'in': {
432
+ if (predicate.values.length === 0)
433
+ return { conditions: [], alwaysFalse: true };
434
+ if (predicate.values.length === 1) {
435
+ return compilePredicate(table, {
436
+ kind: 'eq',
437
+ column: predicate.column,
438
+ value: predicate.values[0]
439
+ });
440
+ }
441
+ const condition = makeInCondition(table, predicate.column, predicate.values);
442
+ return condition ? { conditions: [condition] } : residualPlan(predicate);
443
+ }
444
+ case 'and': {
445
+ const conditions = [];
446
+ const residuals = [];
447
+ for (const child of predicate.predicates) {
448
+ const plan = compilePredicate(table, child);
449
+ if (plan.alwaysFalse)
450
+ return { conditions: [], alwaysFalse: true };
451
+ conditions.push(...plan.conditions);
452
+ if (plan.residual)
453
+ residuals.push(plan.residual);
454
+ }
455
+ return { conditions, residual: andResidual(residuals) };
456
+ }
457
+ case 'inSub': {
458
+ return compilePredicate(table, {
459
+ kind: 'in',
460
+ column: predicate.column,
461
+ values: predicate.subquery.scalarValuesSync()
462
+ });
463
+ }
464
+ case 'exists':
465
+ return predicate.subquery.hasRowsSync() !== predicate.negate
466
+ ? { conditions: [] }
467
+ : { conditions: [], alwaysFalse: true };
468
+ case 'or': {
469
+ const inPlan = compileOrAsIn(table, predicate.predicates);
470
+ return inPlan ?? residualPlan(predicate);
471
+ }
472
+ case 'contains': {
473
+ // Push FmContains on an FM-indexed column; keep the substring check as
474
+ // a residual since the engine returns a superset.
475
+ const condition = makeContainsCondition(table, predicate.column, predicate.substr);
476
+ return condition ? { conditions: [condition], residual: predicate } : residualPlan(predicate);
477
+ }
478
+ case 'null': {
479
+ // Push the engine's page-stat-aware IsNull / IsNotNull; keep the null
480
+ // check as a residual (the engine returns a superset).
481
+ const cond = {
482
+ kind: predicate.not ? 9 /* ConditionKind.IsNotNull */ : 8 /* ConditionKind.IsNull */,
483
+ columnId: predicate.column.id
484
+ };
485
+ return { conditions: [cond], residual: predicate };
486
+ }
487
+ case 'like': {
488
+ // Push FmContainsAll of the pattern's literal runs on an FM column;
489
+ // keep the LIKE match as a residual (the engine returns a superset).
490
+ const condition = makeLikeCondition(table, predicate.column, predicate.pattern);
491
+ return condition ? { conditions: [condition], residual: predicate } : residualPlan(predicate);
492
+ }
493
+ default:
494
+ return residualPlan(predicate);
495
+ }
496
+ }
497
+ function compileOrAsIn(table, predicates) {
498
+ let column;
499
+ const values = [];
500
+ for (const predicate of predicates) {
501
+ if (predicate.kind === 'eq') {
502
+ if (!column)
503
+ column = predicate.column;
504
+ if (column.id !== predicate.column.id)
505
+ return null;
506
+ values.push(predicate.value);
507
+ continue;
508
+ }
509
+ if (predicate.kind === 'in') {
510
+ if (!column)
511
+ column = predicate.column;
512
+ if (column.id !== predicate.column.id)
513
+ return null;
514
+ values.push(...predicate.values);
515
+ continue;
516
+ }
517
+ return null;
518
+ }
519
+ if (!column)
520
+ return { conditions: [], alwaysFalse: true };
521
+ if (values.length === 0)
522
+ return { conditions: [], alwaysFalse: true };
523
+ const condition = makeInCondition(table, column, values);
524
+ return condition ? { conditions: [condition] } : null;
525
+ }
526
+ function evaluatePredicate(db, table, predicate) {
527
+ const plan = compilePredicate(table, predicate);
528
+ if (plan.alwaysFalse)
529
+ return [];
530
+ const rows = plan.conditions.length > 0 ? queryNativeRows(db, table, plan.conditions) : fullScanRows(db, table);
531
+ return plan.residual ? rows.filter((m) => matchRowPredicate(m.row, plan.residual)) : rows;
532
+ }
533
+ /**
534
+ * Pure in-memory predicate evaluation against a single plain row. Used for
535
+ * CTE-materialized sources (no native table to push down into) and for the
536
+ * leaf kinds with no native condition. Mirrors {@link evaluatePredicate}.
537
+ */
538
+ function matchRowPredicate(row, predicate) {
539
+ switch (predicate.kind) {
540
+ case 'and':
541
+ return predicate.predicates.every((p) => matchRowPredicate(row, p));
542
+ case 'or':
543
+ return predicate.predicates.some((p) => matchRowPredicate(row, p));
544
+ case 'not':
545
+ return !matchRowPredicate(row, predicate.predicate);
546
+ case 'eq':
547
+ return row[predicate.column.name] === predicate.value;
548
+ case 'ne':
549
+ return row[predicate.column.name] !== predicate.value;
550
+ case 'gt':
551
+ case 'gte':
552
+ case 'lt':
553
+ case 'lte':
554
+ return makeRangeFilter(predicate.kind, predicate.column, predicate.value)(row);
555
+ case 'null':
556
+ return predicate.not
557
+ ? row[predicate.column.name] != null
558
+ : row[predicate.column.name] == null;
559
+ case 'in':
560
+ return predicate.values.some((v) => row[predicate.column.name] === v);
561
+ case 'notIn':
562
+ return !predicate.values.some((v) => row[predicate.column.name] === v);
563
+ case 'like': {
564
+ const value = row[predicate.column.name];
565
+ return value != null && likeToRegex(predicate.pattern).test(String(value));
566
+ }
567
+ case 'contains': {
568
+ const value = row[predicate.column.name];
569
+ return value != null && String(value).includes(predicate.substr);
570
+ }
571
+ case 'inSub':
572
+ return predicate.subquery
573
+ .scalarValuesSync()
574
+ .some((v) => row[predicate.column.name] === v);
575
+ case 'exists':
576
+ return predicate.subquery.hasRowsSync() !== predicate.negate;
577
+ default:
578
+ throw new KitError('Unexpected predicate kind');
579
+ }
580
+ }
581
+ /** Compiles a SQL `LIKE` pattern into an anchored, case-sensitive RegExp. */
582
+ function likeToRegex(pattern) {
583
+ let out = '^';
584
+ for (const ch of pattern) {
585
+ if (ch === '%')
586
+ out += '.*';
587
+ else if (ch === '_')
588
+ out += '.';
589
+ else
590
+ out += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
591
+ }
592
+ out += '$';
593
+ return new RegExp(out, 's');
594
+ }
595
+ /**
596
+ * Resolve the rows for a builder honoring its `where`, choosing the native
597
+ * pushdown path or — when the builder is backed by a CTE-materialized source —
598
+ * a pure in-memory filter.
599
+ */
600
+ function resolveRows(db, table, where, source) {
601
+ if (source) {
602
+ return where ? source.filter((m) => matchRowPredicate(m.row, where)) : source;
603
+ }
604
+ return where ? evaluatePredicate(db, table, where) : fullScanRows(db, table);
605
+ }
606
+ /** Stable string key for a value, distinguishing types (for distinct/group). */
607
+ function valueKey(value) {
608
+ if (value === null || value === undefined)
609
+ return '\u0000';
610
+ const t = typeof value;
611
+ if (t === 'bigint')
612
+ return 'b' + value.toString();
613
+ if (t === 'number')
614
+ return 'n' + String(value);
615
+ if (t === 'boolean')
616
+ return value ? 'T' : 'F';
617
+ if (t === 'string')
618
+ return 's' + value;
619
+ return 'j' + JSON.stringify(value);
620
+ }
621
+ function compositeKey(values) {
622
+ return values.map(valueKey).join('\u0001');
623
+ }
624
+ /** `COUNT(*)` (no column), `COUNT(col)` (non-null), or `COUNT(DISTINCT col)`
625
+ * (unique non-null) over a group. */
626
+ function computeCount(spec, rows) {
627
+ if (!spec.column)
628
+ return BigInt(rows.length); // COUNT(*)
629
+ const name = spec.column.name;
630
+ const nonNull = rows.map((m) => m.row[name]).filter((v) => v !== null && v !== undefined);
631
+ if (spec.distinct) {
632
+ const seen = new Set();
633
+ for (const v of nonNull)
634
+ seen.add(valueKey(v));
635
+ return BigInt(seen.size);
636
+ }
637
+ return BigInt(nonNull.length);
638
+ }
639
+ /** Computes a scalar aggregate over matched rows, honoring NULL skipping.
640
+ * `distinct` de-duplicates the value set first (a no-op for MIN/MAX). */
641
+ function computeAggregate(kind, column, rows, distinct = false) {
642
+ const isInt = column.storageType === 'int64';
643
+ let values = rows
644
+ .map((m) => m.row[column.name])
645
+ .filter((v) => v !== null && v !== undefined);
646
+ if (distinct) {
647
+ const seen = new Set();
648
+ values = values.filter((v) => {
649
+ const k = valueKey(v);
650
+ if (seen.has(k))
651
+ return false;
652
+ seen.add(k);
653
+ return true;
654
+ });
655
+ }
656
+ switch (kind) {
657
+ case 'sum': {
658
+ if (isInt) {
659
+ let s = 0n;
660
+ for (const v of values)
661
+ s += v;
662
+ return s;
663
+ }
664
+ let s = 0;
665
+ for (const v of values)
666
+ s += Number(v);
667
+ return s;
668
+ }
669
+ case 'avg': {
670
+ if (values.length === 0)
671
+ return null;
672
+ let s = 0;
673
+ for (const v of values)
674
+ s += Number(v);
675
+ return s / values.length;
676
+ }
677
+ case 'min': {
678
+ if (values.length === 0)
679
+ return null;
680
+ let best = values[0];
681
+ for (const v of values)
682
+ if (compareValues(v, best, 'asc') < 0)
683
+ best = v;
684
+ return best;
685
+ }
686
+ case 'max': {
687
+ if (values.length === 0)
688
+ return null;
689
+ let best = values[0];
690
+ for (const v of values)
691
+ if (compareValues(v, best, 'asc') > 0)
692
+ best = v;
693
+ return best;
694
+ }
695
+ }
696
+ }
697
+ /** Synthetic table spec backing an in-memory CTE source. */
698
+ function syntheticTable(name, columns) {
699
+ return {
700
+ tableId: 0,
701
+ name,
702
+ columns,
703
+ primaryKey: [],
704
+ indexes: [],
705
+ foreignKeys: [],
706
+ unique: [],
707
+ checks: [],
708
+ column(columnName) {
709
+ const col = columns.find((c) => c.name === columnName);
710
+ if (!col)
711
+ throw new KitError(`Column "${columnName}" not found in CTE "${name}"`);
712
+ return col;
713
+ }
714
+ };
715
+ }
716
+ function compareValues(a, b, direction) {
717
+ if (a === null || a === undefined)
718
+ return direction === 'asc' ? 1 : -1;
719
+ if (b === null || b === undefined)
720
+ return direction === 'asc' ? -1 : 1;
721
+ let cmp = 0;
722
+ if (typeof a === 'bigint' && typeof b === 'bigint') {
723
+ cmp = a < b ? -1 : a > b ? 1 : 0;
724
+ }
725
+ else if (typeof a === 'number' && typeof b === 'number') {
726
+ cmp = a - b;
727
+ }
728
+ else if (typeof a === 'string' && typeof b === 'string') {
729
+ cmp = a < b ? -1 : a > b ? 1 : 0;
730
+ }
731
+ else if (typeof a === 'boolean' && typeof b === 'boolean') {
732
+ cmp = a === b ? 0 : a ? 1 : -1;
733
+ }
734
+ else {
735
+ cmp = String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
736
+ }
737
+ return direction === 'asc' ? cmp : -cmp;
738
+ }
739
+ function applyOrderBy(rows, orders) {
740
+ if (orders.length === 0)
741
+ return rows;
742
+ return [...rows].sort((a, b) => {
743
+ for (const order of orders) {
744
+ const cmp = compareValues(a.row[order.column.name], b.row[order.column.name], order.direction);
745
+ if (cmp !== 0)
746
+ return cmp;
747
+ }
748
+ return 0;
749
+ });
750
+ }
751
+ function applyLimitOffset(rows, limit, offset) {
752
+ return applyArrayLimitOffset(rows, limit, offset);
753
+ }
754
+ function applyArrayLimitOffset(rows, limit, offset) {
755
+ let result = rows;
756
+ if (offset !== undefined && offset > 0) {
757
+ result = result.slice(offset);
758
+ }
759
+ if (limit !== undefined) {
760
+ result = result.slice(0, limit);
761
+ }
762
+ return result;
763
+ }
764
+ /** Deduplicate output rows by the listed column names (keeps first seen). */
765
+ function dedupeRows(rows, columnNames) {
766
+ const seen = new Set();
767
+ const out = [];
768
+ for (const row of rows) {
769
+ const key = compositeKey(columnNames.map((n) => row[n]));
770
+ if (!seen.has(key)) {
771
+ seen.add(key);
772
+ out.push(row);
773
+ }
774
+ }
775
+ return out;
776
+ }
777
+ function makeConstraintKit(kit) {
778
+ return { db: kit.nativeDb, schema: kit.schema };
779
+ }
780
+ function makeDefaultContext() {
781
+ return {
782
+ now: new Date().toISOString(),
783
+ uuid: () => randomUUID()
784
+ };
785
+ }
786
+ /** Project `row` to only the requested columns, or return it unchanged. */
787
+ function projectRow(row, columns) {
788
+ if (!columns)
789
+ return row;
790
+ const projected = {};
791
+ for (const col of columns)
792
+ projected[col.name] = row[col.name];
793
+ return projected;
794
+ }
795
+ function prepareInsertRowSync(kit, table, row) {
796
+ const withSequence = { ...row };
797
+ // Engine-native AUTO_INCREMENT: reserve the id up front so the cross-table
798
+ // transaction can stage the row with an explicit value (the engine counter
799
+ // is in-memory and becomes durable when the row commits — no __kit_sequences
800
+ // hot row, no extra commit). At most one sequence column per table.
801
+ for (const col of table.columns) {
802
+ if ((withSequence[col.name] === undefined || withSequence[col.name] === null) &&
803
+ col.default?.kind === 'sequence') {
804
+ const reserved = kit.reserveAutoIncSync(table.name);
805
+ if (reserved !== null) {
806
+ withSequence[col.name] = reserved;
807
+ }
808
+ }
809
+ }
810
+ const withDefaults = applyDefaults(table, withSequence, makeDefaultContext());
811
+ // Normalize any column still unset to explicit null, so the stored row and
812
+ // the returned row agree (an unset nullable column reads back as null).
813
+ for (const col of table.columns) {
814
+ if (withDefaults[col.name] === undefined)
815
+ withDefaults[col.name] = null;
816
+ }
817
+ return withDefaults;
818
+ }
819
+ function applyUpdateDefaults(table, merged, patch, ctx) {
820
+ for (const col of table.columns) {
821
+ if (patch[col.name] !== undefined)
822
+ continue;
823
+ // Only `generated: 'now'` columns are write-managed timestamps that refresh
824
+ // on every update (e.g. updatedAt). A plain `default: nowDefault()` is an
825
+ // insert-time value (e.g. createdAt) and must NOT change on update.
826
+ if (col.generated === 'now') {
827
+ merged[col.name] = ctx.now;
828
+ }
829
+ }
830
+ }
831
+ function hasForeignKeyChange(table, patch) {
832
+ return table.foreignKeys.some((fk) => fk.columns.some((colName) => patch[colName] !== undefined));
833
+ }
834
+ /**
835
+ * Apply `patch` to `existingRow` inside `txn`, updating guards and foreign-key
836
+ * touches as needed. Returns the merged row.
837
+ */
838
+ function uniqueConstraintChanged(uq, existingRow, merged) {
839
+ return uq.columns.some((colName) => existingRow[colName] !== merged[colName]);
840
+ }
841
+ function applyUpdateInTxn(kit, txn, table, existingRow, existingRowId, patch) {
842
+ const merged = { ...existingRow, ...patch };
843
+ applyUpdateDefaults(table, merged, patch, makeDefaultContext());
844
+ validateRow(table, merged);
845
+ const oldPkValue = pkValueFromRow(table, existingRow);
846
+ const newPkValue = pkValueFromRow(table, merged);
847
+ const pkChanged = !pkValuesEqual(oldPkValue, newPkValue);
848
+ // Only delete guards for unique constraints whose values actually changed.
849
+ // Deleting unchanged guards and then re-staging them can race with the same
850
+ // transaction's visibility and silently drop the guard.
851
+ const changedConstraints = table.unique
852
+ .filter((uq) => uniqueConstraintChanged(uq, existingRow, merged))
853
+ .map((uq) => uq.name);
854
+ if (changedConstraints.length > 0) {
855
+ deleteUniqueGuards(kit, txn, table, oldPkValue, changedConstraints);
856
+ }
857
+ if (pkChanged) {
858
+ deletePkGuard(kit, txn, table, oldPkValue);
859
+ }
860
+ if (hasForeignKeyChange(table, patch)) {
861
+ enforceForeignKeys(kit, txn, table, merged);
862
+ }
863
+ txn.delete(table.name, existingRowId);
864
+ txn.put(table.name, toCells(table, merged));
865
+ stageUniqueGuards(kit, txn, table, merged, newPkValue);
866
+ if (pkChanged) {
867
+ stagePkGuard(kit, txn, table, newPkValue, true);
868
+ }
869
+ return merged;
870
+ }
871
+ export class SelectBuilder {
872
+ kit;
873
+ table;
874
+ _where;
875
+ _orderBy = [];
876
+ _limit;
877
+ _offset;
878
+ _columns;
879
+ _count = false;
880
+ _distinct = false;
881
+ _aggregate;
882
+ _ann;
883
+ _sparse;
884
+ /** Internal: in-memory rows backing a CTE source instead of a native table. */
885
+ _source;
886
+ constructor(kit, table) {
887
+ this.kit = kit;
888
+ this.table = table;
889
+ }
890
+ where(predicate) {
891
+ this._where = predicate;
892
+ return this;
893
+ }
894
+ orderBy(...orders) {
895
+ this._orderBy.push(...orders);
896
+ return this;
897
+ }
898
+ limit(n) {
899
+ this._limit = n;
900
+ return this;
901
+ }
902
+ offset(n) {
903
+ this._offset = n;
904
+ return this;
905
+ }
906
+ /** Remove duplicate result rows (over the selected columns). */
907
+ distinct() {
908
+ this._distinct = true;
909
+ return this;
910
+ }
911
+ select(columns) {
912
+ const next = new SelectBuilder(this.kit, this.table);
913
+ next._where = this._where;
914
+ next._orderBy = this._orderBy;
915
+ next._limit = this._limit;
916
+ next._offset = this._offset;
917
+ next._columns = columns;
918
+ next._distinct = this._distinct;
919
+ next._source = this._source;
920
+ return next;
921
+ }
922
+ cloneScalar() {
923
+ const next = new SelectBuilder(this.kit, this.table);
924
+ next._where = this._where;
925
+ next._source = this._source;
926
+ return next;
927
+ }
928
+ selectCount() {
929
+ const next = this.cloneScalar();
930
+ next._count = true;
931
+ return next;
932
+ }
933
+ selectSum(column) {
934
+ const next = this.cloneScalar();
935
+ next._aggregate = { kind: 'sum', column };
936
+ return next;
937
+ }
938
+ selectAvg(column) {
939
+ const next = this.cloneScalar();
940
+ next._aggregate = { kind: 'avg', column };
941
+ return next;
942
+ }
943
+ selectMin(column) {
944
+ const next = this.cloneScalar();
945
+ next._aggregate = { kind: 'min', column };
946
+ return next;
947
+ }
948
+ selectMax(column) {
949
+ const next = this.cloneScalar();
950
+ next._aggregate = { kind: 'max', column };
951
+ return next;
952
+ }
953
+ /** Start an INNER JOIN. The `on` predicate runs in JS over the joined row. */
954
+ innerJoin(table, on) {
955
+ return this.startJoin().innerJoin(table, on);
956
+ }
957
+ /** Start a LEFT JOIN; unmatched right side is null in the result row. */
958
+ leftJoin(table, on) {
959
+ return this.startJoin().leftJoin(table, on);
960
+ }
961
+ /** Start a CROSS JOIN (cartesian product; no predicate). */
962
+ crossJoin(table) {
963
+ return this.startJoin().crossJoin(table);
964
+ }
965
+ startJoin() {
966
+ return new JoinBuilder(this.kit, this.table, this._where, this._source);
967
+ }
968
+ /** Group matched rows by the given columns and compute aggregates per group. */
969
+ groupBy(...columns) {
970
+ return new GroupBuilder(this.kit, this.table, columns, this._where, this._source);
971
+ }
972
+ resolveMatched() {
973
+ return resolveRows(this.kit.nativeDb, this.table, this._where, this._source);
974
+ }
975
+ /** Bind an in-memory source (used by CTE materialization). Internal. */
976
+ _bindSource(rows) {
977
+ this._source = rows;
978
+ return this;
979
+ }
980
+ /** Run the query and capture its rows + output columns for CTE materialization. */
981
+ _materialize() {
982
+ const result = this.executeSync();
983
+ if (!Array.isArray(result)) {
984
+ throw new KitError('Only a row-returning select can back a CTE');
985
+ }
986
+ const columns = this._columns ?? [...this.table.columns];
987
+ return { rows: result, columns };
988
+ }
989
+ scalarValuesSync() {
990
+ if (this._count || this._aggregate) {
991
+ throw new KitError('A subquery used in IN/EXISTS must select rows, not an aggregate');
992
+ }
993
+ let colName;
994
+ if (this._columns) {
995
+ if (this._columns.length !== 1) {
996
+ throw new KitError('An IN subquery must select exactly one column');
997
+ }
998
+ colName = this._columns[0].name;
999
+ }
1000
+ else if (this.table.primaryKey.length === 1) {
1001
+ colName = this.table.primaryKey[0];
1002
+ }
1003
+ else {
1004
+ colName = this.table.columns[0]?.name;
1005
+ }
1006
+ if (!colName)
1007
+ return [];
1008
+ return this.resolveMatched().map((m) => m.row[colName]);
1009
+ }
1010
+ hasRowsSync() {
1011
+ return this.resolveMatched().length > 0;
1012
+ }
1013
+ /**
1014
+ * Approximate nearest-neighbour search: return the `k` rows whose `column`
1015
+ * (an `embedding`) is closest to `vector`, resolved by the column's ANN
1016
+ * index. Terminal — call `executeSync()`/`execute()` next.
1017
+ */
1018
+ annSearch(column, vector, k) {
1019
+ const next = this;
1020
+ next._ann = { column, vector, k };
1021
+ return next;
1022
+ }
1023
+ /**
1024
+ * Learned-sparse (SPLADE) retrieval: return the `k` rows whose `column` (a
1025
+ * sparse token vector) best matches the weighted `query` `[token, weight]`
1026
+ * pairs. Terminal — call `executeSync()`/`execute()` next.
1027
+ */
1028
+ sparseMatch(column, query, k) {
1029
+ const next = this;
1030
+ next._sparse = { column, query, k };
1031
+ return next;
1032
+ }
1033
+ executeSync() {
1034
+ const db = this.kit.nativeDb;
1035
+ if (this._ann) {
1036
+ const cond = {
1037
+ kind: 5 /* ConditionKind.Ann */,
1038
+ columnId: this._ann.column.id,
1039
+ embedding: this._ann.vector,
1040
+ k: this._ann.k
1041
+ };
1042
+ return queryNativeRows(db, this.table, [cond]).map((m) => m.row);
1043
+ }
1044
+ if (this._sparse) {
1045
+ const cond = {
1046
+ kind: 11 /* ConditionKind.SparseMatch */,
1047
+ columnId: this._sparse.column.id,
1048
+ sparseTokens: this._sparse.query.map((p) => p[0]),
1049
+ sparseWeights: this._sparse.query.map((p) => p[1]),
1050
+ k: this._sparse.k
1051
+ };
1052
+ return queryNativeRows(db, this.table, [cond]).map((m) => m.row);
1053
+ }
1054
+ if (this._aggregate) {
1055
+ return computeAggregate(this._aggregate.kind, this._aggregate.column, this.resolveMatched());
1056
+ }
1057
+ if (this._count) {
1058
+ if (!this._where && !this._source) {
1059
+ return db.table(this.table.name).count();
1060
+ }
1061
+ if (this._where && !this._source) {
1062
+ const plan = compilePredicate(this.table, this._where);
1063
+ if (plan.alwaysFalse)
1064
+ return 0n;
1065
+ if (!plan.residual) {
1066
+ if (plan.conditions.length === 0) {
1067
+ return db.table(this.table.name).count();
1068
+ }
1069
+ return db.table(this.table.name).countWhere(plan.conditions);
1070
+ }
1071
+ }
1072
+ return BigInt(this.resolveMatched().length);
1073
+ }
1074
+ const matched = this.resolveMatched();
1075
+ let rows = applyOrderBy(matched, this._orderBy);
1076
+ if (!this._distinct) {
1077
+ rows = applyLimitOffset(rows, this._limit, this._offset);
1078
+ }
1079
+ const project = (m) => {
1080
+ if (!this._columns)
1081
+ return m.row;
1082
+ const projected = {};
1083
+ for (const col of this._columns)
1084
+ projected[col.name] = m.row[col.name];
1085
+ return projected;
1086
+ };
1087
+ let out = rows.map(project);
1088
+ if (this._distinct) {
1089
+ const columnNames = this._columns
1090
+ ? this._columns.map((c) => c.name)
1091
+ : this.table.columns.map((c) => c.name);
1092
+ out = dedupeRows(out, columnNames);
1093
+ out = applyArrayLimitOffset(out, this._limit, this._offset);
1094
+ }
1095
+ return out;
1096
+ }
1097
+ async execute() {
1098
+ return this.executeSync();
1099
+ }
1100
+ /**
1101
+ * Execute against the native engine and return the matching rows as an Arrow
1102
+ * (columnar) table — zero-copy from the engine. TypeScript-only: the
1103
+ * Rust/Python kit returns row maps.
1104
+ *
1105
+ * The native Arrow path is index-driven and needs at least one pushed-down
1106
+ * condition, so a `where`/`annSearch`/`sparseMatch` clause is required. It
1107
+ * applies only the pushed-down predicate (exact for `=`/range/`in`, a
1108
+ * superset for `contains`/`like`) and returns every column — `orderBy`,
1109
+ * `limit`, `offset`, and column projection are NOT applied. Use
1110
+ * {@link executeSync} for full query semantics.
1111
+ */
1112
+ executeArrow() {
1113
+ if (this._source) {
1114
+ throw new KitError('executeArrow is not supported for joined/CTE sources');
1115
+ }
1116
+ const db = this.kit.nativeDb;
1117
+ let conditions;
1118
+ if (this._ann) {
1119
+ conditions = [
1120
+ {
1121
+ kind: 5 /* ConditionKind.Ann */,
1122
+ columnId: this._ann.column.id,
1123
+ embedding: this._ann.vector,
1124
+ k: this._ann.k
1125
+ }
1126
+ ];
1127
+ }
1128
+ else if (this._sparse) {
1129
+ conditions = [
1130
+ {
1131
+ kind: 11 /* ConditionKind.SparseMatch */,
1132
+ columnId: this._sparse.column.id,
1133
+ sparseTokens: this._sparse.query.map((p) => p[0]),
1134
+ sparseWeights: this._sparse.query.map((p) => p[1]),
1135
+ k: this._sparse.k
1136
+ }
1137
+ ];
1138
+ }
1139
+ else if (this._where) {
1140
+ const plan = compilePredicate(this.table, this._where);
1141
+ if (plan.alwaysFalse || plan.conditions.length === 0) {
1142
+ throw new KitError('executeArrow requires a pushed-down condition; this predicate has none — use executeSync');
1143
+ }
1144
+ conditions = plan.conditions;
1145
+ }
1146
+ else {
1147
+ throw new KitError('executeArrow requires a where/annSearch/sparseMatch clause');
1148
+ }
1149
+ return tableFromIPC(db.table(this.table.name).queryArrow(conditions));
1150
+ }
1151
+ /**
1152
+ * Describe how this query's `where`/`annSearch`/`sparseMatch` clause would
1153
+ * push down to native index conditions — a diagnostic that plans but does
1154
+ * not run the query. `exact` is true when the whole predicate translated (no
1155
+ * JS residual re-filtering).
1156
+ */
1157
+ explain() {
1158
+ if (this._source) {
1159
+ return { indexAccelerated: false, exact: false, pushedConditions: [] };
1160
+ }
1161
+ if (this._ann) {
1162
+ return { indexAccelerated: true, exact: false, pushedConditions: ['Ann'] };
1163
+ }
1164
+ if (this._sparse) {
1165
+ return { indexAccelerated: true, exact: false, pushedConditions: ['SparseMatch'] };
1166
+ }
1167
+ if (!this._where) {
1168
+ return { indexAccelerated: false, exact: true, pushedConditions: [] };
1169
+ }
1170
+ const plan = compilePredicate(this.table, this._where);
1171
+ if (plan.alwaysFalse) {
1172
+ return { indexAccelerated: false, exact: true, pushedConditions: [] };
1173
+ }
1174
+ return {
1175
+ indexAccelerated: plan.conditions.length > 0,
1176
+ exact: !plan.residual,
1177
+ pushedConditions: plan.conditions.map((c) => CONDITION_LABELS[c.kind] ?? String(c.kind))
1178
+ };
1179
+ }
1180
+ }
1181
+ /**
1182
+ * Nested-loop join executed entirely in JS. The result is a {@link JoinRow}
1183
+ * keyed by table name — e.g. `{ users: { ... }, orders: { ... } }`. For a LEFT
1184
+ * JOIN with no match, the joined side is `null`.
1185
+ */
1186
+ export class JoinBuilder {
1187
+ kit;
1188
+ baseTable;
1189
+ baseWhere;
1190
+ baseSource;
1191
+ clauses = [];
1192
+ _where;
1193
+ _limit;
1194
+ _offset;
1195
+ constructor(kit, baseTable, baseWhere, baseSource) {
1196
+ this.kit = kit;
1197
+ this.baseTable = baseTable;
1198
+ this.baseWhere = baseWhere;
1199
+ this.baseSource = baseSource;
1200
+ }
1201
+ innerJoin(table, on) {
1202
+ this.clauses.push({ table, kind: 'inner', on });
1203
+ return this;
1204
+ }
1205
+ leftJoin(table, on) {
1206
+ this.clauses.push({ table, kind: 'left', on });
1207
+ return this;
1208
+ }
1209
+ crossJoin(table) {
1210
+ this.clauses.push({ table, kind: 'cross' });
1211
+ return this;
1212
+ }
1213
+ /** Post-join filter over the assembled {@link JoinRow}. */
1214
+ where(predicate) {
1215
+ this._where = predicate;
1216
+ return this;
1217
+ }
1218
+ limit(n) {
1219
+ this._limit = n;
1220
+ return this;
1221
+ }
1222
+ offset(n) {
1223
+ this._offset = n;
1224
+ return this;
1225
+ }
1226
+ executeSync() {
1227
+ const db = this.kit.nativeDb;
1228
+ const baseRows = resolveRows(db, this.baseTable, this.baseWhere, this.baseSource);
1229
+ let combos = baseRows.map((m) => ({ [this.baseTable.name]: m.row }));
1230
+ for (const clause of this.clauses) {
1231
+ // FK-equality clauses (joinEq) probe the right table by index over the
1232
+ // distinct left keys; other clauses full-scan. Either way the predicate
1233
+ // below re-checks each combination, so results are identical.
1234
+ const joinRows = joinRightRows(db, clause, combos);
1235
+ const next = [];
1236
+ for (const combo of combos) {
1237
+ if (clause.kind === 'cross') {
1238
+ for (const jr of joinRows)
1239
+ next.push({ ...combo, [clause.table.name]: jr });
1240
+ continue;
1241
+ }
1242
+ let matched = false;
1243
+ for (const jr of joinRows) {
1244
+ const candidate = { ...combo, [clause.table.name]: jr };
1245
+ if (clause.on(candidate)) {
1246
+ next.push(candidate);
1247
+ matched = true;
1248
+ }
1249
+ }
1250
+ if (clause.kind === 'left' && !matched) {
1251
+ next.push({ ...combo, [clause.table.name]: null });
1252
+ }
1253
+ }
1254
+ combos = next;
1255
+ }
1256
+ if (this._where)
1257
+ combos = combos.filter(this._where);
1258
+ return applyArrayLimitOffset(combos, this._limit, this._offset);
1259
+ }
1260
+ async execute() {
1261
+ return this.executeSync();
1262
+ }
1263
+ }
1264
+ /**
1265
+ * Grouped aggregation executed in JS. Each result row carries the group-by
1266
+ * column values plus one entry per named aggregate.
1267
+ */
1268
+ export class GroupBuilder {
1269
+ kit;
1270
+ table;
1271
+ groupColumns;
1272
+ _where;
1273
+ _source;
1274
+ _aggregates = {};
1275
+ _having;
1276
+ constructor(kit, table, groupColumns, _where, _source) {
1277
+ this.kit = kit;
1278
+ this.table = table;
1279
+ this.groupColumns = groupColumns;
1280
+ this._where = _where;
1281
+ this._source = _source;
1282
+ }
1283
+ /** Declare the named aggregates to compute per group. */
1284
+ aggregate(spec) {
1285
+ this._aggregates = spec;
1286
+ return this;
1287
+ }
1288
+ /** Filter groups after aggregation (HAVING), over the assembled group row. */
1289
+ having(predicate) {
1290
+ this._having = predicate;
1291
+ return this;
1292
+ }
1293
+ executeSync() {
1294
+ const rows = resolveRows(this.kit.nativeDb, this.table, this._where, this._source);
1295
+ const groups = new Map();
1296
+ for (const m of rows) {
1297
+ const values = this.groupColumns.map((c) => m.row[c.name]);
1298
+ const key = compositeKey(values);
1299
+ let g = groups.get(key);
1300
+ if (!g) {
1301
+ g = { values, rows: [] };
1302
+ groups.set(key, g);
1303
+ }
1304
+ g.rows.push(m);
1305
+ }
1306
+ const out = [];
1307
+ for (const g of groups.values()) {
1308
+ const result = {};
1309
+ this.groupColumns.forEach((c, i) => {
1310
+ result[c.name] = g.values[i];
1311
+ });
1312
+ for (const [alias, spec] of Object.entries(this._aggregates)) {
1313
+ result[alias] =
1314
+ spec.fn === 'count'
1315
+ ? computeCount(spec, g.rows)
1316
+ : computeAggregate(spec.fn, spec.column, g.rows, spec.distinct);
1317
+ }
1318
+ if (!this._having || this._having(result))
1319
+ out.push(result);
1320
+ }
1321
+ return out;
1322
+ }
1323
+ async execute() {
1324
+ return this.executeSync();
1325
+ }
1326
+ }
1327
+ /**
1328
+ * A scope of materialized common table expressions (CTEs). Each `with` runs its
1329
+ * builder eagerly and stores the result rows in memory so a later `selectFrom`
1330
+ * can read them as if they were a table.
1331
+ * // ponytail: full in-memory materialization — CTEs are not lazy/recursive.
1332
+ */
1333
+ export class CteScope {
1334
+ kit;
1335
+ ctes = new Map();
1336
+ constructor(kit) {
1337
+ this.kit = kit;
1338
+ }
1339
+ with(name, builder) {
1340
+ const { rows, columns } = builder._materialize();
1341
+ const table = syntheticTable(name, columns);
1342
+ const matched = rows.map((row, i) => ({ rowId: BigInt(i), row }));
1343
+ this.ctes.set(name, { rows: matched, table });
1344
+ return this;
1345
+ }
1346
+ selectFrom(name) {
1347
+ const cte = this.ctes.get(name);
1348
+ if (!cte) {
1349
+ throw new KitError(`CTE "${name}" is not defined in this scope`);
1350
+ }
1351
+ return new SelectBuilder(this.kit, cte.table)._bindSource(cte.rows);
1352
+ }
1353
+ }
1354
+ /** Compute the row passed through defaults + the PK-explicit flag for an insert. */
1355
+ function prepareInsertSync(kit, table, row) {
1356
+ const pkExplicit = table.primaryKey.every((name) => row[name] !== undefined && row[name] !== null);
1357
+ const defaulted = prepareInsertRowSync(kit, table, row);
1358
+ validateRow(table, defaulted);
1359
+ const pkValue = pkValueFromRow(table, defaulted);
1360
+ return { defaulted, pkValue, pkExplicit };
1361
+ }
1362
+ export class InsertBuilder {
1363
+ kit;
1364
+ table;
1365
+ _row;
1366
+ _returning;
1367
+ _onConflict;
1368
+ constructor(kit, table) {
1369
+ this.kit = kit;
1370
+ this.table = table;
1371
+ }
1372
+ values(row) {
1373
+ this._row = row;
1374
+ return this;
1375
+ }
1376
+ /**
1377
+ * Insert many rows in a single transaction. Each row still passes through
1378
+ * defaults, validation, and constraint checks, but the whole batch commits
1379
+ * once — far faster than a row-at-a-time loop for bulk loads.
1380
+ */
1381
+ valuesMany(rows) {
1382
+ return new InsertManyBuilder(this.kit, this.table, rows);
1383
+ }
1384
+ returning(...columns) {
1385
+ const next = new InsertBuilder(this.kit, this.table);
1386
+ next._row = this._row;
1387
+ next._returning = columns;
1388
+ next._onConflict = this._onConflict;
1389
+ return next;
1390
+ }
1391
+ // `onConflict*` mutate this builder because the result type does not
1392
+ // change; `returning` clones because it changes the generic result type.
1393
+ onConflictDoNothing() {
1394
+ this._onConflict = { kind: 'do_nothing' };
1395
+ return this;
1396
+ }
1397
+ onConflictDoUpdate(patch) {
1398
+ this._onConflict = { kind: 'do_update', patch: patch };
1399
+ return this;
1400
+ }
1401
+ executeSync() {
1402
+ if (this._row === undefined) {
1403
+ throw new KitError('values() must be called before execute()');
1404
+ }
1405
+ const { defaulted, pkValue, pkExplicit } = prepareInsertSync(this.kit, this.table, this._row);
1406
+ if (this._onConflict) {
1407
+ const existingRowJs = findByPk(this.kit.nativeDb, this.table, pkValue);
1408
+ if (existingRowJs) {
1409
+ const existingRow = rowFromRowJs(this.table, existingRowJs);
1410
+ if (this._onConflict.kind === 'do_nothing') {
1411
+ return projectRow(existingRow, this._returning);
1412
+ }
1413
+ const patch = this._onConflict.patch;
1414
+ const kit = makeConstraintKit(this.kit);
1415
+ let merged;
1416
+ runSyncTxn(this.kit, (txn) => {
1417
+ merged = applyUpdateInTxn(kit, txn, this.table, existingRow, existingRowJs.rowId, patch);
1418
+ });
1419
+ return projectRow(merged, this._returning);
1420
+ }
1421
+ }
1422
+ const kit = makeConstraintKit(this.kit);
1423
+ runSyncTxn(this.kit, (txn) => {
1424
+ enforceForeignKeys(kit, txn, this.table, defaulted);
1425
+ stageUniqueGuards(kit, txn, this.table, defaulted, pkValue);
1426
+ stagePkGuard(kit, txn, this.table, pkValue, pkExplicit);
1427
+ txn.put(this.table.name, toCells(this.table, defaulted));
1428
+ });
1429
+ return projectRow(defaulted, this._returning);
1430
+ }
1431
+ async execute() {
1432
+ return this.executeSync();
1433
+ }
1434
+ }
1435
+ export class InsertManyBuilder {
1436
+ kit;
1437
+ table;
1438
+ rows;
1439
+ constructor(kit, table, rows) {
1440
+ this.kit = kit;
1441
+ this.table = table;
1442
+ this.rows = rows;
1443
+ }
1444
+ executeSync() {
1445
+ const kit = makeConstraintKit(this.kit);
1446
+ const results = [];
1447
+ runSyncTxn(this.kit, (txn) => {
1448
+ results.length = 0;
1449
+ // For a single-column PK, load the existing PKs once so the per-row
1450
+ // duplicate check is O(1) instead of a per-row table scan.
1451
+ const pkSeen = this.table.primaryKey.length === 1
1452
+ ? new Set(fullScanRows(this.kit.nativeDb, this.table).map((m) => encodedPk(pkValueFromRow(this.table, m.row))))
1453
+ : undefined;
1454
+ for (const input of this.rows) {
1455
+ const { defaulted, pkValue, pkExplicit } = prepareInsertSync(this.kit, this.table, input);
1456
+ enforceForeignKeys(kit, txn, this.table, defaulted);
1457
+ stageUniqueGuards(kit, txn, this.table, defaulted, pkValue);
1458
+ stagePkGuard(kit, txn, this.table, pkValue, pkExplicit, pkSeen);
1459
+ results.push(defaulted);
1460
+ }
1461
+ // Stage all main-table rows in one packed crossing instead of a
1462
+ // per-row NAPI `put`. Guard rows above are already staged into the
1463
+ // same transaction; everything commits atomically.
1464
+ if (results.length > 0) {
1465
+ txn.putPacked(this.table.name, packRows(this.table, results));
1466
+ }
1467
+ });
1468
+ return results;
1469
+ }
1470
+ async execute() {
1471
+ return this.executeSync();
1472
+ }
1473
+ }
1474
+ export class UpdateBuilder {
1475
+ kit;
1476
+ table;
1477
+ _patch;
1478
+ _where;
1479
+ _returning;
1480
+ constructor(kit, table) {
1481
+ this.kit = kit;
1482
+ this.table = table;
1483
+ }
1484
+ set(patch) {
1485
+ this._patch = patch;
1486
+ return this;
1487
+ }
1488
+ where(predicate) {
1489
+ this._where = predicate;
1490
+ return this;
1491
+ }
1492
+ returning(...columns) {
1493
+ const next = new UpdateBuilder(this.kit, this.table);
1494
+ next._patch = this._patch;
1495
+ next._where = this._where;
1496
+ next._returning = columns;
1497
+ return next;
1498
+ }
1499
+ executeSync() {
1500
+ if (this._patch === undefined) {
1501
+ throw new KitError('set() must be called before execute()');
1502
+ }
1503
+ const db = this.kit.nativeDb;
1504
+ const matches = this._where
1505
+ ? evaluatePredicate(db, this.table, this._where)
1506
+ : fullScanRows(db, this.table);
1507
+ const patch = this._patch;
1508
+ const kit = makeConstraintKit(this.kit);
1509
+ const updated = [];
1510
+ runSyncTxn(this.kit, (txn) => {
1511
+ for (const matched of matches) {
1512
+ const merged = applyUpdateInTxn(kit, txn, this.table, matched.row, matched.rowId, patch);
1513
+ updated.push(merged);
1514
+ }
1515
+ });
1516
+ return updated.map((row) => projectRow(row, this._returning));
1517
+ }
1518
+ async execute() {
1519
+ return this.executeSync();
1520
+ }
1521
+ }
1522
+ export class DeleteBuilder {
1523
+ kit;
1524
+ table;
1525
+ _where;
1526
+ _returning;
1527
+ constructor(kit, table) {
1528
+ this.kit = kit;
1529
+ this.table = table;
1530
+ }
1531
+ where(predicate) {
1532
+ this._where = predicate;
1533
+ return this;
1534
+ }
1535
+ returning(...columns) {
1536
+ const next = new DeleteBuilder(this.kit, this.table);
1537
+ next._where = this._where;
1538
+ next._returning = columns;
1539
+ return next;
1540
+ }
1541
+ executeSync() {
1542
+ const db = this.kit.nativeDb;
1543
+ const matches = this._where
1544
+ ? evaluatePredicate(db, this.table, this._where)
1545
+ : fullScanRows(db, this.table);
1546
+ const kit = makeConstraintKit(this.kit);
1547
+ if (this._returning) {
1548
+ const projected = matches.map((m) => projectRow(m.row, this._returning));
1549
+ if (this.table.primaryKey.length === 1 &&
1550
+ this.table.unique.length === 0 &&
1551
+ !isReferencedTable(this.kit.schema, this.table.name)) {
1552
+ if (matches.length > 0) {
1553
+ runSyncTxn(this.kit, (txn) => {
1554
+ txn.deletePacked(this.table.name, packRowIds(matches.map((m) => m.rowId)));
1555
+ });
1556
+ }
1557
+ return projected;
1558
+ }
1559
+ runSyncTxn(this.kit, (txn) => {
1560
+ for (const matched of matches) {
1561
+ const pkValue = pkValueFromRow(this.table, matched.row);
1562
+ // Reuse the row already fetched by the scan to avoid an O(n^2) re-read.
1563
+ planDelete(kit, txn, this.table, pkValue, {
1564
+ row: matched.row,
1565
+ rowId: matched.rowId
1566
+ });
1567
+ }
1568
+ });
1569
+ return projected;
1570
+ }
1571
+ // Fast path: a single-column-PK table with no unique constraints and no
1572
+ // incoming foreign keys has no guard rows and no cascade work, so a delete
1573
+ // reduces to dropping the matched row ids. Batch them in one packed
1574
+ // crossing instead of per-row planDelete (which would also run a
1575
+ // __kit_unique_keys / __kit_row_guards query per row).
1576
+ if (this.table.primaryKey.length === 1 &&
1577
+ this.table.unique.length === 0 &&
1578
+ !isReferencedTable(this.kit.schema, this.table.name)) {
1579
+ if (matches.length === 0)
1580
+ return 0n;
1581
+ runSyncTxn(this.kit, (txn) => {
1582
+ txn.deletePacked(this.table.name, packRowIds(matches.map((m) => m.rowId)));
1583
+ });
1584
+ return BigInt(matches.length);
1585
+ }
1586
+ let deleted = 0;
1587
+ runSyncTxn(this.kit, (txn) => {
1588
+ deleted = 0;
1589
+ for (const matched of matches) {
1590
+ const pkValue = pkValueFromRow(this.table, matched.row);
1591
+ // Reuse the row already fetched by the scan to avoid an O(n^2) re-read.
1592
+ planDelete(kit, txn, this.table, pkValue, {
1593
+ row: matched.row,
1594
+ rowId: matched.rowId
1595
+ });
1596
+ deleted++;
1597
+ }
1598
+ });
1599
+ return BigInt(deleted);
1600
+ }
1601
+ async execute() {
1602
+ return this.executeSync();
1603
+ }
1604
+ }
1605
+ KitDatabase.prototype.selectFrom = function (table) {
1606
+ return new SelectBuilder(this, table);
1607
+ };
1608
+ KitDatabase.prototype.insertInto = function (table) {
1609
+ return new InsertBuilder(this, table);
1610
+ };
1611
+ KitDatabase.prototype.updateTable = function (table) {
1612
+ return new UpdateBuilder(this, table);
1613
+ };
1614
+ KitDatabase.prototype.deleteFrom = function (table) {
1615
+ return new DeleteBuilder(this, table);
1616
+ };
1617
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1618
+ KitDatabase.prototype.with = function (name, builder) {
1619
+ return new CteScope(this).with(name, builder);
1620
+ };
1621
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1622
+ KitDatabase.prototype.incrementalAggregate = function (table, agg, column, filter) {
1623
+ const spec = this.schema.table(table);
1624
+ let columnId;
1625
+ if (column !== undefined) {
1626
+ const col = spec.columns.find((c) => c.name === column);
1627
+ if (!col)
1628
+ throw new KitError(`unknown column '${column}' on table '${table}'`);
1629
+ columnId = col.id;
1630
+ }
1631
+ else if (agg !== 'count') {
1632
+ throw new KitError(`incremental ${agg} requires a column`);
1633
+ }
1634
+ let conditions = [];
1635
+ if (filter) {
1636
+ const plan = compilePredicate(spec, filter);
1637
+ if (plan.alwaysFalse || plan.residual || plan.conditions.length === 0) {
1638
+ throw new KitError('filter has no exact pushdown; an incremental aggregate cannot apply it');
1639
+ }
1640
+ conditions = plan.conditions;
1641
+ }
1642
+ const raw = this.nativeDb.table(table).incrementalAggregate(agg, columnId, conditions);
1643
+ return JSON.parse(raw);
1644
+ };
1645
+ //# sourceMappingURL=query.js.map