@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.
- package/dist/constraints.d.ts +35 -0
- package/dist/constraints.d.ts.map +1 -0
- package/dist/constraints.js +404 -0
- package/dist/constraints.js.map +1 -0
- package/dist/db.d.ts +135 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +495 -0
- package/dist/db.js.map +1 -0
- package/dist/defaults.d.ts +26 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +56 -0
- package/dist/defaults.js.map +1 -0
- package/dist/errors.d.ts +54 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +104 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/internalTables.d.ts +63 -0
- package/dist/internalTables.d.ts.map +1 -0
- package/dist/internalTables.js +60 -0
- package/dist/internalTables.js.map +1 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +84 -0
- package/dist/keys.js.map +1 -0
- package/dist/migrate.d.ts +132 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +1004 -0
- package/dist/migrate.js.map +1 -0
- package/dist/packing.d.ts +12 -0
- package/dist/packing.d.ts.map +1 -0
- package/dist/packing.js +137 -0
- package/dist/packing.js.map +1 -0
- package/dist/query.d.ts +423 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +1645 -0
- package/dist/query.js.map +1 -0
- package/dist/remote.d.ts +29 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +42 -0
- package/dist/remote.js.map +1 -0
- package/dist/rows.d.ts +5 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +38 -0
- package/dist/rows.js.map +1 -0
- package/dist/schema.d.ts +91 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +206 -0
- package/dist/schema.js.map +1 -0
- package/dist/tsv.d.ts +4 -0
- package/dist/tsv.d.ts.map +1 -0
- package/dist/tsv.js +102 -0
- package/dist/tsv.js.map +1 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +3 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +98 -0
- package/dist/validation.js.map +1 -0
- 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
|