@lunora/sql-store 0.0.0 → 1.0.0-alpha.10

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.
@@ -0,0 +1,1784 @@
1
+ import { aggregateTableName, rankTableName, ftsTableName, hasTrigger, runRowValidators, assertFlatPredicate, mergeWhere, sortColumnName, resolveRankPartition, encodePartitionKey, decodeCursor, RANK_TIEBREAK, CountRlsUnsupportedError, normalizeIdStructurally, assertValidClientId, aggregateSqlFunction, softDeleteScope, compileWhereSql, normalizeOrderKeys, buildSeekWhere, resolveRelationPredicates, resolveWith, applySelect, encodeCursor, NotFoundError, applyOnDelete, ConflictError, normalizeCountArgument, selectIndexForCount, encodeAggregateKey, selectIndexForAggregate, readAggregateValue, renderSql, runTriggers, matchesRankStaticWhere, stringifySearchText, selectIndexForGroupBy, fanOutScalarCounts, matchesStaticWhere, coerceAggregateNumber, foldAggregateTally, NotUniqueError, throwingScheduler, tokenizeSearch, buildFtsMatch, scoreDocument } from '@lunora/do';
2
+ import { sql } from 'drizzle-orm';
3
+ import { sqliteDecode, effectiveColumnKind, sqliteEncode } from './decodeBigint-Dedu92k4.mjs';
4
+
5
+ const physicalColumn = (field) => field === "_id" || field === "id" ? "id" : field;
6
+ const columnRefSql = (field) => sql`${sql.identifier(physicalColumn(field))}`;
7
+ const ID_ORDER_FIELDS = /* @__PURE__ */ new Set(["_id", "id"]);
8
+ const nullSafeEqualsSql = (engine, reference, value) => {
9
+ if (engine === "postgres") {
10
+ return sql`${reference} IS NOT DISTINCT FROM ${value}`;
11
+ }
12
+ if (engine === "mysql") {
13
+ return sql`${reference} <=> ${value}`;
14
+ }
15
+ return sql`${reference} IS ${value}`;
16
+ };
17
+ const compileOrderBySql = (keys) => {
18
+ const parts = keys.map((key) => sql`${columnRefSql(key.field)} ${sql.raw(key.direction === "desc" ? "DESC" : "ASC")}`);
19
+ if (!keys.some((key) => ID_ORDER_FIELDS.has(key.field))) {
20
+ parts.push(sql`${columnRefSql("id")} ASC`);
21
+ }
22
+ return sql.join(parts, sql`, `);
23
+ };
24
+ const queryAll = (exec, dialect, query) => {
25
+ const { params, sql: text } = renderSql(dialect.name, query);
26
+ return exec.all(text, params);
27
+ };
28
+ const queryRun = (exec, dialect, query) => {
29
+ const { params, sql: text } = renderSql(dialect.name, query);
30
+ return exec.run(text, params);
31
+ };
32
+ const createIndexIfNotExists = async (exec, dialect, spec) => {
33
+ const unique = spec.unique ? sql`UNIQUE ` : sql``;
34
+ if (dialect.name === "mysql") {
35
+ try {
36
+ await queryRun(exec, dialect, sql`CREATE ${unique}INDEX ${sql.identifier(spec.name)} ON ${sql.identifier(spec.table)} (${spec.columns})`);
37
+ } catch (error) {
38
+ if (error.errno !== 1061) {
39
+ throw error;
40
+ }
41
+ }
42
+ return;
43
+ }
44
+ await queryRun(exec, dialect, sql`CREATE ${unique}INDEX IF NOT EXISTS ${sql.identifier(spec.name)} ON ${sql.identifier(spec.table)} (${spec.columns})`);
45
+ };
46
+ const MAX_TRIGGER_DEPTH = 50;
47
+ const serializeColumnValue = sqliteEncode;
48
+ const ftsAvailabilityCache = /* @__PURE__ */ new WeakMap();
49
+ const isFtsAvailable = (exec) => {
50
+ const cached = ftsAvailabilityCache.get(exec);
51
+ if (cached !== void 0) {
52
+ return cached;
53
+ }
54
+ const probe = (async () => {
55
+ let available;
56
+ try {
57
+ await exec.run(`CREATE VIRTUAL TABLE IF NOT EXISTS "__lunora_fts_probe" USING fts5(x)`, []);
58
+ available = true;
59
+ } catch {
60
+ available = false;
61
+ } finally {
62
+ try {
63
+ await exec.run(`DROP TABLE IF EXISTS "__lunora_fts_probe"`, []);
64
+ } catch {
65
+ }
66
+ }
67
+ return available;
68
+ })();
69
+ ftsAvailabilityCache.set(exec, probe);
70
+ return probe;
71
+ };
72
+ const tableColumns = (definition) => {
73
+ const columns = [];
74
+ for (const [field, validator] of Object.entries(definition.shape)) {
75
+ const column = validator._meta?.column;
76
+ if (column) {
77
+ columns.push([field, column]);
78
+ }
79
+ }
80
+ return columns;
81
+ };
82
+ const decodeGlobalRow = (definition, row) => {
83
+ const decoded = {};
84
+ for (const [field, validator] of Object.entries(definition.shape)) {
85
+ const raw = row[field];
86
+ if (raw === void 0) {
87
+ continue;
88
+ }
89
+ decoded[field] = sqliteDecode(raw, effectiveColumnKind(validator));
90
+ }
91
+ decoded["_id"] = row["id"];
92
+ decoded["_creationTime"] = row["_creationTime"];
93
+ return decoded;
94
+ };
95
+ const decodeRow = (definition, row) => {
96
+ if (!row) {
97
+ return null;
98
+ }
99
+ return decodeGlobalRow(definition, row);
100
+ };
101
+ const applyInsertDefaults = (definition, document, auth) => {
102
+ const result = { ...document };
103
+ for (const [field, column] of tableColumns(definition)) {
104
+ if (column.serverDefault) {
105
+ result[field] = column.serverDefault({ auth });
106
+ continue;
107
+ }
108
+ if (result[field] !== void 0) {
109
+ continue;
110
+ }
111
+ if (column.defaultFn) {
112
+ result[field] = column.defaultFn();
113
+ } else if ("defaultValue" in column) {
114
+ result[field] = column.defaultValue;
115
+ }
116
+ }
117
+ return result;
118
+ };
119
+ const applyOnUpdate = (definition, provided, target, auth) => {
120
+ for (const [field, column] of tableColumns(definition)) {
121
+ if (column.serverDefault) {
122
+ if (field in provided) {
123
+ target[field] = column.serverDefault({ auth });
124
+ }
125
+ continue;
126
+ }
127
+ if (column.onUpdateFn && !(field in provided)) {
128
+ target[field] = column.onUpdateFn();
129
+ }
130
+ }
131
+ };
132
+ const TABLE_NAME_CACHE_CAPACITY = 128;
133
+ const createTableNameCache = () => {
134
+ const map = /* @__PURE__ */ new Map();
135
+ return {
136
+ delete: (id) => {
137
+ map.delete(id);
138
+ },
139
+ get: (id) => {
140
+ const hit = map.get(id);
141
+ if (hit === void 0) {
142
+ return void 0;
143
+ }
144
+ map.delete(id);
145
+ map.set(id, hit);
146
+ return hit;
147
+ },
148
+ set: (id, table) => {
149
+ if (map.has(id)) {
150
+ map.delete(id);
151
+ } else if (map.size >= TABLE_NAME_CACHE_CAPACITY) {
152
+ const oldest = map.keys().next().value;
153
+ if (oldest !== void 0) {
154
+ map.delete(oldest);
155
+ }
156
+ }
157
+ map.set(id, table);
158
+ }
159
+ };
160
+ };
161
+ const tableNameFromId = async (exec, dialect, schema, id, cache) => {
162
+ const cached = cache.get(id);
163
+ if (cached !== void 0) {
164
+ return cached;
165
+ }
166
+ const candidates = [];
167
+ for (const [tableName, definition] of Object.entries(schema.tables)) {
168
+ if (definition.shardMode !== void 0 && definition.shardMode.kind !== "global") {
169
+ continue;
170
+ }
171
+ candidates.push(tableName);
172
+ }
173
+ const probes = await Promise.all(
174
+ candidates.map(async (tableName) => {
175
+ const rows = await queryAll(exec, dialect, sql`SELECT 1 FROM ${sql.identifier(tableName)} WHERE ${sql.identifier("id")} = ${id} LIMIT 1`);
176
+ return { found: rows.length > 0, tableName };
177
+ })
178
+ );
179
+ for (const result of probes) {
180
+ if (result.found) {
181
+ cache.set(id, result.tableName);
182
+ return result.tableName;
183
+ }
184
+ }
185
+ return void 0;
186
+ };
187
+ const aggregateScalar = (value) => value === null || value === void 0 ? null : Number(value);
188
+ const mapGroupByRows = (by, rows) => rows.map((row) => {
189
+ const key = {};
190
+ for (const field of by) {
191
+ key[field] = row[field] ?? null;
192
+ }
193
+ return { key, value: aggregateScalar(row.value) };
194
+ });
195
+ const decodeRows = (definition, rows) => {
196
+ const documents = [];
197
+ for (const row of rows) {
198
+ const decoded = decodeRow(definition, row);
199
+ if (decoded) {
200
+ documents.push(decoded);
201
+ }
202
+ }
203
+ return documents;
204
+ };
205
+ const searchViaFts = async (exec, dialect, definition, tableName, search, limit) => {
206
+ const tokens = tokenizeSearch(search.query);
207
+ if (tokens.length === 0) {
208
+ return [];
209
+ }
210
+ const ftName = ftsTableName(tableName, search.indexName);
211
+ const conditions = [sql`f.${sql.identifier("__text__")} MATCH ${buildFtsMatch(tokens)}`];
212
+ for (const filter of search.filters) {
213
+ conditions.push(sql`m.${columnRefSql(filter.field)} = ${serializeColumnValue(filter.value)}`);
214
+ }
215
+ if (definition.softDeleteMode) {
216
+ conditions.push(sql`m.${columnRefSql(definition.softDeleteMode.field)} IS NULL`);
217
+ }
218
+ let query = sql`SELECT m.* FROM ${sql.identifier(ftName)} f JOIN ${sql.identifier(tableName)} m ON m.${sql.identifier("id")} = f.${sql.identifier("__id__")} WHERE ${sql.join(conditions, sql` AND `)} ORDER BY f.rank, m.${sql.identifier("_creationTime")} DESC`;
219
+ if (typeof limit === "number") {
220
+ query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
221
+ }
222
+ const rows = await queryAll(exec, dialect, query);
223
+ return decodeRows(definition, rows);
224
+ };
225
+ const searchViaScan = async (exec, dialect, definition, tableName, search, limit) => {
226
+ const tokens = tokenizeSearch(search.query);
227
+ if (tokens.length === 0) {
228
+ return [];
229
+ }
230
+ const conditions = search.filters.map((filter) => sql`${columnRefSql(filter.field)} = ${serializeColumnValue(filter.value)}`);
231
+ if (definition.softDeleteMode) {
232
+ conditions.push(sql`${columnRefSql(definition.softDeleteMode.field)} IS NULL`);
233
+ }
234
+ let query = sql`SELECT * FROM ${sql.identifier(tableName)}`;
235
+ if (conditions.length > 0) {
236
+ query = sql`${query} WHERE ${sql.join(conditions, sql` AND `)}`;
237
+ }
238
+ const rows = await queryAll(exec, dialect, query);
239
+ const scored = [];
240
+ for (const record of decodeRows(definition, rows)) {
241
+ const score = scoreDocument(stringifySearchText(record[search.field]), tokens);
242
+ if (score > 0) {
243
+ scored.push({ creationTime: typeof record["_creationTime"] === "number" ? record["_creationTime"] : 0, doc: record, score });
244
+ }
245
+ }
246
+ scored.sort((a, b) => b.score - a.score || b.creationTime - a.creationTime);
247
+ const documents = scored.map((entry) => entry.doc);
248
+ return typeof limit === "number" ? documents.slice(0, Math.max(0, Math.floor(limit))) : documents;
249
+ };
250
+ const createSearchBuilder = (search, tableName) => {
251
+ const builder = {
252
+ eq: (field, value) => {
253
+ if (!search.definition.filterFields?.includes(field)) {
254
+ throw new Error(`field "${field}" is not a filter field of search index "${search.indexName}" on table "${tableName}"`);
255
+ }
256
+ search.filters.push({ field, value });
257
+ return builder;
258
+ },
259
+ search: (field, query) => {
260
+ if (field !== search.definition.field) {
261
+ throw new Error(`search index "${search.indexName}" on table "${tableName}" indexes "${search.definition.field}", not "${field}"`);
262
+ }
263
+ const stage = search;
264
+ stage.field = field;
265
+ stage.query = query;
266
+ stage.hasQuery = true;
267
+ return builder;
268
+ }
269
+ };
270
+ return builder;
271
+ };
272
+ const andBranch = (conditions) => conditions.length === 1 ? conditions[0] : sql`(${sql.join(conditions, sql` AND `)})`;
273
+ const buildRankBeforeBranches = (engine, index, sortColumns, own, rowId) => {
274
+ const branches = [];
275
+ for (let pivot = 0; pivot < sortColumns.length + 1; pivot += 1) {
276
+ const conditions = sortColumns.slice(0, pivot).map((prefixColumn) => nullSafeEqualsSql(engine, sql`${sql.identifier(prefixColumn)}`, own[prefixColumn]));
277
+ const column = sortColumns[pivot];
278
+ const sortKey = index.sortBy[pivot];
279
+ if (pivot < sortColumns.length && column !== void 0 && sortKey !== void 0) {
280
+ conditions.push(sql`${sql.identifier(column)} ${sql.raw(sortKey.direction === "desc" ? ">" : "<")} ${own[column]}`);
281
+ } else {
282
+ conditions.push(sql`${sql.identifier(RANK_TIEBREAK)} < ${rowId}`);
283
+ }
284
+ branches.push(andBranch(conditions));
285
+ }
286
+ return branches.length > 0 ? sql.join(branches, sql` OR `) : void 0;
287
+ };
288
+ const buildRankCursorSeek = (engine, columns, decoded) => {
289
+ if (decoded.length !== columns.length) {
290
+ return void 0;
291
+ }
292
+ const branches = [];
293
+ for (const [pivot, col] of columns.entries()) {
294
+ const conditions = [];
295
+ for (let prefix = 0; prefix < pivot; prefix += 1) {
296
+ const prefixCol = columns[prefix];
297
+ if (prefixCol === void 0) {
298
+ continue;
299
+ }
300
+ conditions.push(nullSafeEqualsSql(engine, sql`${sql.identifier(prefixCol.column)}`, decoded[prefix]));
301
+ }
302
+ conditions.push(sql`${sql.identifier(col.column)} ${sql.raw(col.direction === "desc" ? "<" : ">")} ${decoded[pivot]}`);
303
+ branches.push(andBranch(conditions));
304
+ }
305
+ return sql`(${sql.join(branches, sql` OR `)})`;
306
+ };
307
+ const rankPageColumns = (index, sortColumns) => {
308
+ if (index.sortBy.length === 0) {
309
+ throw new Error(`rankIndex "${index.name}" requires at least one "sortBy" column for stable pagination`);
310
+ }
311
+ const columns = [{ column: "__partition__", direction: "asc" }];
312
+ for (const [i, sortKey] of index.sortBy.entries()) {
313
+ columns.push({ column: sortColumns[i] ?? sortColumnName(i), direction: sortKey.direction });
314
+ }
315
+ columns.push({ column: RANK_TIEBREAK, direction: "asc" });
316
+ return columns;
317
+ };
318
+ const hydrateRankRows = async (exec, dialect, definition, tableName, ids) => {
319
+ const IN_CHUNK_SIZE = 50;
320
+ const chunks = [];
321
+ for (let cursor = 0; cursor < ids.length; cursor += IN_CHUNK_SIZE) {
322
+ chunks.push(ids.slice(cursor, cursor + IN_CHUNK_SIZE));
323
+ }
324
+ const fetched = await Promise.all(
325
+ chunks.map(async (chunk) => {
326
+ const list = sql.join(
327
+ chunk.map((value) => sql`${value}`),
328
+ sql`, `
329
+ );
330
+ return queryAll(exec, dialect, sql`SELECT * FROM ${sql.identifier(tableName)} WHERE ${sql.identifier("id")} IN (${list})`);
331
+ })
332
+ );
333
+ const byId = /* @__PURE__ */ new Map();
334
+ for (const rows of fetched) {
335
+ for (const row of rows) {
336
+ byId.set(row["id"], row);
337
+ }
338
+ }
339
+ const documents = [];
340
+ for (const id of ids) {
341
+ const decoded = decodeRow(definition, byId.get(id));
342
+ if (decoded) {
343
+ documents.push(decoded);
344
+ }
345
+ }
346
+ return documents;
347
+ };
348
+ const encodeRankCursor = (cursorValues) => {
349
+ const json = JSON.stringify(cursorValues);
350
+ const bytes = new TextEncoder().encode(json);
351
+ let binary = "";
352
+ for (const byte of bytes) {
353
+ binary += String.fromCodePoint(byte);
354
+ }
355
+ return btoa(binary);
356
+ };
357
+ const BACKFILL_BATCH_SIZE = 500;
358
+ const forEachRowPaged = async (exec, dialect, definition, tableName, onDoc) => {
359
+ let cursorId;
360
+ let hasMore = true;
361
+ while (hasMore) {
362
+ const pageRows = cursorId === void 0 ? (
363
+ // eslint-disable-next-line no-await-in-loop -- keyset paging is inherently sequential: each page's WHERE depends on the prior page's last id.
364
+ await queryAll(
365
+ exec,
366
+ dialect,
367
+ sql`SELECT * FROM ${sql.identifier(tableName)} ORDER BY ${sql.identifier("id")} ASC LIMIT ${sql.raw(String(BACKFILL_BATCH_SIZE))}`
368
+ )
369
+ ) : (
370
+ // eslint-disable-next-line no-await-in-loop -- keyset paging is inherently sequential: each page's WHERE depends on the prior page's last id.
371
+ await queryAll(
372
+ exec,
373
+ dialect,
374
+ sql`SELECT * FROM ${sql.identifier(tableName)} WHERE ${sql.identifier("id")} > ${cursorId} ORDER BY ${sql.identifier("id")} ASC LIMIT ${sql.raw(String(BACKFILL_BATCH_SIZE))}`
375
+ )
376
+ );
377
+ for (const row of pageRows) {
378
+ const decoded = decodeRow(definition, row);
379
+ if (decoded) {
380
+ onDoc(decoded);
381
+ }
382
+ }
383
+ cursorId = pageRows.at(-1)?.["id"];
384
+ hasMore = pageRows.length === BACKFILL_BATCH_SIZE;
385
+ }
386
+ };
387
+ const globalColumnAffinity = (validator, dialect) => dialect.columnType(effectiveColumnKind(validator));
388
+ const globalTableColumnsDdl = (definition, dialect) => {
389
+ const fieldColumns = [];
390
+ for (const [field, validator] of Object.entries(definition.shape)) {
391
+ if (!validator._meta?.column) {
392
+ continue;
393
+ }
394
+ const notNull = validator._meta.column.notNull && validator.kind !== "optional" ? " NOT NULL" : "";
395
+ fieldColumns.push(sql`${sql.identifier(field)} ${sql.raw(`${globalColumnAffinity(validator, dialect)}${notNull}`)}`);
396
+ }
397
+ const frameworkColumns = dialect.frameworkColumns().map((column) => sql`${sql.identifier(column.name)} ${sql.raw(column.type)}`);
398
+ return sql.join([...frameworkColumns, ...fieldColumns], sql`, `);
399
+ };
400
+ const createGlobalTableIndexes = async (exec, tableName, definition, dialect) => {
401
+ const indexRef = (field) => {
402
+ const reference = columnRefSql(field);
403
+ const validator = definition.shape[field];
404
+ const prefix = validator && dialect.indexKeyPrefix ? dialect.indexKeyPrefix(effectiveColumnKind(validator)) : void 0;
405
+ return prefix === void 0 ? reference : sql`${reference}(${sql.raw(String(prefix))})`;
406
+ };
407
+ for (const index of definition.indexes) {
408
+ const expressions = sql.join(
409
+ index.fields.map((field) => indexRef(field)),
410
+ sql`, `
411
+ );
412
+ await createIndexIfNotExists(exec, dialect, {
413
+ columns: expressions,
414
+ name: `${tableName}_${index.name}`,
415
+ table: tableName,
416
+ unique: index.unique ?? false
417
+ });
418
+ }
419
+ for (const [field, column] of tableColumns(definition)) {
420
+ if (!column.unique) {
421
+ continue;
422
+ }
423
+ await createIndexIfNotExists(exec, dialect, { columns: indexRef(field), name: `${tableName}_unique_${field}`, table: tableName, unique: true });
424
+ }
425
+ };
426
+ const runSqlGlobalTableMigrations = async (exec, schema, dialect) => {
427
+ for (const [tableName, definition] of Object.entries(schema.tables)) {
428
+ if (definition.shardMode?.kind !== "global") {
429
+ continue;
430
+ }
431
+ const columns = globalTableColumnsDdl(definition, dialect);
432
+ await queryRun(exec, dialect, sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(tableName)} (${columns})`);
433
+ await createGlobalTableIndexes(exec, tableName, definition, dialect);
434
+ }
435
+ };
436
+ const runSqlAggregateMigrations = async (exec, schema, dialect) => {
437
+ const { integer, key, real } = dialect.companionTypes;
438
+ for (const [tableName, definition] of Object.entries(schema.tables)) {
439
+ const indexes = definition.aggregateIndexes;
440
+ if (!indexes || indexes.length === 0) {
441
+ continue;
442
+ }
443
+ for (const index of indexes) {
444
+ const aggTable = aggregateTableName(tableName, index.name);
445
+ await queryRun(
446
+ exec,
447
+ dialect,
448
+ sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(aggTable)} (${sql.identifier("__key__")} ${sql.raw(key)} PRIMARY KEY, ${sql.identifier("__value__")} ${sql.raw(real)}, ${sql.identifier("__count__")} ${sql.raw(integer)} NOT NULL DEFAULT 0)`
449
+ );
450
+ if (dialect.name === "sqlite") {
451
+ const columns = await queryAll(exec, dialect, sql`PRAGMA table_info(${sql.identifier(aggTable)})`);
452
+ if (!columns.some((column) => column["name"] === "__count__")) {
453
+ await queryRun(
454
+ exec,
455
+ dialect,
456
+ sql`ALTER TABLE ${sql.identifier(aggTable)} ADD COLUMN ${sql.identifier("__count__")} ${sql.raw(integer)} NOT NULL DEFAULT 0`
457
+ );
458
+ }
459
+ }
460
+ }
461
+ }
462
+ };
463
+ const rankIndexColumn = (dialect, column, direction, needsPrefix) => {
464
+ const reference = dialect.name === "mysql" && needsPrefix ? sql`${sql.identifier(column)}(191)` : sql`${sql.identifier(column)}`;
465
+ return sql`${reference} ${sql.raw(direction)}`;
466
+ };
467
+ const rankBtreeColumns = (dialect, index, definition) => {
468
+ const columns = [rankIndexColumn(dialect, "__partition__", "ASC", true)];
469
+ for (const [i, sortKey] of index.sortBy.entries()) {
470
+ const validator = definition.shape[sortKey.field];
471
+ const needsPrefix = validator !== void 0 && dialect.indexKeyPrefix?.(effectiveColumnKind(validator)) !== void 0;
472
+ columns.push(rankIndexColumn(dialect, sortColumnName(i), sortKey.direction === "desc" ? "DESC" : "ASC", needsPrefix));
473
+ }
474
+ columns.push(rankIndexColumn(dialect, "__id__", "ASC", true));
475
+ return columns;
476
+ };
477
+ const rankSortColumnDefs = (dialect, index, definition) => index.sortBy.map((sortKey, i) => {
478
+ const validator = definition.shape[sortKey.field];
479
+ const columnType = dialect.columnType(validator ? effectiveColumnKind(validator) : void 0);
480
+ return sql`${sql.identifier(sortColumnName(i))} ${sql.raw(columnType)}`;
481
+ });
482
+ const runSqlRankMigrations = async (exec, schema, dialect) => {
483
+ const { key } = dialect.companionTypes;
484
+ for (const [tableName, definition] of Object.entries(schema.tables)) {
485
+ const indexes = definition.rankIndexes;
486
+ if (!indexes || indexes.length === 0) {
487
+ continue;
488
+ }
489
+ for (const index of indexes) {
490
+ const rankTable = rankTableName(tableName, index.name);
491
+ const sortColumnDefs = rankSortColumnDefs(dialect, index, definition);
492
+ const columnPart = sortColumnDefs.length > 0 ? sql`, ${sql.join(sortColumnDefs, sql`, `)}` : sql``;
493
+ await queryRun(
494
+ exec,
495
+ dialect,
496
+ sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(rankTable)} (${sql.identifier("__id__")} ${sql.raw(key)} PRIMARY KEY, ${sql.identifier("__partition__")} ${sql.raw(key)} NOT NULL${columnPart})`
497
+ );
498
+ const orderedColumns = rankBtreeColumns(dialect, index, definition);
499
+ const btreeName = `${tableName}__rank_${index.name}__btree`;
500
+ await createIndexIfNotExists(exec, dialect, {
501
+ columns: sql.join(orderedColumns, sql`, `),
502
+ name: btreeName,
503
+ table: rankTable,
504
+ unique: false
505
+ });
506
+ }
507
+ }
508
+ };
509
+ const runSqlSearchMigrations = async (exec, schema, dialect) => {
510
+ if (!await isFtsAvailable(exec)) {
511
+ return;
512
+ }
513
+ for (const [tableName, definition] of Object.entries(schema.tables)) {
514
+ const indexes = definition.searchIndexes;
515
+ if (!indexes || indexes.length === 0) {
516
+ continue;
517
+ }
518
+ for (const index of indexes) {
519
+ const ftName = ftsTableName(tableName, index.name);
520
+ await queryRun(
521
+ exec,
522
+ dialect,
523
+ sql`CREATE VIRTUAL TABLE IF NOT EXISTS ${sql.identifier(ftName)} USING fts5(${sql.identifier("__text__")}, ${sql.identifier("__id__")} UNINDEXED)`
524
+ );
525
+ }
526
+ }
527
+ };
528
+ const CDC_LOG_TABLE = "__cdc_log";
529
+ const runSqlCdcMigration = async (exec, dialect) => {
530
+ const { autoincrementPrimaryKey, key, real, text } = dialect.companionTypes;
531
+ await queryRun(
532
+ exec,
533
+ dialect,
534
+ sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(CDC_LOG_TABLE)} (${sql.identifier("seq")} ${sql.raw(autoincrementPrimaryKey)}, ${sql.identifier("ts")} ${sql.raw(real)} NOT NULL, ${sql.identifier("table")} ${sql.raw(key)} NOT NULL, ${sql.identifier("id")} ${sql.raw(key)} NOT NULL, ${sql.identifier("op")} ${sql.raw(key)} NOT NULL, ${sql.identifier("doc")} ${sql.raw(text)})`
535
+ );
536
+ };
537
+ const appendSqlCdcChange = async (exec, ts, table, id, op, doc, dialect) => {
538
+ await queryRun(
539
+ exec,
540
+ dialect,
541
+ sql`INSERT INTO ${sql.identifier(CDC_LOG_TABLE)} (${sql.identifier("ts")}, ${sql.identifier("table")}, ${sql.identifier("id")}, ${sql.identifier("op")}, ${sql.identifier("doc")}) VALUES (${ts}, ${table}, ${id}, ${op}, ${// eslint-disable-next-line unicorn/no-null -- SQL NULL is the correct post-image for a delete; the `id` identifies the removed row.
542
+ doc === void 0 ? null : JSON.stringify(doc)})`
543
+ );
544
+ };
545
+ const readSqlCdcChanges = async (exec, options, dialect) => {
546
+ const sinceSeq = options.sinceSeq ?? 0;
547
+ const limit = Math.max(1, Math.min(options.limit ?? 1e3, 1e4));
548
+ const rows = await queryAll(
549
+ exec,
550
+ dialect,
551
+ sql`SELECT seq, ts, ${sql.identifier("table")}, id, op, doc FROM ${sql.identifier(CDC_LOG_TABLE)} WHERE seq > ${sinceSeq} ORDER BY seq ASC LIMIT ${sql.raw(String(limit))}`
552
+ );
553
+ const changes = rows.map((row) => {
554
+ const { doc } = row;
555
+ const base = { id: String(row.id), op: String(row.op), seq: Number(row.seq), table: String(row.table), ts: Number(row.ts) };
556
+ return typeof doc === "string" ? { ...base, doc: JSON.parse(doc) } : base;
557
+ });
558
+ return { changes, cursor: changes.at(-1)?.seq ?? sinceSeq };
559
+ };
560
+ const trimSqlCdcChanges = async (exec, throughSeq, dialect) => {
561
+ await queryRun(exec, dialect, sql`DELETE FROM ${sql.identifier(CDC_LOG_TABLE)} WHERE ${sql.identifier("seq")} <= ${throughSeq}`);
562
+ };
563
+ const createSqlCtxDb = (options) => {
564
+ const { crossShardCounter, crossShardReader, exec, maxRelationKeys, schema } = options;
565
+ const { dialect } = options;
566
+ const { isUniqueViolation } = dialect;
567
+ const whereSqlStrategy = {
568
+ fieldRef: columnRefSql,
569
+ serialize: serializeColumnValue,
570
+ // MySQL has no `||` string concat; the rest use the portable form compileWhereSql defaults to.
571
+ ...dialect.name === "mysql" ? { likeContains: (reference, term) => sql`${reference} LIKE CONCAT('%', ${term}, '%')` } : {}
572
+ };
573
+ const nullSafeEquals = (reference, value) => nullSafeEqualsSql(dialect.name, reference, value);
574
+ const upsertSql = (config) => {
575
+ const columnList = sql.join(
576
+ config.columns.map((column) => sql`${sql.identifier(column)}`),
577
+ sql`, `
578
+ );
579
+ const valueList = sql.join(
580
+ config.values.map((value) => sql`${value}`),
581
+ sql`, `
582
+ );
583
+ const excluded = (column) => dialect.name === "mysql" ? sql`VALUES(${sql.identifier(column)})` : sql`excluded.${sql.identifier(column)}`;
584
+ const current = (column) => dialect.name === "postgres" ? sql`${sql.identifier(config.table)}.${sql.identifier(column)}` : sql`${sql.identifier(column)}`;
585
+ const assignments = sql.join(
586
+ Object.entries(config.set(excluded, current)).map(([column, expression]) => sql`${sql.identifier(column)} = ${expression}`),
587
+ sql`, `
588
+ );
589
+ const conflict = dialect.name === "mysql" ? sql`ON DUPLICATE KEY UPDATE ${assignments}` : sql`ON CONFLICT(${sql.identifier(config.conflictKey)}) DO UPDATE SET ${assignments}`;
590
+ return sql`INSERT INTO ${sql.identifier(config.table)} (${columnList}) VALUES (${valueList}) ${conflict}`;
591
+ };
592
+ const clock = options.clock ?? (() => Date.now());
593
+ const generateId = options.idGenerator ?? (() => crypto.randomUUID());
594
+ const cdcEnabled = options.cdc ?? false;
595
+ const auth = options.auth ?? { identity: null, userId: null };
596
+ const recordCdc = async (table, id, op, doc) => {
597
+ if (cdcEnabled) {
598
+ await appendSqlCdcChange(exec, clock(), table, id, op, doc, dialect);
599
+ }
600
+ };
601
+ const scheduler = options.scheduler ?? throwingScheduler;
602
+ const tableNameCache = createTableNameCache();
603
+ let triggerDepth = 0;
604
+ let migratedPromise;
605
+ const ensureMigrated = async () => {
606
+ migratedPromise ??= (async () => {
607
+ await runSqlGlobalTableMigrations(exec, schema, dialect);
608
+ await runSqlAggregateMigrations(exec, schema, dialect);
609
+ await runSqlRankMigrations(exec, schema, dialect);
610
+ await runSqlSearchMigrations(exec, schema, dialect);
611
+ if (cdcEnabled) {
612
+ await runSqlCdcMigration(exec, dialect);
613
+ }
614
+ })().catch((error) => {
615
+ migratedPromise = void 0;
616
+ throw error;
617
+ });
618
+ return migratedPromise;
619
+ };
620
+ const resolveTableName = async (id, expectedTable) => {
621
+ await ensureMigrated();
622
+ const tableName = await tableNameFromId(exec, dialect, schema, id, tableNameCache);
623
+ if (expectedTable !== void 0 && tableName !== expectedTable) {
624
+ return void 0;
625
+ }
626
+ return tableName;
627
+ };
628
+ const backfilled = /* @__PURE__ */ new Map();
629
+ const counterTableExists = async (table, indexName) => {
630
+ const aggTable = aggregateTableName(table, indexName);
631
+ const rows = await queryAll(exec, dialect, dialect.tableExists(aggTable));
632
+ return rows.length > 0;
633
+ };
634
+ const ensureBackfilled = async (tableName, index) => {
635
+ const cacheKey = `${tableName}::${index.name}`;
636
+ const cached = backfilled.get(cacheKey);
637
+ if (cached !== void 0) {
638
+ return cached;
639
+ }
640
+ const exists = await counterTableExists(tableName, index.name);
641
+ if (!exists) {
642
+ backfilled.set(cacheKey, false);
643
+ return false;
644
+ }
645
+ const definition = schema.tables[tableName];
646
+ if (!definition) {
647
+ backfilled.set(cacheKey, false);
648
+ return false;
649
+ }
650
+ const by = index.by ?? [];
651
+ const tallies = /* @__PURE__ */ new Map();
652
+ await forEachRowPaged(exec, dialect, definition, tableName, (document) => {
653
+ if (index.where && !matchesStaticWhere(document, index.where)) {
654
+ return;
655
+ }
656
+ const encoded = encodeAggregateKey(by, document);
657
+ foldAggregateTally(tallies, encoded, index, document);
658
+ });
659
+ const aggTable = aggregateTableName(tableName, index.name);
660
+ await queryRun(exec, dialect, sql`DELETE FROM ${sql.identifier(aggTable)}`);
661
+ for (const [encoded, tally] of tallies) {
662
+ await queryRun(
663
+ exec,
664
+ dialect,
665
+ sql`INSERT INTO ${sql.identifier(aggTable)} (${sql.identifier("__key__")}, ${sql.identifier("__value__")}, ${sql.identifier("__count__")}) VALUES (${encoded}, ${tally.value}, ${tally.count})`
666
+ );
667
+ }
668
+ backfilled.set(cacheKey, true);
669
+ return true;
670
+ };
671
+ const recomputeExtreme = async (tableName, index, document) => {
672
+ const sqlFunction = aggregateSqlFunction(index.op);
673
+ const field = index.field ?? "";
674
+ const conditions = [];
675
+ for (const key of index.by ?? []) {
676
+ const value = serializeColumnValue(document[key] ?? null);
677
+ conditions.push(value === null ? sql`${columnRefSql(key)} IS NULL` : sql`${columnRefSql(key)} = ${value}`);
678
+ }
679
+ for (const [key, expected] of Object.entries(index.where ?? {})) {
680
+ const literal = expected !== null && typeof expected === "object" && !Array.isArray(expected) ? expected.eq : expected;
681
+ const value = serializeColumnValue(literal);
682
+ conditions.push(value === null ? sql`${columnRefSql(key)} IS NULL` : sql`${columnRefSql(key)} = ${value}`);
683
+ }
684
+ const query = sql`SELECT ${sql.raw(sqlFunction)}(${columnRefSql(field)}) AS value FROM ${sql.identifier(tableName)}`;
685
+ const rows = await queryAll(exec, dialect, conditions.length > 0 ? sql`${query} WHERE ${sql.join(conditions, sql` AND `)}` : query);
686
+ return aggregateScalar(rows[0]?.["value"]);
687
+ };
688
+ const pruneEmptyGroup = async (aggTable, encoded) => {
689
+ await queryRun(
690
+ exec,
691
+ dialect,
692
+ sql`DELETE FROM ${sql.identifier(aggTable)} WHERE ${sql.identifier("__key__")} = ${encoded} AND ${sql.identifier("__count__")} <= 0`
693
+ );
694
+ };
695
+ const applyAggregateDelta = async (tableName, index, previous, next) => {
696
+ const aggTable = aggregateTableName(tableName, index.name);
697
+ const { op } = index;
698
+ const field = index.field ?? "";
699
+ const removes = previous && (!index.where || matchesStaticWhere(previous, index.where)) ? previous : void 0;
700
+ const adds = next && (!index.where || matchesStaticWhere(next, index.where)) ? next : void 0;
701
+ if (!removes && !adds) {
702
+ return;
703
+ }
704
+ if (op === "count") {
705
+ const touched = /* @__PURE__ */ new Set();
706
+ for (const [document, delta] of [
707
+ [removes, -1],
708
+ [adds, 1]
709
+ ]) {
710
+ if (!document) {
711
+ continue;
712
+ }
713
+ const encoded = encodeAggregateKey(index.by ?? [], document);
714
+ touched.add(encoded);
715
+ await queryRun(
716
+ exec,
717
+ dialect,
718
+ upsertSql({
719
+ columns: ["__key__", "__value__", "__count__"],
720
+ conflictKey: "__key__",
721
+ set: (excluded, current) => {
722
+ return {
723
+ __count__: sql`${current("__count__")} + ${excluded("__count__")}`,
724
+ __value__: sql`${current("__value__")} + ${excluded("__value__")}`
725
+ };
726
+ },
727
+ table: aggTable,
728
+ values: [encoded, delta, delta]
729
+ })
730
+ );
731
+ }
732
+ for (const encoded of touched) {
733
+ await pruneEmptyGroup(aggTable, encoded);
734
+ }
735
+ return;
736
+ }
737
+ if (op === "sum" || op === "avg") {
738
+ const touched = /* @__PURE__ */ new Set();
739
+ for (const [document, sign] of [
740
+ [removes, -1],
741
+ [adds, 1]
742
+ ]) {
743
+ if (!document) {
744
+ continue;
745
+ }
746
+ const numeric = coerceAggregateNumber(document[field]);
747
+ if (numeric === void 0) {
748
+ continue;
749
+ }
750
+ const encoded = encodeAggregateKey(index.by ?? [], document);
751
+ touched.add(encoded);
752
+ await queryRun(
753
+ exec,
754
+ dialect,
755
+ upsertSql({
756
+ columns: ["__key__", "__value__", "__count__"],
757
+ conflictKey: "__key__",
758
+ set: (excluded, current) => {
759
+ return {
760
+ __count__: sql`${current("__count__")} + ${excluded("__count__")}`,
761
+ __value__: sql`COALESCE(${current("__value__")}, 0) + ${excluded("__value__")}`
762
+ };
763
+ },
764
+ table: aggTable,
765
+ values: [encoded, sign * numeric, sign]
766
+ })
767
+ );
768
+ }
769
+ for (const encoded of touched) {
770
+ await pruneEmptyGroup(aggTable, encoded);
771
+ }
772
+ return;
773
+ }
774
+ if (removes) {
775
+ const encoded = encodeAggregateKey(index.by ?? [], removes);
776
+ const removedValue = coerceAggregateNumber(removes[field]);
777
+ const existingRows = await queryAll(
778
+ exec,
779
+ dialect,
780
+ sql`SELECT ${sql.identifier("__value__")} AS value, ${sql.identifier("__count__")} AS count FROM ${sql.identifier(aggTable)} WHERE ${sql.identifier("__key__")} = ${encoded}`
781
+ );
782
+ const existing = existingRows[0];
783
+ const existingValue = aggregateScalar(existing?.value);
784
+ const remainingCount = (existing?.count ?? 0) - 1;
785
+ if (remainingCount <= 0) {
786
+ await queryRun(exec, dialect, sql`DELETE FROM ${sql.identifier(aggTable)} WHERE ${sql.identifier("__key__")} = ${encoded}`);
787
+ } else if (existing && removedValue !== void 0 && existingValue !== null && removedValue === existingValue) {
788
+ const recomputed = await recomputeExtreme(tableName, index, removes);
789
+ await queryRun(
790
+ exec,
791
+ dialect,
792
+ sql`UPDATE ${sql.identifier(aggTable)} SET ${sql.identifier("__value__")} = ${recomputed}, ${sql.identifier("__count__")} = ${remainingCount} WHERE ${sql.identifier("__key__")} = ${encoded}`
793
+ );
794
+ } else {
795
+ await queryRun(
796
+ exec,
797
+ dialect,
798
+ sql`UPDATE ${sql.identifier(aggTable)} SET ${sql.identifier("__count__")} = ${sql.identifier("__count__")} - 1 WHERE ${sql.identifier("__key__")} = ${encoded}`
799
+ );
800
+ }
801
+ }
802
+ if (adds) {
803
+ const encoded = encodeAggregateKey(index.by ?? [], adds);
804
+ const addedValue = coerceAggregateNumber(adds[field]);
805
+ if (addedValue === void 0) {
806
+ await queryRun(
807
+ exec,
808
+ dialect,
809
+ upsertSql({
810
+ columns: ["__key__", "__value__", "__count__"],
811
+ conflictKey: "__key__",
812
+ set: (_excluded, current) => {
813
+ return { __count__: sql`${current("__count__")} + 1` };
814
+ },
815
+ table: aggTable,
816
+ // eslint-disable-next-line unicorn/no-null -- seeds an extreme-less group with NULL value; the literal 1 seeds the count
817
+ values: [encoded, null, 1]
818
+ })
819
+ );
820
+ } else {
821
+ const op2 = op === "min" ? "MIN" : "MAX";
822
+ await queryRun(
823
+ exec,
824
+ dialect,
825
+ upsertSql({
826
+ columns: ["__key__", "__value__", "__count__"],
827
+ conflictKey: "__key__",
828
+ set: (excluded, current) => {
829
+ return {
830
+ __count__: sql`${current("__count__")} + 1`,
831
+ __value__: sql`${sql.raw(op2)}(COALESCE(${current("__value__")}, ${excluded("__value__")}), ${excluded("__value__")})`
832
+ };
833
+ },
834
+ table: aggTable,
835
+ values: [encoded, addedValue, 1]
836
+ })
837
+ );
838
+ }
839
+ }
840
+ };
841
+ const ensureBackfilledForTable = async (tableName) => {
842
+ const indexes = schema.tables[tableName]?.aggregateIndexes;
843
+ if (!indexes || indexes.length === 0) {
844
+ return;
845
+ }
846
+ for (const index of indexes) {
847
+ await ensureBackfilled(tableName, index);
848
+ }
849
+ };
850
+ const syncAggregates = async (tableName, previous, next) => {
851
+ const indexes = schema.tables[tableName]?.aggregateIndexes;
852
+ if (!indexes || indexes.length === 0) {
853
+ return;
854
+ }
855
+ for (const index of indexes) {
856
+ const cacheKey = `${tableName}::${index.name}`;
857
+ const cached = backfilled.get(cacheKey);
858
+ const exists = cached ?? await counterTableExists(tableName, index.name);
859
+ if (!exists) {
860
+ continue;
861
+ }
862
+ await applyAggregateDelta(tableName, index, previous, next);
863
+ }
864
+ };
865
+ const rankBackfilled = /* @__PURE__ */ new Map();
866
+ const rankTableExists = async (table, indexName) => {
867
+ const rankTable = rankTableName(table, indexName);
868
+ const rows = await queryAll(exec, dialect, dialect.tableExists(rankTable));
869
+ return rows.length > 0;
870
+ };
871
+ const ensureRankBackfilled = async (tableName, index) => {
872
+ const cacheKey = `${tableName}::rank::${index.name}`;
873
+ const cached = rankBackfilled.get(cacheKey);
874
+ if (cached !== void 0) {
875
+ return cached;
876
+ }
877
+ const exists = await rankTableExists(tableName, index.name);
878
+ if (!exists) {
879
+ rankBackfilled.set(cacheKey, false);
880
+ return false;
881
+ }
882
+ const definition = schema.tables[tableName];
883
+ if (!definition) {
884
+ rankBackfilled.set(cacheKey, false);
885
+ return false;
886
+ }
887
+ const rankTable = rankTableName(tableName, index.name);
888
+ await queryRun(exec, dialect, sql`DELETE FROM ${sql.identifier(rankTable)}`);
889
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
890
+ const insertColumnList = sql.join(
891
+ ["__id__", "__partition__", ...sortColumns].map((column) => sql`${sql.identifier(column)}`),
892
+ sql`, `
893
+ );
894
+ const rankTuples = [];
895
+ await forEachRowPaged(exec, dialect, definition, tableName, (document) => {
896
+ if (index.where && !matchesRankStaticWhere(document, index.where)) {
897
+ return;
898
+ }
899
+ const partitionKey = encodePartitionKey(index.partitionBy ?? [], document);
900
+ const sortValues = index.sortBy.map((key) => serializeColumnValue(document[key.field] ?? null));
901
+ rankTuples.push([document["_id"], partitionKey, ...sortValues]);
902
+ });
903
+ for (const tuple of rankTuples) {
904
+ const valueList = sql.join(
905
+ tuple.map((value) => sql`${value}`),
906
+ sql`, `
907
+ );
908
+ await queryRun(exec, dialect, sql`INSERT INTO ${sql.identifier(rankTable)} (${insertColumnList}) VALUES (${valueList})`);
909
+ }
910
+ rankBackfilled.set(cacheKey, true);
911
+ return true;
912
+ };
913
+ const ensureRankBackfilledForTable = async (tableName) => {
914
+ const indexes = schema.tables[tableName]?.rankIndexes;
915
+ if (!indexes || indexes.length === 0) {
916
+ return;
917
+ }
918
+ for (const index of indexes) {
919
+ await ensureRankBackfilled(tableName, index);
920
+ }
921
+ };
922
+ const syncRanks = async (tableName, id, previous, next) => {
923
+ const indexes = schema.tables[tableName]?.rankIndexes;
924
+ if (!indexes || indexes.length === 0) {
925
+ return;
926
+ }
927
+ for (const index of indexes) {
928
+ const cacheKey = `${tableName}::rank::${index.name}`;
929
+ const cached = rankBackfilled.get(cacheKey);
930
+ const exists = cached ?? await rankTableExists(tableName, index.name);
931
+ if (!exists) {
932
+ continue;
933
+ }
934
+ const rankTable = rankTableName(tableName, index.name);
935
+ if (previous) {
936
+ await queryRun(exec, dialect, sql`DELETE FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__id__")} = ${id}`);
937
+ }
938
+ if (next) {
939
+ if (index.where && !matchesRankStaticWhere(next, index.where)) {
940
+ continue;
941
+ }
942
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
943
+ const columnList = sql.join(
944
+ ["__id__", "__partition__", ...sortColumns].map((column) => sql`${sql.identifier(column)}`),
945
+ sql`, `
946
+ );
947
+ const partitionKey = encodePartitionKey(index.partitionBy ?? [], next);
948
+ const sortValues = index.sortBy.map((key) => serializeColumnValue(next[key.field] ?? null));
949
+ const valueList = sql.join(
950
+ [id, partitionKey, ...sortValues].map((value) => sql`${value}`),
951
+ sql`, `
952
+ );
953
+ await queryRun(exec, dialect, sql`INSERT INTO ${sql.identifier(rankTable)} (${columnList}) VALUES (${valueList})`);
954
+ }
955
+ }
956
+ };
957
+ const syncSearch = async (tableName, id, document) => {
958
+ const indexes = schema.tables[tableName]?.searchIndexes;
959
+ if (!indexes || indexes.length === 0 || !await isFtsAvailable(exec)) {
960
+ return;
961
+ }
962
+ for (const index of indexes) {
963
+ const ftName = ftsTableName(tableName, index.name);
964
+ await queryRun(exec, dialect, sql`DELETE FROM ${sql.identifier(ftName)} WHERE ${sql.identifier("__id__")} = ${id}`);
965
+ if (document) {
966
+ await queryRun(
967
+ exec,
968
+ dialect,
969
+ sql`INSERT INTO ${sql.identifier(ftName)} (${sql.identifier("__text__")}, ${sql.identifier("__id__")}) VALUES (${stringifySearchText(document[index.field])}, ${id})`
970
+ );
971
+ }
972
+ }
973
+ };
974
+ const triggerMatchers = /* @__PURE__ */ new Set();
975
+ for (const [tableName, definition] of Object.entries(schema.tables)) {
976
+ for (const trigger of Object.values(definition.triggerMap ?? {})) {
977
+ triggerMatchers.add(`${tableName} ${trigger.timing} ${trigger.op}`);
978
+ }
979
+ }
980
+ const hasMatchingTrigger = (tableName, timing, op) => triggerMatchers.has(`${tableName} ${timing} ${op}`);
981
+ let triggerContext;
982
+ const fireTriggers = async (timing, op, event) => {
983
+ triggerDepth += 1;
984
+ if (triggerDepth > MAX_TRIGGER_DEPTH) {
985
+ triggerDepth -= 1;
986
+ throw new ConflictError(
987
+ `trigger recursion exceeded ${String(MAX_TRIGGER_DEPTH)} levels on "${event.table}" — check for a self-triggering write`,
988
+ "trigger"
989
+ );
990
+ }
991
+ try {
992
+ await runTriggers({ ctx: triggerContext, event, op, schema, tableName: event.table, timing });
993
+ } finally {
994
+ triggerDepth -= 1;
995
+ }
996
+ };
997
+ const runWrite = async (table, query) => {
998
+ try {
999
+ await queryRun(exec, dialect, query);
1000
+ } catch (error) {
1001
+ if (isUniqueViolation(error)) {
1002
+ throw new ConflictError(`unique constraint violation on "${table}"`, "unique");
1003
+ }
1004
+ throw error;
1005
+ }
1006
+ };
1007
+ const rawRow = async (tableName, id) => {
1008
+ const rows = await queryAll(exec, dialect, sql`SELECT * FROM ${sql.identifier(tableName)} WHERE ${sql.identifier("id")} = ${id}`);
1009
+ return rows[0];
1010
+ };
1011
+ const runGuardedWrite = async (table, verb, setClause, snapshot) => {
1012
+ if (snapshot === void 0) {
1013
+ return;
1014
+ }
1015
+ const guardClause = sql.join(
1016
+ Object.keys(snapshot).map((column) => nullSafeEquals(sql`${sql.identifier(column)}`, snapshot[column])),
1017
+ sql` AND `
1018
+ );
1019
+ const base = verb === "UPDATE" ? sql`UPDATE ${sql.identifier(table)} SET ${setClause} WHERE ${guardClause}` : sql`DELETE FROM ${sql.identifier(table)} WHERE ${guardClause}`;
1020
+ const occConflict = () => {
1021
+ throw new ConflictError(`optimistic concurrency conflict on "${table}" — the row changed during this mutation; refetch and retry`, "occ");
1022
+ };
1023
+ try {
1024
+ if (dialect.supportsReturning) {
1025
+ const returned = await queryAll(exec, dialect, sql`${base} RETURNING ${sql.identifier("id")}`);
1026
+ if (returned.length === 0) {
1027
+ occConflict();
1028
+ }
1029
+ } else {
1030
+ const result = await queryRun(exec, dialect, base);
1031
+ const affected = dialect.affectedRows ? dialect.affectedRows(result ?? { rowsAffected: 0 }) : 0;
1032
+ if (affected === 0) {
1033
+ occConflict();
1034
+ }
1035
+ }
1036
+ } catch (error) {
1037
+ if (isUniqueViolation(error)) {
1038
+ throw new ConflictError(`unique constraint violation on "${table}"`, "unique");
1039
+ }
1040
+ throw error;
1041
+ }
1042
+ };
1043
+ const columnTuple = (definition, id, creationTime, document) => {
1044
+ const fields = Object.keys(definition.shape);
1045
+ return {
1046
+ // Raw (unquoted) column names — the INSERT quotes them via `sql.identifier`.
1047
+ columns: ["id", "_creationTime", ...fields],
1048
+ // eslint-disable-next-line unicorn/no-null -- SQL bind value: an absent column must bind `null`, not undefined.
1049
+ values: [id, creationTime, ...fields.map((field) => serializeColumnValue(document[field] ?? null))]
1050
+ };
1051
+ };
1052
+ const tryIndexedGroupBy = async (tableName, aggregateIndexes, agg, groupOptions) => {
1053
+ const planned = selectIndexForGroupBy(aggregateIndexes, agg.op, agg.field, groupOptions.by, groupOptions.where);
1054
+ if (!planned) {
1055
+ return void 0;
1056
+ }
1057
+ const counterReady = await ensureBackfilled(tableName, planned.index);
1058
+ if (!counterReady) {
1059
+ return void 0;
1060
+ }
1061
+ const aggTable = aggregateTableName(tableName, planned.index.name);
1062
+ const partialKeys = Object.keys(planned.partial);
1063
+ if (partialKeys.length === (planned.index.by ?? []).length && partialKeys.length > 0) {
1064
+ const encoded = encodeAggregateKey(planned.index.by ?? [], planned.partial);
1065
+ const rowsIndexed2 = await queryAll(
1066
+ exec,
1067
+ dialect,
1068
+ sql`SELECT ${sql.identifier("__value__")} AS value, ${sql.identifier("__count__")} AS count FROM ${sql.identifier(aggTable)} WHERE ${sql.identifier("__key__")} = ${encoded}`
1069
+ );
1070
+ if (rowsIndexed2.length === 0) {
1071
+ return [];
1072
+ }
1073
+ const row = rowsIndexed2[0];
1074
+ return [{ key: { ...planned.partial }, value: readAggregateValue(agg.op, { count: row.count, value: aggregateScalar(row.value) }) }];
1075
+ }
1076
+ const rowsIndexed = await queryAll(
1077
+ exec,
1078
+ dialect,
1079
+ sql`SELECT ${sql.identifier("__key__")} AS key, ${sql.identifier("__value__")} AS value, ${sql.identifier("__count__")} AS count FROM ${sql.identifier(aggTable)}`
1080
+ );
1081
+ return rowsIndexed.map((row) => {
1082
+ const typed = row;
1083
+ return {
1084
+ key: JSON.parse(typed.key),
1085
+ value: readAggregateValue(agg.op, { count: typed.count, value: aggregateScalar(typed.value) })
1086
+ };
1087
+ });
1088
+ };
1089
+ const isShardLocalTarget = (childTable) => {
1090
+ const kind = schema.tables[childTable]?.shardMode?.kind;
1091
+ return kind === "shardBy" || kind === "root";
1092
+ };
1093
+ const crossBackendUnsupported = (childTable) => {
1094
+ throw new Error(
1095
+ `cross-backend relation: a global table cannot load the shard-local relation '${childTable}' (it spans every shard) — wire a cross-shard reader to support it`
1096
+ );
1097
+ };
1098
+ const relationPredicateFetcher = (childTable, childArgs) => {
1099
+ if (!isShardLocalTarget(childTable)) {
1100
+ return writer.findMany(childTable, childArgs);
1101
+ }
1102
+ return crossShardReader ? crossShardReader(childTable, childArgs) : crossBackendUnsupported(childTable);
1103
+ };
1104
+ const resolveAggregateRelations = (where, predicateTable, relationBaseWhere) => resolveRelationPredicates(where, {
1105
+ fetcher: relationPredicateFetcher,
1106
+ maxRelationKeys,
1107
+ relationBaseWhere,
1108
+ schema,
1109
+ tableName: predicateTable
1110
+ });
1111
+ const writer = {
1112
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- routes count/sum/avg/min/max through the indexed companion vs scan fallback; the branching reads clearer inline than split across per-op helpers
1113
+ async aggregate(tableName, aggOptions) {
1114
+ const definition = schema.tables[tableName];
1115
+ if (!definition) {
1116
+ throw new Error(`unknown table: ${tableName}`);
1117
+ }
1118
+ await ensureMigrated();
1119
+ aggregateSqlFunction(aggOptions.op);
1120
+ if (aggOptions.op === "count") {
1121
+ return writer.count(tableName, {
1122
+ baseWhere: aggOptions.baseWhere,
1123
+ relationBaseWhere: aggOptions.relationBaseWhere,
1124
+ restrictsCounts: aggOptions.restrictsCounts,
1125
+ where: aggOptions.where
1126
+ });
1127
+ }
1128
+ if (!aggOptions.field) {
1129
+ throw new Error(`aggregate(${tableName}, { op: "${aggOptions.op}" }): "field" is required for non-count reducers`);
1130
+ }
1131
+ const aggScope = softDeleteScope(definition.softDeleteMode, void 0);
1132
+ const effective = mergeWhere(mergeWhere(aggOptions.baseWhere, aggOptions.where), aggScope);
1133
+ const resolved = await resolveAggregateRelations(effective, tableName, aggOptions.relationBaseWhere);
1134
+ const hasRelation = resolved !== effective;
1135
+ if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation && !aggScope) {
1136
+ const planned = selectIndexForAggregate(definition.aggregateIndexes, aggOptions.op, aggOptions.field, aggOptions.where);
1137
+ if (planned) {
1138
+ const counterReady = await ensureBackfilled(tableName, planned.index);
1139
+ if (counterReady) {
1140
+ const encoded = encodeAggregateKey(planned.index.by ?? [], planned.key);
1141
+ const aggTable = aggregateTableName(tableName, planned.index.name);
1142
+ const rows2 = await queryAll(
1143
+ exec,
1144
+ dialect,
1145
+ sql`SELECT ${sql.identifier("__value__")} AS value, ${sql.identifier("__count__")} AS count FROM ${sql.identifier(aggTable)} WHERE ${sql.identifier("__key__")} = ${encoded}`
1146
+ );
1147
+ const row = rows2[0];
1148
+ return readAggregateValue(aggOptions.op, row === void 0 ? void 0 : { count: row.count, value: aggregateScalar(row.value) });
1149
+ }
1150
+ }
1151
+ }
1152
+ const whereCondition = compileWhereSql(resolved, whereSqlStrategy);
1153
+ const aggregateFunction = sql.raw(aggregateSqlFunction(aggOptions.op));
1154
+ const query = sql`SELECT ${aggregateFunction}(${columnRefSql(aggOptions.field)}) AS value FROM ${sql.identifier(tableName)}`;
1155
+ const rows = await queryAll(exec, dialect, whereCondition ? sql`${query} WHERE ${whereCondition}` : query);
1156
+ const value = rows[0]?.["value"];
1157
+ return value === null || value === void 0 ? null : Number(value);
1158
+ },
1159
+ async count(tableName, whereOrOptions) {
1160
+ const definition = schema.tables[tableName];
1161
+ if (!definition) {
1162
+ throw new Error(`unknown table: ${tableName}`);
1163
+ }
1164
+ await ensureMigrated();
1165
+ const countOptions = normalizeCountArgument(whereOrOptions);
1166
+ if (countOptions.restrictsCounts) {
1167
+ throw new CountRlsUnsupportedError(tableName);
1168
+ }
1169
+ const countScope = softDeleteScope(definition.softDeleteMode, void 0);
1170
+ const effective = mergeWhere(mergeWhere(countOptions.baseWhere, countOptions.where), countScope);
1171
+ const resolved = await resolveAggregateRelations(effective, tableName, countOptions.relationBaseWhere);
1172
+ const hasRelation = resolved !== effective;
1173
+ if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation && !countScope) {
1174
+ const planned = selectIndexForCount(definition.aggregateIndexes, countOptions.where);
1175
+ if (planned) {
1176
+ const counterReady = await ensureBackfilled(tableName, planned.index);
1177
+ if (counterReady) {
1178
+ const encoded = encodeAggregateKey(planned.index.by ?? [], planned.key);
1179
+ const aggTable = aggregateTableName(tableName, planned.index.name);
1180
+ const rows2 = await queryAll(
1181
+ exec,
1182
+ dialect,
1183
+ sql`SELECT ${sql.identifier("__value__")} AS value FROM ${sql.identifier(aggTable)} WHERE ${sql.identifier("__key__")} = ${encoded}`
1184
+ );
1185
+ return Number(rows2[0]?.["value"] ?? 0);
1186
+ }
1187
+ }
1188
+ }
1189
+ const whereCondition = compileWhereSql(resolved, whereSqlStrategy);
1190
+ const query = sql`SELECT COUNT(*) AS count FROM ${sql.identifier(tableName)}`;
1191
+ const rows = await queryAll(exec, dialect, whereCondition ? sql`${query} WHERE ${whereCondition}` : query);
1192
+ return Number(rows[0]?.["count"] ?? 0);
1193
+ },
1194
+ async delete(id, expectedTable, deleteOptions) {
1195
+ const tableName = await resolveTableName(id, expectedTable);
1196
+ if (!tableName) {
1197
+ return;
1198
+ }
1199
+ const definition = schema.tables[tableName];
1200
+ if (!definition) {
1201
+ return;
1202
+ }
1203
+ const snapshot = await rawRow(tableName, id);
1204
+ const existing = decodeRow(definition, snapshot);
1205
+ const hard = deleteOptions?.hard === true;
1206
+ const softField = !hard && definition.softDeleteMode ? definition.softDeleteMode.field : void 0;
1207
+ if (softField && (!existing || existing[softField] !== null && existing[softField] !== void 0)) {
1208
+ return;
1209
+ }
1210
+ if (hasMatchingTrigger(tableName, "before", "delete")) {
1211
+ await fireTriggers("before", "delete", { id, op: "delete", previous: existing ?? void 0, table: tableName });
1212
+ }
1213
+ await applyOnDelete({
1214
+ deletedId: id,
1215
+ deletedReference: (references) => existing?.[references],
1216
+ findHolders: async (holderTable, field, value) => {
1217
+ if (schema.tables[holderTable]?.shardMode?.kind === "shardBy") {
1218
+ throw new Error(
1219
+ `cross-backend cascade from global '${tableName}' into shardBy '${holderTable}' is not supported — would require Query Coordinator fan-out across shards`
1220
+ );
1221
+ }
1222
+ const holders = await writer.findMany(holderTable, { includeDeleted: hard, where: { [field]: value } });
1223
+ return holders.page;
1224
+ },
1225
+ onCascade: (_holderTable, holderId) => writer.delete(holderId, void 0, deleteOptions),
1226
+ onRestrict: (message) => {
1227
+ throw new ConflictError(message, "restrict");
1228
+ },
1229
+ // eslint-disable-next-line unicorn/no-null -- onSetNull writes a SQL NULL into the holder column; that is the literal value being persisted.
1230
+ onSetNull: (_holderTable, holderId, field) => writer.patch(holderId, { [field]: null }),
1231
+ schema,
1232
+ tableName
1233
+ });
1234
+ await ensureBackfilledForTable(tableName);
1235
+ await ensureRankBackfilledForTable(tableName);
1236
+ if (softField && existing) {
1237
+ const merged = { ...existing, [softField]: clock() };
1238
+ const assignments = sql.join(
1239
+ // eslint-disable-next-line unicorn/no-null -- SQL bind value: an absent column binds `null`, matching the patch path.
1240
+ Object.keys(definition.shape).map((field) => sql`${sql.identifier(field)} = ${serializeColumnValue(merged[field] ?? null)}`),
1241
+ sql`, `
1242
+ );
1243
+ await runGuardedWrite(tableName, "UPDATE", assignments, snapshot);
1244
+ await syncAggregates(tableName, existing, merged);
1245
+ await syncRanks(tableName, id, existing, void 0);
1246
+ await syncSearch(tableName, id, merged);
1247
+ await recordCdc(tableName, id, "update", merged);
1248
+ if (hasMatchingTrigger(tableName, "after", "delete")) {
1249
+ await fireTriggers("after", "delete", { id, op: "delete", previous: existing, table: tableName });
1250
+ }
1251
+ return;
1252
+ }
1253
+ await runGuardedWrite(tableName, "DELETE", void 0, snapshot);
1254
+ tableNameCache.delete(id);
1255
+ await syncAggregates(tableName, existing ?? void 0, void 0);
1256
+ await syncRanks(tableName, id, existing ?? void 0, void 0);
1257
+ await syncSearch(tableName, id, void 0);
1258
+ await recordCdc(tableName, id, "delete");
1259
+ if (hasMatchingTrigger(tableName, "after", "delete")) {
1260
+ await fireTriggers("after", "delete", { id, op: "delete", previous: existing ?? void 0, table: tableName });
1261
+ }
1262
+ },
1263
+ async findFirst(tableName, args = {}) {
1264
+ const result = await writer.findMany(tableName, { ...args, limit: 1 });
1265
+ return result.page[0] ?? null;
1266
+ },
1267
+ async findFirstOrThrow(tableName, args = {}) {
1268
+ const document = await writer.findFirst(tableName, args);
1269
+ if (document === null) {
1270
+ throw new NotFoundError(`findFirstOrThrow: no "${tableName}" document matched`);
1271
+ }
1272
+ return document;
1273
+ },
1274
+ async findMany(tableName, args = {}) {
1275
+ const definition = schema.tables[tableName];
1276
+ if (!definition) {
1277
+ throw new Error(`unknown table: ${tableName}`);
1278
+ }
1279
+ await ensureMigrated();
1280
+ const orderKeys = normalizeOrderKeys(args.orderBy);
1281
+ const seek = args.cursor ? buildSeekWhere(orderKeys, decodeCursor(args.cursor)) : void 0;
1282
+ const relationFetcher = relationPredicateFetcher;
1283
+ const relationGroupedCounter = async (childTable, whereField, values, policyWhere) => {
1284
+ if (isShardLocalTarget(childTable)) {
1285
+ if (!crossShardCounter) {
1286
+ return crossBackendUnsupported(childTable);
1287
+ }
1288
+ return fanOutScalarCounts(crossShardCounter, childTable, whereField, values, policyWhere);
1289
+ }
1290
+ const childDefinition = schema.tables[childTable];
1291
+ if (!childDefinition) {
1292
+ throw new Error(`unknown table: ${childTable}`);
1293
+ }
1294
+ const softScope = softDeleteScope(childDefinition.softDeleteMode, void 0);
1295
+ const inFilter = { [whereField]: { in: values } };
1296
+ const combined = mergeWhere(mergeWhere(inFilter, policyWhere), softScope);
1297
+ const resolvedCombined = await resolveAggregateRelations(combined, childTable, void 0);
1298
+ const whereCondition2 = compileWhereSql(resolvedCombined, whereSqlStrategy);
1299
+ const fieldRef = columnRefSql(whereField);
1300
+ let groupQuery = sql`SELECT ${fieldRef} AS __fk__, COUNT(*) AS count FROM ${sql.identifier(childTable)}`;
1301
+ if (whereCondition2) {
1302
+ groupQuery = sql`${groupQuery} WHERE ${whereCondition2}`;
1303
+ }
1304
+ groupQuery = sql`${groupQuery} GROUP BY ${fieldRef}`;
1305
+ const groupRows = await queryAll(exec, dialect, groupQuery);
1306
+ const result = /* @__PURE__ */ new Map();
1307
+ for (const row of groupRows) {
1308
+ result.set(row["__fk__"], Number(row["count"] ?? 0));
1309
+ }
1310
+ return result;
1311
+ };
1312
+ let predicate = mergeWhere(args.baseWhere, args.where);
1313
+ predicate = mergeWhere(predicate, softDeleteScope(definition.softDeleteMode, args.includeDeleted));
1314
+ predicate = await resolveRelationPredicates(predicate, {
1315
+ fetcher: relationFetcher,
1316
+ maxRelationKeys,
1317
+ relationBaseWhere: args.relationBaseWhere,
1318
+ schema,
1319
+ tableName
1320
+ });
1321
+ if (seek) {
1322
+ predicate = predicate ? { AND: [predicate, seek] } : seek;
1323
+ }
1324
+ const whereCondition = compileWhereSql(predicate, whereSqlStrategy);
1325
+ const orderBy = compileOrderBySql(orderKeys);
1326
+ let query = sql`SELECT * FROM ${sql.identifier(tableName)}`;
1327
+ if (whereCondition) {
1328
+ query = sql`${query} WHERE ${whereCondition}`;
1329
+ }
1330
+ query = sql`${query} ORDER BY ${orderBy}`;
1331
+ const limit = typeof args.limit === "number" ? Math.max(0, Math.floor(args.limit)) : void 0;
1332
+ if (limit !== void 0) {
1333
+ query = sql`${query} LIMIT ${sql.raw(String(limit + 1))}`;
1334
+ }
1335
+ const rows = await queryAll(exec, dialect, query);
1336
+ const documents = decodeRows(definition, rows);
1337
+ if (limit === void 0) {
1338
+ if (args.with) {
1339
+ await resolveWith({
1340
+ fetcher: relationFetcher,
1341
+ groupedCounter: relationGroupedCounter,
1342
+ parents: documents,
1343
+ schema,
1344
+ tableName,
1345
+ with: args.with
1346
+ });
1347
+ }
1348
+ return { continueCursor: null, isDone: true, page: applySelect(documents, args.select, args.with) };
1349
+ }
1350
+ const hasMore = documents.length > limit;
1351
+ const page = hasMore ? documents.slice(0, limit) : documents;
1352
+ const last = page.at(-1);
1353
+ if (args.with) {
1354
+ await resolveWith({ fetcher: relationFetcher, groupedCounter: relationGroupedCounter, parents: page, schema, tableName, with: args.with });
1355
+ }
1356
+ return {
1357
+ // The cursor is encoded from `last` (the full, unprojected row), so
1358
+ // `applySelect` only trims the returned payload — paging is intact.
1359
+ // eslint-disable-next-line unicorn/no-null -- public return shape: `continueCursor` is `string | null`; `null` marks the final page.
1360
+ continueCursor: hasMore && last ? encodeCursor(last, orderKeys) : null,
1361
+ isDone: !hasMore,
1362
+ page: applySelect(page, args.select, args.with)
1363
+ };
1364
+ },
1365
+ async get(id, expectedTable) {
1366
+ const tableName = await resolveTableName(id, expectedTable);
1367
+ if (!tableName) {
1368
+ return null;
1369
+ }
1370
+ const definition = schema.tables[tableName];
1371
+ if (!definition) {
1372
+ return null;
1373
+ }
1374
+ const rows = await queryAll(exec, dialect, sql`SELECT * FROM ${sql.identifier(tableName)} WHERE ${sql.identifier("id")} = ${id}`);
1375
+ return decodeRow(definition, rows[0]);
1376
+ },
1377
+ async groupBy(tableName, groupOptions) {
1378
+ const definition = schema.tables[tableName];
1379
+ if (!definition) {
1380
+ throw new Error(`unknown table: ${tableName}`);
1381
+ }
1382
+ await ensureMigrated();
1383
+ const agg = groupOptions.agg ?? { op: "count" };
1384
+ aggregateSqlFunction(agg.op);
1385
+ if (agg.op !== "count" && !agg.field) {
1386
+ throw new Error(`groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
1387
+ }
1388
+ const groupScope = softDeleteScope(definition.softDeleteMode, void 0);
1389
+ const effective = mergeWhere(mergeWhere(groupOptions.baseWhere, groupOptions.where), groupScope);
1390
+ const resolved = await resolveAggregateRelations(effective, tableName, groupOptions.relationBaseWhere);
1391
+ const hasRelation = resolved !== effective;
1392
+ if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation && !groupScope) {
1393
+ const indexed = await tryIndexedGroupBy(tableName, definition.aggregateIndexes, agg, groupOptions);
1394
+ if (indexed !== void 0) {
1395
+ return indexed;
1396
+ }
1397
+ }
1398
+ const whereCondition = compileWhereSql(resolved, whereSqlStrategy);
1399
+ const select = groupOptions.by.map((field) => sql`${columnRefSql(field)} AS ${sql.identifier(field)}`);
1400
+ if (agg.op === "count") {
1401
+ select.push(sql`COUNT(*) AS value`);
1402
+ } else {
1403
+ if (!agg.field) {
1404
+ throw new Error(`groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
1405
+ }
1406
+ select.push(sql`${sql.raw(aggregateSqlFunction(agg.op))}(${columnRefSql(agg.field)}) AS value`);
1407
+ }
1408
+ const groupBy = sql.join(
1409
+ groupOptions.by.map((field) => columnRefSql(field)),
1410
+ sql`, `
1411
+ );
1412
+ let query = sql`SELECT ${sql.join(select, sql`, `)} FROM ${sql.identifier(tableName)}`;
1413
+ if (whereCondition) {
1414
+ query = sql`${query} WHERE ${whereCondition}`;
1415
+ }
1416
+ query = sql`${query} GROUP BY ${groupBy}`;
1417
+ const rows = await queryAll(exec, dialect, query);
1418
+ return mapGroupByRows(groupOptions.by, rows);
1419
+ },
1420
+ /**
1421
+ * Insert a document. A client-chosen `_id` is **ignored** by default —
1422
+ * a caller able to pick its own id can collide with peer rows, defeat
1423
+ * unique constraints, and forge references in foreign tables.
1424
+ *
1425
+ * Two opt-ins override that: a validated `options.clientId` (public —
1426
+ * a UUID an optimistic client supplies so a sync engine can reconcile by
1427
+ * key) or `options.allowExplicitId` (the trusted dev/admin import path,
1428
+ * honoring a verbatim `_id` on `document`). Otherwise a fresh id is
1429
+ * minted even if a handler forwards a raw client payload.
1430
+ */
1431
+ async insert(tableName, document, insertOptions) {
1432
+ const definition = schema.tables[tableName];
1433
+ if (!definition) {
1434
+ throw new Error(`unknown table: ${tableName}`);
1435
+ }
1436
+ await ensureMigrated();
1437
+ const withDefaults = applyInsertDefaults(definition, document, auth);
1438
+ runRowValidators(definition, withDefaults);
1439
+ let id;
1440
+ let usedExplicitId = true;
1441
+ if (insertOptions?.clientId !== void 0) {
1442
+ assertValidClientId(insertOptions.clientId);
1443
+ id = insertOptions.clientId;
1444
+ } else if (insertOptions?.allowExplicitId && typeof withDefaults["_id"] === "string") {
1445
+ id = withDefaults["_id"];
1446
+ } else {
1447
+ id = generateId();
1448
+ usedExplicitId = false;
1449
+ }
1450
+ const creationTime = typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
1451
+ const documentWithMeta = { ...withDefaults, _creationTime: creationTime, _id: id };
1452
+ if (hasMatchingTrigger(tableName, "before", "insert")) {
1453
+ await fireTriggers("before", "insert", { doc: { ...documentWithMeta }, id, op: "insert", table: tableName });
1454
+ }
1455
+ await ensureBackfilledForTable(tableName);
1456
+ await ensureRankBackfilledForTable(tableName);
1457
+ const { columns, values } = columnTuple(definition, id, creationTime, withDefaults);
1458
+ const columnList = sql.join(
1459
+ columns.map((column) => sql`${sql.identifier(column)}`),
1460
+ sql`, `
1461
+ );
1462
+ const valueList = sql.join(
1463
+ values.map((value) => sql`${value}`),
1464
+ sql`, `
1465
+ );
1466
+ await runWrite(tableName, sql`INSERT INTO ${sql.identifier(tableName)} (${columnList}) VALUES (${valueList})`);
1467
+ if (usedExplicitId) {
1468
+ tableNameCache.set(id, tableName);
1469
+ }
1470
+ await syncAggregates(tableName, void 0, documentWithMeta);
1471
+ await syncRanks(tableName, id, void 0, documentWithMeta);
1472
+ await syncSearch(tableName, id, documentWithMeta);
1473
+ await recordCdc(tableName, id, "insert", documentWithMeta);
1474
+ if (hasMatchingTrigger(tableName, "after", "insert")) {
1475
+ await fireTriggers("after", "insert", { doc: documentWithMeta, id, op: "insert", table: tableName });
1476
+ }
1477
+ return id;
1478
+ },
1479
+ normalizeId(tableName, id) {
1480
+ return normalizeIdStructurally(schema, tableName, id);
1481
+ },
1482
+ async patch(id, patch, expectedTable) {
1483
+ const tableName = await resolveTableName(id, expectedTable);
1484
+ if (!tableName) {
1485
+ throw new Error(`document not found: ${id}`);
1486
+ }
1487
+ const definition = schema.tables[tableName];
1488
+ if (!definition) {
1489
+ throw new Error(`document not found: ${id}`);
1490
+ }
1491
+ const snapshot = await rawRow(tableName, id);
1492
+ const existing = decodeRow(definition, snapshot);
1493
+ if (!existing) {
1494
+ throw new Error(`document not found: ${id}`);
1495
+ }
1496
+ const merged = { ...existing, ...patch, _id: id };
1497
+ applyOnUpdate(definition, patch, merged, auth);
1498
+ runRowValidators(definition, merged);
1499
+ if (hasMatchingTrigger(tableName, "before", "update")) {
1500
+ await fireTriggers("before", "update", { doc: { ...merged }, id, op: "update", previous: existing, table: tableName });
1501
+ }
1502
+ await ensureBackfilledForTable(tableName);
1503
+ await ensureRankBackfilledForTable(tableName);
1504
+ const fields = Object.keys(definition.shape);
1505
+ const assignments = sql.join(
1506
+ // eslint-disable-next-line unicorn/no-null -- SQL bind value: an absent column must bind `null`, not undefined.
1507
+ fields.map((field) => sql`${sql.identifier(field)} = ${serializeColumnValue(merged[field] ?? null)}`),
1508
+ sql`, `
1509
+ );
1510
+ await runGuardedWrite(tableName, "UPDATE", assignments, snapshot);
1511
+ await syncAggregates(tableName, existing, merged);
1512
+ await syncRanks(tableName, id, existing, merged);
1513
+ await syncSearch(tableName, id, merged);
1514
+ await recordCdc(tableName, id, "update", merged);
1515
+ if (hasMatchingTrigger(tableName, "after", "update")) {
1516
+ await fireTriggers("after", "update", { doc: merged, id, op: "update", previous: existing, table: tableName });
1517
+ }
1518
+ },
1519
+ async restore(id, expectedTable) {
1520
+ const tableName = await resolveTableName(id, expectedTable);
1521
+ if (!tableName) {
1522
+ throw new Error(`document not found: ${id}`);
1523
+ }
1524
+ const definition = schema.tables[tableName];
1525
+ const field = definition?.softDeleteMode?.field;
1526
+ if (!definition || !field) {
1527
+ throw new Error(`ctx.db.restore: table "${tableName}" is not a .softDelete() table`);
1528
+ }
1529
+ const snapshot = await rawRow(tableName, id);
1530
+ const wasDeleted = snapshot?.[field] !== null && snapshot?.[field] !== void 0;
1531
+ await writer.patch(id, { [field]: null }, expectedTable);
1532
+ if (wasDeleted) {
1533
+ const row = decodeRow(definition, snapshot);
1534
+ if (row !== null) {
1535
+ await syncRanks(tableName, id, void 0, row);
1536
+ }
1537
+ }
1538
+ },
1539
+ query(tableName) {
1540
+ const definition = schema.tables[tableName];
1541
+ if (!definition) {
1542
+ throw new Error(`unknown table: ${tableName}`);
1543
+ }
1544
+ const LEGACY_READER_ERROR = "the legacy query()/withIndex() reader is not available on the D1 (global) backend; use findMany";
1545
+ const runSearch = async (stage, limit) => {
1546
+ await ensureMigrated();
1547
+ return await isFtsAvailable(exec) ? searchViaFts(exec, dialect, definition, tableName, stage, limit) : searchViaScan(exec, dialect, definition, tableName, stage, limit);
1548
+ };
1549
+ const buildReader = (stage) => {
1550
+ const reader = {
1551
+ async collect() {
1552
+ if (!stage) {
1553
+ throw new Error(LEGACY_READER_ERROR);
1554
+ }
1555
+ return runSearch(stage, void 0);
1556
+ },
1557
+ filter() {
1558
+ throw new Error(LEGACY_READER_ERROR);
1559
+ },
1560
+ async first() {
1561
+ if (!stage) {
1562
+ throw new Error(LEGACY_READER_ERROR);
1563
+ }
1564
+ const rows = await runSearch(stage, 1);
1565
+ return rows[0] ?? null;
1566
+ },
1567
+ order() {
1568
+ return reader;
1569
+ },
1570
+ // eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike.paginate returns a Promise; search queries don't support pagination on either backend
1571
+ async paginate() {
1572
+ if (stage) {
1573
+ throw new Error("pagination is not supported on search queries; use .take(n) or .collect()");
1574
+ }
1575
+ throw new Error(LEGACY_READER_ERROR);
1576
+ },
1577
+ async take(limit) {
1578
+ if (!stage) {
1579
+ throw new Error(LEGACY_READER_ERROR);
1580
+ }
1581
+ return runSearch(stage, limit);
1582
+ },
1583
+ async unique() {
1584
+ if (!stage) {
1585
+ throw new Error(LEGACY_READER_ERROR);
1586
+ }
1587
+ const rows = await runSearch(stage, 2);
1588
+ if (rows.length > 1) {
1589
+ throw new NotUniqueError(`unique() on table "${tableName}" matched ${String(rows.length)} documents; expected at most one`);
1590
+ }
1591
+ return rows[0] ?? null;
1592
+ },
1593
+ withIndex() {
1594
+ throw new Error(LEGACY_READER_ERROR);
1595
+ },
1596
+ withSearchIndex(indexName, search) {
1597
+ const searchDefinition = (definition.searchIndexes ?? []).find((index) => index.name === indexName);
1598
+ if (!searchDefinition) {
1599
+ throw new Error(`unknown search index "${indexName}" on table "${tableName}"`);
1600
+ }
1601
+ const searchStage = {
1602
+ definition: searchDefinition,
1603
+ field: searchDefinition.field,
1604
+ filters: [],
1605
+ hasQuery: false,
1606
+ indexName,
1607
+ query: ""
1608
+ };
1609
+ search(createSearchBuilder(searchStage, tableName));
1610
+ if (!searchStage.hasQuery) {
1611
+ throw new Error(`search index "${indexName}" on table "${tableName}" requires a .search(field, query) call`);
1612
+ }
1613
+ return buildReader(searchStage);
1614
+ }
1615
+ };
1616
+ return reader;
1617
+ };
1618
+ return buildReader(void 0);
1619
+ },
1620
+ async rank(tableName, indexName, rankOptions) {
1621
+ const definition = schema.tables[tableName];
1622
+ if (!definition) {
1623
+ throw new Error(`unknown table: ${tableName}`);
1624
+ }
1625
+ const index = definition.rankIndexes?.find((i) => i.name === indexName);
1626
+ if (!index) {
1627
+ throw new Error(`unknown rankIndex "${indexName}" on table "${tableName}"`);
1628
+ }
1629
+ if (rankOptions.restrictsCounts) {
1630
+ throw new CountRlsUnsupportedError(tableName);
1631
+ }
1632
+ await ensureMigrated();
1633
+ const counterReady = await ensureRankBackfilled(tableName, index);
1634
+ if (!counterReady) {
1635
+ return null;
1636
+ }
1637
+ const rowId = typeof rankOptions.row === "string" ? rankOptions.row : rankOptions.row["_id"];
1638
+ if (!rowId) {
1639
+ return null;
1640
+ }
1641
+ const rankTable = rankTableName(tableName, index.name);
1642
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
1643
+ const selectList = sql.join([sql`${sql.identifier("__partition__")}`, ...sortColumns.map((column) => sql`${sql.identifier(column)}`)], sql`, `);
1644
+ const ownRows = await queryAll(
1645
+ exec,
1646
+ dialect,
1647
+ sql`SELECT ${selectList} FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__id__")} = ${rowId}`
1648
+ );
1649
+ const own = ownRows[0];
1650
+ if (!own) {
1651
+ return null;
1652
+ }
1653
+ const partitionKey = own["__partition__"];
1654
+ const effective = mergeWhere(rankOptions.baseWhere, rankOptions.where);
1655
+ assertFlatPredicate(effective, schema, tableName, "rank");
1656
+ const partitionFromWhere = resolveRankPartition(index, effective);
1657
+ if (partitionFromWhere) {
1658
+ const requestedKey = encodePartitionKey(index.partitionBy ?? [], partitionFromWhere);
1659
+ if (requestedKey !== partitionKey) {
1660
+ return null;
1661
+ }
1662
+ }
1663
+ const beforeClause = buildRankBeforeBranches(dialect.name, index, sortColumns, own, rowId);
1664
+ const beforeRows = await queryAll(
1665
+ exec,
1666
+ dialect,
1667
+ sql`SELECT COUNT(*) AS c FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__partition__")} = ${partitionKey}${beforeClause ? sql` AND (${beforeClause})` : sql``}`
1668
+ );
1669
+ const totalRows = await queryAll(
1670
+ exec,
1671
+ dialect,
1672
+ sql`SELECT COUNT(*) AS c FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__partition__")} = ${partitionKey}`
1673
+ );
1674
+ return { position: Number(beforeRows[0]?.["c"] ?? 0) + 1, total: Number(totalRows[0]?.["c"] ?? 0) };
1675
+ },
1676
+ async rankPage(tableName, indexName, rankPageOptions = {}) {
1677
+ assertFlatPredicate(mergeWhere(rankPageOptions.baseWhere, rankPageOptions.where), schema, tableName, "rankPage");
1678
+ const definition = schema.tables[tableName];
1679
+ if (!definition) {
1680
+ throw new Error(`unknown table: ${tableName}`);
1681
+ }
1682
+ const index = definition.rankIndexes?.find((i) => i.name === indexName);
1683
+ if (!index) {
1684
+ throw new Error(`unknown rankIndex "${indexName}" on table "${tableName}"`);
1685
+ }
1686
+ await ensureMigrated();
1687
+ const counterReady = await ensureRankBackfilled(tableName, index);
1688
+ if (!counterReady) {
1689
+ return { continueCursor: null, isDone: true, page: [] };
1690
+ }
1691
+ const rankTable = rankTableName(tableName, index.name);
1692
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
1693
+ const take = Math.max(1, Math.min(1e3, Math.floor(rankPageOptions.take ?? 100)));
1694
+ const effective = mergeWhere(rankPageOptions.baseWhere, rankPageOptions.where);
1695
+ const partitionFromWhere = resolveRankPartition(index, effective);
1696
+ const rankColumns = rankPageColumns(index, sortColumns);
1697
+ const orderBy = sql.join(
1698
+ rankColumns.map((col) => sql`${sql.identifier(col.column)} ${sql.raw(col.direction === "desc" ? "DESC" : "ASC")}`),
1699
+ sql`, `
1700
+ );
1701
+ const whereClauses = [];
1702
+ if (partitionFromWhere) {
1703
+ whereClauses.push(sql`${sql.identifier("__partition__")} = ${encodePartitionKey(index.partitionBy ?? [], partitionFromWhere)}`);
1704
+ }
1705
+ if (rankPageOptions.cursor) {
1706
+ const seek = buildRankCursorSeek(dialect.name, rankColumns, decodeCursor(rankPageOptions.cursor));
1707
+ if (seek !== void 0) {
1708
+ whereClauses.push(seek);
1709
+ }
1710
+ }
1711
+ const selectColumns = sql.join(
1712
+ [
1713
+ sql`${sql.identifier(RANK_TIEBREAK)}`,
1714
+ sql`${sql.identifier("__partition__")}`,
1715
+ ...sortColumns.map((column) => sql`${sql.identifier(column)}`)
1716
+ ],
1717
+ sql`, `
1718
+ );
1719
+ let query = sql`SELECT ${selectColumns} FROM ${sql.identifier(rankTable)}`;
1720
+ if (whereClauses.length > 0) {
1721
+ query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
1722
+ }
1723
+ query = sql`${query} ORDER BY ${orderBy} LIMIT ${sql.raw(String(take + 1))}`;
1724
+ const rankRows = await queryAll(exec, dialect, query);
1725
+ const hasMore = rankRows.length > take;
1726
+ const usable = hasMore ? rankRows.slice(0, take) : rankRows;
1727
+ const ids = usable.map((rankRow) => rankRow[RANK_TIEBREAK]);
1728
+ const documents = await hydrateRankRows(exec, dialect, definition, tableName, ids);
1729
+ let continueCursor = null;
1730
+ const last = usable.at(-1);
1731
+ if (hasMore && last !== void 0) {
1732
+ const cursorValues = [last["__partition__"], ...sortColumns.map((column) => last[column]), last[RANK_TIEBREAK]];
1733
+ continueCursor = encodeRankCursor(cursorValues);
1734
+ }
1735
+ return { continueCursor, isDone: !hasMore, page: documents };
1736
+ },
1737
+ async replace(id, document, expectedTable) {
1738
+ const tableName = await resolveTableName(id, expectedTable);
1739
+ if (!tableName) {
1740
+ throw new Error(`document not found: ${id}`);
1741
+ }
1742
+ const definition = schema.tables[tableName];
1743
+ if (!definition) {
1744
+ throw new Error(`document not found: ${id}`);
1745
+ }
1746
+ const snapshot = await rawRow(tableName, id);
1747
+ if (snapshot === void 0) {
1748
+ throw new Error(`document not found: ${id}`);
1749
+ }
1750
+ const needsPrevious = hasTrigger(schema, tableName, "update") || (definition.aggregateIndexes ?? []).length > 0 || (definition.rankIndexes ?? []).length > 0;
1751
+ const previous = needsPrevious ? decodeRow(definition, snapshot) ?? void 0 : void 0;
1752
+ const creationTime = typeof document["_creationTime"] === "number" ? document["_creationTime"] : clock();
1753
+ const replaced = { ...document, _creationTime: creationTime, _id: id };
1754
+ applyOnUpdate(definition, document, replaced, auth);
1755
+ runRowValidators(definition, replaced);
1756
+ if (hasMatchingTrigger(tableName, "before", "update")) {
1757
+ await fireTriggers("before", "update", { doc: { ...replaced }, id, op: "update", previous, table: tableName });
1758
+ }
1759
+ await ensureBackfilledForTable(tableName);
1760
+ await ensureRankBackfilledForTable(tableName);
1761
+ const fields = Object.keys(definition.shape);
1762
+ const assignments = sql.join(
1763
+ [
1764
+ sql`${sql.identifier("_creationTime")} = ${creationTime}`,
1765
+ // eslint-disable-next-line unicorn/no-null -- SQL bind value: an absent column must bind `null`, not undefined.
1766
+ ...fields.map((field) => sql`${sql.identifier(field)} = ${serializeColumnValue(replaced[field] ?? null)}`)
1767
+ ],
1768
+ sql`, `
1769
+ );
1770
+ await runGuardedWrite(tableName, "UPDATE", assignments, snapshot);
1771
+ await syncAggregates(tableName, previous, replaced);
1772
+ await syncRanks(tableName, id, previous, replaced);
1773
+ await syncSearch(tableName, id, replaced);
1774
+ await recordCdc(tableName, id, "update", replaced);
1775
+ if (hasMatchingTrigger(tableName, "after", "update")) {
1776
+ await fireTriggers("after", "update", { doc: replaced, id, op: "update", previous, table: tableName });
1777
+ }
1778
+ }
1779
+ };
1780
+ triggerContext = { db: writer, scheduler };
1781
+ return writer;
1782
+ };
1783
+
1784
+ export { createSqlCtxDb, decodeGlobalRow, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges };