@lunora/do 0.0.0 → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +115 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/index.d.mts +5599 -0
  5. package/dist/index.d.ts +5599 -0
  6. package/dist/index.mjs +35 -0
  7. package/dist/packem_shared/ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs +313 -0
  8. package/dist/packem_shared/AUTH_METRICS_BUCKET_MS-CiHHYeJi.mjs +84 -0
  9. package/dist/packem_shared/ConflictError-C0STs6bU.mjs +13 -0
  10. package/dist/packem_shared/CountRlsUnsupportedError-28ZvvwKS.mjs +133 -0
  11. package/dist/packem_shared/DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs +237 -0
  12. package/dist/packem_shared/LogBuffer-B_Ezju_N.mjs +37 -0
  13. package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +10 -0
  14. package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-DQkmGiCS.mjs +4009 -0
  15. package/dist/packem_shared/ReactiveCache-ByVzgH3d.mjs +259 -0
  16. package/dist/packem_shared/SCAN_DEP-DLJF8dsj.mjs +19 -0
  17. package/dist/packem_shared/SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs +180 -0
  18. package/dist/packem_shared/SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs +146 -0
  19. package/dist/packem_shared/aggregateTableName-CxNqY1Sl.mjs +64 -0
  20. package/dist/packem_shared/applyCdcChanges-Ctdmxmrv.mjs +103 -0
  21. package/dist/packem_shared/applyOnDelete-CMif2RKw.mjs +165 -0
  22. package/dist/packem_shared/armRestore-BJk53Ro8.mjs +55 -0
  23. package/dist/packem_shared/assertFlatPredicate-DyVYReuT.mjs +160 -0
  24. package/dist/packem_shared/assertReadonly-dDcFE1YZ.mjs +29 -0
  25. package/dist/packem_shared/assertValidClientId-CBZ1zC96.mjs +1745 -0
  26. package/dist/packem_shared/backfillAggregateIndexes-BF5eL7kW.mjs +80 -0
  27. package/dist/packem_shared/buildSecurityAudit-CCAvoFlr.mjs +1 -0
  28. package/dist/packem_shared/buildSeekWhere-lVsNXSLy.mjs +84 -0
  29. package/dist/packem_shared/clearCapturedMail-CPpgl-dX.mjs +104 -0
  30. package/dist/packem_shared/compileWhereSql-CXrhFA3G.mjs +127 -0
  31. package/dist/packem_shared/createSystemReader-8CzSZP9V.mjs +80 -0
  32. package/dist/packem_shared/ctx-db-idempotency-DkC9rP91.mjs +35 -0
  33. package/dist/packem_shared/do-exec-5eQy5cEi.mjs +12 -0
  34. package/dist/packem_shared/do-sql-BCHCWtrD.mjs +87 -0
  35. package/dist/packem_shared/encodePartitionKey-C6blLR5K.mjs +1 -0
  36. package/dist/packem_shared/ensureFunctionMetricsTables-UDNVD7FS.mjs +248 -0
  37. package/dist/packem_shared/exportShardRows-DZEhUeyI.mjs +156 -0
  38. package/dist/packem_shared/ftsTableName-BLEMawrp.mjs +38 -0
  39. package/dist/packem_shared/guardWriter-u3UlnCH5.mjs +128 -0
  40. package/dist/packem_shared/matchesStaticWhere-CFk6adSu.mjs +54 -0
  41. package/dist/packem_shared/rank-CrkEIpF4.mjs +102 -0
  42. package/dist/packem_shared/renderSql-D6eUcn2N.mjs +16 -0
  43. package/dist/packem_shared/runShardMigrations-C3bn5r93.mjs +103 -0
  44. package/dist/packem_shared/runTriggers-5N6_Fx0A.mjs +20 -0
  45. package/dist/packem_shared/security-audit-CucgBice.mjs +158 -0
  46. package/dist/packem_shared/serveRelationFanout-Clr1a05L.mjs +24 -0
  47. package/package.json +41 -17
@@ -0,0 +1,1745 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { matchesStaticWhere, aggregateSqlFunction, normalizeCountArgument, throwingScheduler } from './matchesStaticWhere-CFk6adSu.mjs';
3
+ import { encodeAggregateKey, foldAggregateTally, aggregateTableName, coerceAggregateNumber, readAggregateValue } from './aggregateTableName-CxNqY1Sl.mjs';
4
+ import { mergeWhere, CountRlsUnsupportedError, selectIndexForGroupBy, selectIndexForCount, selectIndexForAggregate } from './CountRlsUnsupportedError-28ZvvwKS.mjs';
5
+ import { appendCdcChange } from './applyCdcChanges-Ctdmxmrv.mjs';
6
+ export { CDC_LOG_TABLE, CDC_META_TABLE, applyCdcChanges, bumpCdcEpoch, migrateCdcLog, migrateCdcMeta, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './applyCdcChanges-Ctdmxmrv.mjs';
7
+ import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
8
+ import { i as isFtsAvailable, D as DOC_COLUMN$1, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT, d as aggUpsertSql, j as jsonPathSql, q as quoteIdentifier, t as tableColumns, e as qualifiedJsonPathSql } from './do-sql-BCHCWtrD.mjs';
9
+ import { param } from './renderSql-D6eUcn2N.mjs';
10
+ import { s as sortColumnName, m as matchesRankStaticWhere, e as encodePartitionKey, b as serializeSqlValue, r as rankTableName, a as resolveRankPartition, R as RANK_TIEBREAK } from './rank-CrkEIpF4.mjs';
11
+ import { stringifySearchText, ftsTableName, tokenizeSearch, buildFtsMatch, scoreDocument } from './ftsTableName-BLEMawrp.mjs';
12
+ import { SCAN_DEP } from './SCAN_DEP-DLJF8dsj.mjs';
13
+ import { decodeCursor, normalizeOrderKeys, buildSeekWhere, encodeCursor, buildSeekBeforeWhere } from './buildSeekWhere-lVsNXSLy.mjs';
14
+ import NotFoundError from './NotFoundError-CMuMZt81.mjs';
15
+ import { assertFlatPredicate, resolveRelationPredicates } from './assertFlatPredicate-DyVYReuT.mjs';
16
+ import { runRowValidators, resolveWith, applyOnDelete } from './applyOnDelete-CMif2RKw.mjs';
17
+ import { guardWriter } from './guardWriter-u3UlnCH5.mjs';
18
+ import { createSystemReader } from './createSystemReader-8CzSZP9V.mjs';
19
+ import { ConflictError } from './ConflictError-C0STs6bU.mjs';
20
+ import { runTriggers } from './runTriggers-5N6_Fx0A.mjs';
21
+ import { compileWhereSql } from './compileWhereSql-CXrhFA3G.mjs';
22
+ export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-BF5eL7kW.mjs';
23
+ export { I as IDEMPOTENCY_TABLE, m as migrateIdempotency, r as readIdempotent, t as trimIdempotent, w as writeIdempotent } from './ctx-db-idempotency-DkC9rP91.mjs';
24
+ export { runShardMigrations } from './runShardMigrations-C3bn5r93.mjs';
25
+
26
+ const rankIndexFieldsUnchanged = (index, previous, next) => {
27
+ const fields = [...index.partitionBy ?? [], ...index.sortBy.map((key) => key.field), ...index.where ? Object.keys(index.where) : []];
28
+ return fields.every((field) => previous[field] === next[field]);
29
+ };
30
+ const syncRankIndexEntry = (sql$1, tableName, index, id, previous, next) => {
31
+ if (previous && next && rankIndexFieldsUnchanged(index, previous, next)) {
32
+ return;
33
+ }
34
+ const rankTable = rankTableName(tableName, index.name);
35
+ if (previous) {
36
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__id__")} = ${id}`);
37
+ }
38
+ if (!next || index.where && !matchesRankStaticWhere(next, index.where)) {
39
+ return;
40
+ }
41
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
42
+ const columnsSql = sql.join(
43
+ ["__id__", "__partition__", ...sortColumns].map((column) => sql.identifier(column)),
44
+ sql`, `
45
+ );
46
+ const partitionKey = encodePartitionKey(index.partitionBy ?? [], next);
47
+ const sortValues = index.sortBy.map((key) => serializeSqlValue(next[key.field] ?? null));
48
+ const valuesSql = sql.join(
49
+ [id, partitionKey, ...sortValues].map((value) => param(value)),
50
+ sql`, `
51
+ );
52
+ runDrizzle(sql$1, sql`INSERT INTO ${sql.identifier(rankTable)} (${columnsSql}) VALUES (${valuesSql})`);
53
+ };
54
+ const createCompanionSync = (deps) => {
55
+ const { broadcast, invalidateCache, recordCdc, schema, sql: sql$1 } = deps;
56
+ const backfilled = /* @__PURE__ */ new Set();
57
+ const rankBackfilled = /* @__PURE__ */ new Set();
58
+ const ensureBackfilledIndex = (tableName, index) => {
59
+ const cacheKey = `${tableName}::${index.name}`;
60
+ if (backfilled.has(cacheKey)) {
61
+ return;
62
+ }
63
+ const aggTable = aggregateTableName(tableName, index.name);
64
+ const by = index.by ?? [];
65
+ const tallies = /* @__PURE__ */ new Map();
66
+ const rows = runDrizzle(sql$1, sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`).toArray();
67
+ for (const row of rows) {
68
+ const record = rowToDocument(row);
69
+ if (!record) {
70
+ continue;
71
+ }
72
+ if (index.where && !matchesStaticWhere(record, index.where)) {
73
+ continue;
74
+ }
75
+ const encoded = encodeAggregateKey(by, record);
76
+ foldAggregateTally(tallies, encoded, index, record);
77
+ }
78
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(aggTable)}`);
79
+ const CHUNK_ROWS = 32;
80
+ const entries = [...tallies];
81
+ for (let start = 0; start < entries.length; start += CHUNK_ROWS) {
82
+ const chunk = entries.slice(start, start + CHUNK_ROWS);
83
+ const rowsSql = sql.join(
84
+ chunk.map(([encoded, tally]) => sql`(${encoded}, ${tally.value}, ${tally.count})`),
85
+ sql`, `
86
+ );
87
+ runDrizzle(sql$1, sql`INSERT INTO ${sql.identifier(aggTable)} (${AGG_KEY}, ${AGG_VALUE}, ${AGG_COUNT}) VALUES ${rowsSql}`);
88
+ }
89
+ backfilled.add(cacheKey);
90
+ };
91
+ const recomputeExtreme = (tableName, index, record) => {
92
+ const by = index.by ?? [];
93
+ const sqlFunction = aggregateSqlFunction(index.op);
94
+ const field = index.field ?? "";
95
+ const conditions = [];
96
+ for (const key of by) {
97
+ const value = serializeSqlValue(record[key] ?? null);
98
+ if (value === null) {
99
+ conditions.push(sql`${jsonPathSql(key)} IS NULL`);
100
+ } else {
101
+ conditions.push(sql`${jsonPathSql(key)} = ${value}`);
102
+ }
103
+ }
104
+ for (const [key, expected] of Object.entries(index.where ?? {})) {
105
+ const literal = expected !== null && typeof expected === "object" && !Array.isArray(expected) ? expected.eq : expected;
106
+ const value = serializeSqlValue(literal);
107
+ if (value === null) {
108
+ conditions.push(sql`${jsonPathSql(key)} IS NULL`);
109
+ } else {
110
+ conditions.push(sql`${jsonPathSql(key)} = ${value}`);
111
+ }
112
+ }
113
+ const whereSql = conditions.length > 0 ? sql` WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
114
+ const ref = jsonPathSql(field);
115
+ const row = runDrizzle(
116
+ sql$1,
117
+ sql`SELECT ${sql.raw(sqlFunction)}(${ref}) AS value FROM ${sql.identifier(tableName)}${whereSql}`
118
+ ).one();
119
+ return { value: row.value ?? null };
120
+ };
121
+ const applyAggregateDelta = (tableName, index, previous, next) => {
122
+ const aggTable = aggregateTableName(tableName, index.name);
123
+ const { op } = index;
124
+ const field = index.field ?? "";
125
+ const pruneIfEmpty = (encodedKey) => {
126
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(aggTable)} WHERE ${AGG_KEY} = ${encodedKey} AND ${AGG_COUNT} <= 0`);
127
+ };
128
+ const removes = previous && (!index.where || matchesStaticWhere(previous, index.where)) ? previous : void 0;
129
+ const adds = next && (!index.where || matchesStaticWhere(next, index.where)) ? next : void 0;
130
+ if (!removes && !adds) {
131
+ return;
132
+ }
133
+ if (op === "count") {
134
+ for (const [record, delta] of [
135
+ [removes, -1],
136
+ [adds, 1]
137
+ ]) {
138
+ if (!record) {
139
+ continue;
140
+ }
141
+ const encoded = encodeAggregateKey(index.by ?? [], record);
142
+ runDrizzle(
143
+ sql$1,
144
+ aggUpsertSql(
145
+ aggTable,
146
+ encoded,
147
+ delta,
148
+ delta,
149
+ sql`${AGG_VALUE} = ${AGG_VALUE} + excluded.${AGG_VALUE}, ${AGG_COUNT} = ${AGG_COUNT} + excluded.${AGG_COUNT}`
150
+ )
151
+ );
152
+ }
153
+ if (removes) {
154
+ pruneIfEmpty(encodeAggregateKey(index.by ?? [], removes));
155
+ }
156
+ return;
157
+ }
158
+ if (op === "sum" || op === "avg") {
159
+ for (const [record, sign] of [
160
+ [removes, -1],
161
+ [adds, 1]
162
+ ]) {
163
+ if (!record) {
164
+ continue;
165
+ }
166
+ const numeric = coerceAggregateNumber(record[field]);
167
+ if (numeric === void 0) {
168
+ continue;
169
+ }
170
+ const encoded = encodeAggregateKey(index.by ?? [], record);
171
+ runDrizzle(
172
+ sql$1,
173
+ aggUpsertSql(
174
+ aggTable,
175
+ encoded,
176
+ sign * numeric,
177
+ sign,
178
+ sql`${AGG_VALUE} = COALESCE(${AGG_VALUE}, 0) + excluded.${AGG_VALUE}, ${AGG_COUNT} = ${AGG_COUNT} + excluded.${AGG_COUNT}`
179
+ )
180
+ );
181
+ }
182
+ if (removes) {
183
+ pruneIfEmpty(encodeAggregateKey(index.by ?? [], removes));
184
+ }
185
+ return;
186
+ }
187
+ if (removes) {
188
+ const encoded = encodeAggregateKey(index.by ?? [], removes);
189
+ const removedValue = coerceAggregateNumber(removes[field]);
190
+ const existing = runDrizzle(
191
+ sql$1,
192
+ sql`SELECT ${AGG_VALUE} AS value, ${AGG_COUNT} AS count FROM ${sql.identifier(aggTable)} WHERE ${AGG_KEY} = ${encoded}`
193
+ ).toArray()[0];
194
+ const remainingCount = (existing?.count ?? 0) - 1;
195
+ if (remainingCount <= 0) {
196
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(aggTable)} WHERE ${AGG_KEY} = ${encoded}`);
197
+ } else if (existing && removedValue !== void 0 && existing.value !== null && removedValue === existing.value) {
198
+ const recomputed = recomputeExtreme(tableName, index, removes);
199
+ runDrizzle(
200
+ sql$1,
201
+ sql`UPDATE ${sql.identifier(aggTable)} SET ${AGG_VALUE} = ${recomputed.value}, ${AGG_COUNT} = ${remainingCount} WHERE ${AGG_KEY} = ${encoded}`
202
+ );
203
+ } else {
204
+ runDrizzle(sql$1, sql`UPDATE ${sql.identifier(aggTable)} SET ${AGG_COUNT} = ${AGG_COUNT} - 1 WHERE ${AGG_KEY} = ${encoded}`);
205
+ }
206
+ }
207
+ if (adds) {
208
+ const encoded = encodeAggregateKey(index.by ?? [], adds);
209
+ const addedValue = coerceAggregateNumber(adds[field]);
210
+ if (addedValue === void 0) {
211
+ runDrizzle(
212
+ sql$1,
213
+ // eslint-disable-next-line unicorn/no-null -- seeds an extreme-less group with NULL value
214
+ aggUpsertSql(aggTable, encoded, null, 1, sql`${AGG_COUNT} = ${AGG_COUNT} + 1`)
215
+ );
216
+ } else {
217
+ const op2 = op === "min" ? "MIN" : "MAX";
218
+ runDrizzle(
219
+ sql$1,
220
+ aggUpsertSql(
221
+ aggTable,
222
+ encoded,
223
+ addedValue,
224
+ 1,
225
+ sql`${AGG_VALUE} = ${sql.raw(op2)}(COALESCE(${AGG_VALUE}, excluded.${AGG_VALUE}), excluded.${AGG_VALUE}), ${AGG_COUNT} = ${AGG_COUNT} + 1`
226
+ )
227
+ );
228
+ }
229
+ }
230
+ };
231
+ const ensureBackfilledForTable = (tableName) => {
232
+ const indexes = schema.tables[tableName]?.aggregateIndexes;
233
+ if (!indexes || indexes.length === 0) {
234
+ return;
235
+ }
236
+ for (const index of indexes) {
237
+ ensureBackfilledIndex(tableName, index);
238
+ }
239
+ };
240
+ const syncAggregates = (tableName, previous, next) => {
241
+ const indexes = schema.tables[tableName]?.aggregateIndexes;
242
+ if (!indexes || indexes.length === 0) {
243
+ return;
244
+ }
245
+ for (const index of indexes) {
246
+ applyAggregateDelta(tableName, index, previous, next);
247
+ }
248
+ };
249
+ const ensureRankBackfilled = (tableName, index) => {
250
+ const cacheKey = `${tableName}::rank::${index.name}`;
251
+ if (rankBackfilled.has(cacheKey)) {
252
+ return;
253
+ }
254
+ const rankTable = rankTableName(tableName, index.name);
255
+ const rows = runDrizzle(sql$1, sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`).toArray();
256
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(rankTable)}`);
257
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
258
+ const columnsSql = sql.join(
259
+ ["__id__", "__partition__", ...sortColumns].map((column) => sql.identifier(column)),
260
+ sql`, `
261
+ );
262
+ for (const row of rows) {
263
+ const record = rowToDocument(row);
264
+ if (!record) {
265
+ continue;
266
+ }
267
+ if (index.where && !matchesRankStaticWhere(record, index.where)) {
268
+ continue;
269
+ }
270
+ const partitionKey = encodePartitionKey(index.partitionBy ?? [], record);
271
+ const sortValues = index.sortBy.map((key) => serializeSqlValue(record[key.field] ?? null));
272
+ const valuesSql = sql.join(
273
+ [record["_id"], partitionKey, ...sortValues].map((value) => param(value)),
274
+ sql`, `
275
+ );
276
+ runDrizzle(sql$1, sql`INSERT INTO ${sql.identifier(rankTable)} (${columnsSql}) VALUES (${valuesSql})`);
277
+ }
278
+ rankBackfilled.add(cacheKey);
279
+ };
280
+ const ensureRankBackfilledForTable = (tableName) => {
281
+ const indexes = schema.tables[tableName]?.rankIndexes;
282
+ if (!indexes || indexes.length === 0) {
283
+ return;
284
+ }
285
+ for (const index of indexes) {
286
+ ensureRankBackfilled(tableName, index);
287
+ }
288
+ };
289
+ const syncRanks = (tableName, id, previous, next) => {
290
+ const indexes = schema.tables[tableName]?.rankIndexes;
291
+ if (!indexes || indexes.length === 0) {
292
+ return;
293
+ }
294
+ for (const index of indexes) {
295
+ syncRankIndexEntry(sql$1, tableName, index, id, previous, next);
296
+ }
297
+ };
298
+ const syncSearch = (tableName, id, document) => {
299
+ const indexes = schema.tables[tableName]?.searchIndexes;
300
+ if (!indexes || indexes.length === 0 || !isFtsAvailable(sql$1)) {
301
+ return;
302
+ }
303
+ for (const index of indexes) {
304
+ const ftName = ftsTableName(tableName, index.name);
305
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(ftName)} WHERE ${sql.identifier("__id__")} = ${id}`);
306
+ if (document) {
307
+ runDrizzle(
308
+ sql$1,
309
+ sql`INSERT INTO ${sql.identifier(ftName)} (${sql.identifier("__text__")}, ${sql.identifier("__id__")}) VALUES (${stringifySearchText(document[index.field])}, ${id})`
310
+ );
311
+ }
312
+ }
313
+ };
314
+ const syncCompanionsForInsert = (tableName, id, document) => {
315
+ syncSearch(tableName, id, document);
316
+ syncAggregates(tableName, void 0, document);
317
+ syncRanks(tableName, id, void 0, document);
318
+ invalidateCache(tableName, id);
319
+ recordCdc(tableName, id, "insert", document);
320
+ broadcast({ key: id, op: "insert", row: document, table: tableName });
321
+ };
322
+ return {
323
+ ensureBackfilledForTable,
324
+ ensureBackfilledIndex,
325
+ ensureRankBackfilled,
326
+ ensureRankBackfilledForTable,
327
+ syncAggregates,
328
+ syncCompanionsForInsert,
329
+ syncRanks,
330
+ syncSearch
331
+ };
332
+ };
333
+
334
+ const DOC_COLUMN = "__doc__";
335
+ const encodeRankCursor = (values) => {
336
+ const json = JSON.stringify(values);
337
+ const bytes = new TextEncoder().encode(json);
338
+ let binary = "";
339
+ for (const byte of bytes) {
340
+ binary += String.fromCodePoint(byte);
341
+ }
342
+ return btoa(binary);
343
+ };
344
+ const resolveRankSeekTuple = (options) => {
345
+ if (options.after) {
346
+ return [options.after.partitionKey, ...options.after.sortValues, options.after.rowId];
347
+ }
348
+ return options.cursor ? decodeCursor(options.cursor) : void 0;
349
+ };
350
+ const buildRankSeekClause = (decoded, sortColumns, sortBy) => {
351
+ if (decoded?.length !== 1 + sortColumns.length + 1) {
352
+ return void 0;
353
+ }
354
+ const cols = [{ column: "__partition__", direction: "asc" }];
355
+ for (const [i, column] of sortColumns.entries()) {
356
+ cols.push({ column, direction: sortBy[i]?.direction ?? "asc" });
357
+ }
358
+ cols.push({ column: RANK_TIEBREAK, direction: "asc" });
359
+ const branches = [];
360
+ for (const [pivot, col] of cols.entries()) {
361
+ const conditions = [];
362
+ for (const [prefix, prefixCol] of cols.slice(0, pivot).entries()) {
363
+ conditions.push(sql`${sql.identifier(prefixCol.column)} IS ${decoded[prefix]}`);
364
+ }
365
+ conditions.push(sql`${sql.identifier(col.column)} ${sql.raw(col.direction === "desc" ? "<" : ">")} ${decoded[pivot]}`);
366
+ const [firstCondition] = conditions;
367
+ branches.push(conditions.length === 1 && firstCondition !== void 0 ? firstCondition : sql`(${sql.join(conditions, sql` AND `)})`);
368
+ }
369
+ return sql`(${sql.join(branches, sql` OR `)})`;
370
+ };
371
+ const NO_RANK_CURSOR = null;
372
+ const buildRankContinueCursor = (last, sortColumns) => {
373
+ if (last === void 0) {
374
+ return NO_RANK_CURSOR;
375
+ }
376
+ const cursorValues = [last["__partition__"], ...sortColumns.map((column) => last[column]), last[RANK_TIEBREAK]];
377
+ return encodeRankCursor(cursorValues);
378
+ };
379
+ const buildRankPageRows = (rankRows, byId, sortColumns) => {
380
+ const rows = [];
381
+ for (const rankRow of rankRows) {
382
+ const rowId = rankRow[RANK_TIEBREAK];
383
+ if (typeof rowId !== "string") {
384
+ continue;
385
+ }
386
+ const doc = byId.get(rowId);
387
+ if (!doc) {
388
+ continue;
389
+ }
390
+ const partitionKey = typeof rankRow["__partition__"] === "string" ? rankRow["__partition__"] : "";
391
+ const sortValues = sortColumns.map((column) => rankRow[column] ?? null);
392
+ rows.push({ doc, key: { partitionKey, rowId, sortValues } });
393
+ }
394
+ return rows;
395
+ };
396
+ const hydrateDocsById = (deps, tableName, ids) => {
397
+ const { rowToDocument } = deps;
398
+ const byId = /* @__PURE__ */ new Map();
399
+ if (ids.length === 0) {
400
+ return byId;
401
+ }
402
+ const idList = sql.join(
403
+ ids.map((id) => param(id)),
404
+ sql`, `
405
+ );
406
+ const documentRows = runDrizzle(
407
+ deps.sql,
408
+ sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN)} FROM ${sql.identifier(tableName)} WHERE id IN (${idList})`
409
+ ).toArray();
410
+ for (const documentRow of documentRows) {
411
+ const record = rowToDocument(documentRow);
412
+ const recordId = documentRow["id"];
413
+ if (record && typeof recordId === "string") {
414
+ byId.set(recordId, record);
415
+ }
416
+ }
417
+ return byId;
418
+ };
419
+ const computeRankPage = (deps, tableName, indexName, rankPageOptions) => {
420
+ const { assertRankPartitionLocal, ensureRankBackfilled, onRead, schema } = deps;
421
+ const definition = schema.tables[tableName];
422
+ if (!definition) {
423
+ throw new Error(`unknown table: ${tableName}`);
424
+ }
425
+ const index = definition.rankIndexes?.find((i) => i.name === indexName);
426
+ if (!index) {
427
+ throw new Error(`unknown rankIndex "${indexName}" on table "${tableName}"`);
428
+ }
429
+ assertRankPartitionLocal(tableName, definition, index);
430
+ onRead(tableName, SCAN_DEP);
431
+ ensureRankBackfilled(tableName, index);
432
+ const rankTable = rankTableName(tableName, index.name);
433
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
434
+ const take = Math.max(1, Math.min(1e3, Math.floor(rankPageOptions.take ?? 100)));
435
+ const effective = mergeWhere(rankPageOptions.baseWhere, rankPageOptions.where);
436
+ const partitionFromWhere = resolveRankPartition(index, effective);
437
+ const orderClauses = [sql`${sql.identifier("__partition__")} ASC`];
438
+ for (const [i, column] of sortColumns.entries()) {
439
+ const direction = index.sortBy[i]?.direction;
440
+ orderClauses.push(sql`${sql.identifier(column)} ${sql.raw(direction === "desc" ? "DESC" : "ASC")}`);
441
+ }
442
+ orderClauses.push(sql`${sql.identifier(RANK_TIEBREAK)} ASC`);
443
+ const whereClauses = [];
444
+ if (typeof rankPageOptions.partitionKey === "string") {
445
+ whereClauses.push(sql`${sql.identifier("__partition__")} = ${rankPageOptions.partitionKey}`);
446
+ } else if (partitionFromWhere) {
447
+ whereClauses.push(sql`${sql.identifier("__partition__")} = ${encodePartitionKey(index.partitionBy ?? [], partitionFromWhere)}`);
448
+ }
449
+ const decoded = resolveRankSeekTuple(rankPageOptions);
450
+ const seek = buildRankSeekClause(decoded, sortColumns, index.sortBy);
451
+ if (seek) {
452
+ whereClauses.push(seek);
453
+ }
454
+ const idColumn = sql.identifier(RANK_TIEBREAK);
455
+ const partitionColumn = sql.identifier("__partition__");
456
+ const innerWhere = whereClauses.length > 0 ? sql` WHERE ${sql.join(whereClauses, sql` AND `)}` : sql``;
457
+ const selectColumns = sortColumns.length > 0 ? sql`${idColumn}, ${partitionColumn}, ${sql.join(
458
+ sortColumns.map((column) => sql.identifier(column)),
459
+ sql`, `
460
+ )}` : sql`${idColumn}, ${partitionColumn}`;
461
+ const query = sql`SELECT ${selectColumns} FROM ${sql.identifier(rankTable)}${innerWhere} ORDER BY ${sql.join(orderClauses, sql`, `)} LIMIT ${sql.raw(String(take + 1))}`;
462
+ const rankRows = runDrizzle(deps.sql, query).toArray();
463
+ const hasMore = rankRows.length > take;
464
+ const usable = hasMore ? rankRows.slice(0, take) : rankRows;
465
+ const pageIds = usable.map((rankRow) => rankRow[RANK_TIEBREAK]);
466
+ const rows = buildRankPageRows(usable, hydrateDocsById(deps, tableName, pageIds), sortColumns);
467
+ const continueCursor = hasMore ? buildRankContinueCursor(usable.at(-1), sortColumns) : NO_RANK_CURSOR;
468
+ const directions = index.sortBy.map((key) => key.direction === "desc" ? "desc" : "asc");
469
+ return { continueCursor, directions, hasMore, rows };
470
+ };
471
+
472
+ const CLIENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
473
+ const assertValidClientId = (clientId) => {
474
+ if (!CLIENT_ID_PATTERN.test(clientId)) {
475
+ throw new Error(`invalid clientId ${JSON.stringify(clientId)}: a client-supplied row id must be a UUID`);
476
+ }
477
+ };
478
+ const MAX_TRIGGER_DEPTH = 50;
479
+ const DEFAULT_BATCH_LIMIT = 500;
480
+ const assertBatchLimit = (count, limit, op) => {
481
+ const cap = limit ?? DEFAULT_BATCH_LIMIT;
482
+ if (count > cap) {
483
+ throw Object.assign(new Error(`${op}: batch of ${String(count)} exceeds the limit of ${String(cap)} (raise options.limit or chunk the call)`), {
484
+ code: "BATCH_LIMIT_EXCEEDED",
485
+ name: "LunoraError",
486
+ status: 400
487
+ });
488
+ }
489
+ };
490
+ const createRangeBuilder = (stage) => {
491
+ const builder = {
492
+ eq: (field, value) => {
493
+ stage.sqlConditions.push({ comparator: "=", field, value });
494
+ return builder;
495
+ },
496
+ gt: (field, value) => {
497
+ stage.sqlConditions.push({ comparator: ">", field, value });
498
+ return builder;
499
+ },
500
+ gte: (field, value) => {
501
+ stage.sqlConditions.push({ comparator: ">=", field, value });
502
+ return builder;
503
+ },
504
+ lt: (field, value) => {
505
+ stage.sqlConditions.push({ comparator: "<", field, value });
506
+ return builder;
507
+ },
508
+ lte: (field, value) => {
509
+ stage.sqlConditions.push({ comparator: "<=", field, value });
510
+ return builder;
511
+ }
512
+ };
513
+ return builder;
514
+ };
515
+ const createSearchBuilder = (search, tableName) => {
516
+ const builder = {
517
+ eq: (field, value) => {
518
+ if (!search.definition.filterFields?.includes(field)) {
519
+ throw new Error(`field "${field}" is not a filter field of search index "${search.indexName}" on table "${tableName}"`);
520
+ }
521
+ search.filters.push({ field, value });
522
+ return builder;
523
+ },
524
+ search: (field, query) => {
525
+ if (field !== search.definition.field) {
526
+ throw new Error(`search index "${search.indexName}" on table "${tableName}" indexes "${search.definition.field}", not "${field}"`);
527
+ }
528
+ const stage = search;
529
+ stage.field = field;
530
+ stage.query = query;
531
+ stage.hasQuery = true;
532
+ return builder;
533
+ }
534
+ };
535
+ return builder;
536
+ };
537
+ const searchViaFts = (sql$1, tableName, search, limit) => {
538
+ const tokens = tokenizeSearch(search.query);
539
+ if (tokens.length === 0) {
540
+ return [];
541
+ }
542
+ const ftName = ftsTableName(tableName, search.indexName);
543
+ const whereClauses = [sql`f.${sql.identifier("__text__")} MATCH ${buildFtsMatch(tokens)}`];
544
+ for (const filter of search.filters) {
545
+ whereClauses.push(sql`${jsonPathSql(filter.field)} = ${serializeSqlValue(filter.value)}`);
546
+ }
547
+ let query = sql`SELECT m.id, m._creationTime, m.${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(ftName)} f JOIN ${sql.identifier(tableName)} m ON m.id = f.${sql.identifier("__id__")} WHERE ${sql.join(whereClauses, sql` AND `)} ORDER BY f.rank, m._creationTime DESC`;
548
+ if (typeof limit === "number") {
549
+ query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
550
+ }
551
+ const rows = runDrizzle(sql$1, query).toArray();
552
+ const docs = [];
553
+ for (const row of rows) {
554
+ const record = rowToDocument(row);
555
+ if (record) {
556
+ docs.push(record);
557
+ }
558
+ }
559
+ return docs;
560
+ };
561
+ const searchViaScan = (sql$1, tableName, search, limit) => {
562
+ const tokens = tokenizeSearch(search.query);
563
+ if (tokens.length === 0) {
564
+ return [];
565
+ }
566
+ const whereClauses = [];
567
+ for (const filter of search.filters) {
568
+ whereClauses.push(sql`${jsonPathSql(filter.field)} = ${serializeSqlValue(filter.value)}`);
569
+ }
570
+ let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
571
+ if (whereClauses.length > 0) {
572
+ query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
573
+ }
574
+ const rows = runDrizzle(sql$1, query).toArray();
575
+ const scored = [];
576
+ for (const row of rows) {
577
+ const record = rowToDocument(row);
578
+ if (!record) {
579
+ continue;
580
+ }
581
+ const score = scoreDocument(stringifySearchText(record[search.field]), tokens);
582
+ if (score > 0) {
583
+ scored.push({ creationTime: typeof record["_creationTime"] === "number" ? record["_creationTime"] : 0, doc: record, score });
584
+ }
585
+ }
586
+ scored.sort((a, b) => b.score - a.score || b.creationTime - a.creationTime);
587
+ const docs = scored.map((entry) => entry.doc);
588
+ return typeof limit === "number" ? docs.slice(0, Math.max(0, Math.floor(limit))) : docs;
589
+ };
590
+ const doWhereSqlStrategy = { fieldRef: jsonPathSql, serialize: serializeSqlValue };
591
+ const makeRelationExistsSqlStrategy = (onRead) => {
592
+ let aliasCounter = 0;
593
+ const scopeStack = [];
594
+ const strategy = {
595
+ fieldRef: jsonPathSql,
596
+ relationExists: (request) => {
597
+ const { childWhere, negated, parentTable, relation } = request;
598
+ const alias = `__rel_${String(aliasCounter)}`;
599
+ const parentRef = scopeStack.at(-1) ?? parentTable;
600
+ aliasCounter += 1;
601
+ onRead(relation.table, SCAN_DEP);
602
+ const parentColumn = relation.kind === "one" ? relation.field : relation.references;
603
+ const childColumn = relation.kind === "one" ? relation.references : relation.field;
604
+ const correlation = sql`${qualifiedJsonPathSql(alias, childColumn)} = ${qualifiedJsonPathSql(parentRef, parentColumn)}`;
605
+ scopeStack.push(alias);
606
+ const childSql = compileWhereSql(childWhere, strategy);
607
+ scopeStack.pop();
608
+ const condition = childSql ? sql`${correlation} AND ${childSql}` : correlation;
609
+ const body = sql`EXISTS (SELECT 1 FROM ${sql.identifier(relation.table)} AS ${sql.identifier(alias)} WHERE ${condition})`;
610
+ return negated ? sql`NOT ${body}` : body;
611
+ },
612
+ serialize: serializeSqlValue
613
+ };
614
+ return strategy;
615
+ };
616
+ const compileOrderBySql = (keys) => {
617
+ const parts = keys.map((key) => sql`${jsonPathSql(key.field)} ${sql.raw(key.direction === "desc" ? "DESC" : "ASC")}`);
618
+ if (!keys.some((key) => key.field === "_id" || key.field === "id")) {
619
+ parts.push(sql`${jsonPathSql("id")} ASC`);
620
+ }
621
+ return sql.join(parts, sql`, `);
622
+ };
623
+ const COMPARATOR_TO_OPERATOR = { "<": "lt", "<=": "lte", "=": "eq", ">": "gt", ">=": "gte" };
624
+ const paginateOrderKeys = (stage) => {
625
+ const direction = stage.order;
626
+ if (stage.indexFields.length > 0) {
627
+ return stage.indexFields.map((field) => {
628
+ return { direction, field };
629
+ });
630
+ }
631
+ return [{ direction, field: "_creationTime" }];
632
+ };
633
+ const paginateWhere = (stage, orderKeys, cursor, endCursor) => {
634
+ const clauses = stage.sqlConditions.map((condition) => {
635
+ return {
636
+ [condition.field]: { [COMPARATOR_TO_OPERATOR[condition.comparator] ?? "eq"]: condition.value }
637
+ };
638
+ });
639
+ if (cursor) {
640
+ clauses.push(buildSeekWhere(orderKeys, decodeCursor(cursor)));
641
+ }
642
+ if (endCursor) {
643
+ clauses.push(buildSeekBeforeWhere(orderKeys, decodeCursor(endCursor)));
644
+ }
645
+ if (clauses.length === 0) {
646
+ return void 0;
647
+ }
648
+ return clauses.length === 1 ? clauses[0] : { AND: clauses };
649
+ };
650
+ const scanDocs = (rows, filters, cap) => {
651
+ const docs = [];
652
+ for (const row of rows) {
653
+ const record = rowToDocument(row);
654
+ if (record && filters.every((predicate) => predicate(record))) {
655
+ docs.push(record);
656
+ if (cap !== void 0 && docs.length > cap) {
657
+ break;
658
+ }
659
+ }
660
+ }
661
+ return docs;
662
+ };
663
+ const paginateStage = (sql$1, tableName, stage, options) => {
664
+ const numberItems = Math.max(0, Math.floor(options.numItems));
665
+ const orderKeys = paginateOrderKeys(stage);
666
+ const bounded = typeof options.endCursor === "string";
667
+ const whereCondition = compileWhereSql(paginateWhere(stage, orderKeys, options.cursor, options.endCursor), doWhereSqlStrategy);
668
+ let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
669
+ if (whereCondition) {
670
+ query = sql`${query} WHERE ${whereCondition}`;
671
+ }
672
+ query = sql`${query} ORDER BY ${compileOrderBySql(orderKeys)}`;
673
+ const filtered = stage.inMemoryFilters.length > 0;
674
+ if (!filtered && !bounded) {
675
+ query = sql`${query} LIMIT ${sql.raw(String(numberItems + 1))}`;
676
+ }
677
+ const rows = runDrizzle(sql$1, query).toArray();
678
+ const docs = scanDocs(rows, stage.inMemoryFilters, filtered || bounded ? void 0 : numberItems);
679
+ if (bounded) {
680
+ const middle = docs.length >= 2 ? docs[Math.floor(docs.length / 2) - 1] : void 0;
681
+ return {
682
+ // eslint-disable-next-line unicorn/no-null -- QueryPage.continueCursor is `null | string`; a bounded page echoes its fixed endCursor (never null in this branch since `bounded` requires it), the `?? null` only satisfies the type
683
+ continueCursor: options.endCursor ?? null,
684
+ isDone: true,
685
+ page: docs,
686
+ // eslint-disable-next-line unicorn/no-null -- splitCursor is `null | string`; null marks "too small to split" so the client can read the field unconditionally
687
+ splitCursor: middle ? encodeCursor(middle, orderKeys) : null
688
+ };
689
+ }
690
+ const hasMore = docs.length > numberItems;
691
+ const page = hasMore ? docs.slice(0, numberItems) : docs;
692
+ const last = page.at(-1);
693
+ return {
694
+ // eslint-disable-next-line unicorn/no-null -- QueryPage.continueCursor is `null | string`: null is the documented "no further page" cursor on the wire
695
+ continueCursor: hasMore && last ? encodeCursor(last, orderKeys) : null,
696
+ isDone: !hasMore,
697
+ page
698
+ };
699
+ };
700
+ class NotUniqueError extends Error {
701
+ code = "NOT_UNIQUE";
702
+ status = 400;
703
+ constructor(message = "unique() found more than one matching document") {
704
+ super(message);
705
+ this.name = "NotUniqueError";
706
+ }
707
+ }
708
+ const ID_WHITESPACE_PATTERN = /\s/u;
709
+ const NUL_CHARACTER = String.fromCodePoint(0);
710
+ const normalizeIdStructurally = (schema, tableName, id) => {
711
+ if (!schema.tables[tableName]) {
712
+ throw new Error(`unknown table: ${tableName}`);
713
+ }
714
+ if (typeof id !== "string" || id.length === 0 || ID_WHITESPACE_PATTERN.test(id) || id.includes(NUL_CHARACTER)) {
715
+ return null;
716
+ }
717
+ return id;
718
+ };
719
+ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
720
+ const tableDefinition = schema.tables[tableName];
721
+ if (!tableDefinition) {
722
+ throw new Error(`unknown table: ${tableName}`);
723
+ }
724
+ const stage = {
725
+ indexFields: [],
726
+ indexName: void 0,
727
+ inMemoryFilters: [],
728
+ order: "asc",
729
+ sqlConditions: []
730
+ };
731
+ const runSearchFetch = (limit) => {
732
+ const { search } = stage;
733
+ if (!search) {
734
+ throw new Error("runSearchFetch called without a staged search");
735
+ }
736
+ const filtered = stage.inMemoryFilters.length > 0;
737
+ const engineLimit = filtered ? void 0 : limit;
738
+ const docs = isFtsAvailable(sql$1) ? searchViaFts(sql$1, tableName, search, engineLimit) : searchViaScan(sql$1, tableName, search, engineLimit);
739
+ if (!filtered) {
740
+ return docs;
741
+ }
742
+ const result = [];
743
+ for (const record of docs) {
744
+ if (stage.inMemoryFilters.every((predicate) => predicate(record))) {
745
+ result.push(record);
746
+ if (typeof limit === "number" && result.length >= limit) {
747
+ break;
748
+ }
749
+ }
750
+ }
751
+ return result;
752
+ };
753
+ const buildOrderClause = () => {
754
+ const orderFields = stage.indexFields.length > 0 ? stage.indexFields : ["_creationTime"];
755
+ const orderDirection = stage.order === "desc" ? "DESC" : "ASC";
756
+ return sql.join(
757
+ orderFields.map((field) => sql`${jsonPathSql(field)} ${sql.raw(orderDirection)}`),
758
+ sql`, `
759
+ );
760
+ };
761
+ const runFetch = (limit) => {
762
+ if (stage.search) {
763
+ return runSearchFetch(limit);
764
+ }
765
+ const whereClauses = [];
766
+ for (const condition of stage.sqlConditions) {
767
+ whereClauses.push(sql`${jsonPathSql(condition.field)} ${sql.raw(condition.comparator)} ${serializeSqlValue(condition.value)}`);
768
+ }
769
+ let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
770
+ if (whereClauses.length > 0) {
771
+ query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
772
+ }
773
+ query = sql`${query} ORDER BY ${buildOrderClause()}`;
774
+ if (typeof limit === "number" && stage.inMemoryFilters.length === 0) {
775
+ query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
776
+ }
777
+ const rows = runDrizzle(sql$1, query).toArray();
778
+ const docs = [];
779
+ for (const row of rows) {
780
+ const record = rowToDocument(row);
781
+ if (!record) {
782
+ continue;
783
+ }
784
+ if (stage.inMemoryFilters.every((predicate) => predicate(record))) {
785
+ docs.push(record);
786
+ if (typeof limit === "number" && docs.length >= limit) {
787
+ break;
788
+ }
789
+ }
790
+ }
791
+ return docs;
792
+ };
793
+ const reader = {
794
+ // eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
795
+ async collect() {
796
+ return runFetch(void 0);
797
+ },
798
+ filter(predicate) {
799
+ stage.inMemoryFilters.push(predicate);
800
+ return reader;
801
+ },
802
+ // eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
803
+ async first() {
804
+ const rows = runFetch(stage.inMemoryFilters.length > 0 ? void 0 : 1);
805
+ return rows[0] ?? null;
806
+ },
807
+ order(direction) {
808
+ stage.order = direction === "desc" ? "desc" : "asc";
809
+ return reader;
810
+ },
811
+ // eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
812
+ async paginate(options) {
813
+ if (stage.search) {
814
+ throw new Error("pagination is not supported on search queries; use .take(n) or .collect()");
815
+ }
816
+ return paginateStage(sql$1, tableName, stage, options);
817
+ },
818
+ // eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
819
+ async take(limit) {
820
+ return runFetch(limit);
821
+ },
822
+ // eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
823
+ async unique() {
824
+ const rows = runFetch(stage.inMemoryFilters.length > 0 ? void 0 : 2);
825
+ if (rows.length > 1) {
826
+ throw new NotUniqueError(`unique() on table "${tableName}" matched ${String(rows.length)} documents; expected at most one`);
827
+ }
828
+ return rows[0] ?? null;
829
+ },
830
+ withIndex(indexName, range) {
831
+ const definition = tableDefinition.indexes.find((index) => index.name === indexName);
832
+ if (!definition) {
833
+ throw new Error(`unknown index "${indexName}" on table "${tableName}"`);
834
+ }
835
+ onIndexUse(tableName, indexName, "index");
836
+ stage.indexName = indexName;
837
+ stage.indexFields = definition.fields;
838
+ if (range) {
839
+ range(createRangeBuilder(stage));
840
+ }
841
+ return reader;
842
+ },
843
+ withSearchIndex(indexName, search) {
844
+ const definition = (tableDefinition.searchIndexes ?? []).find((index) => index.name === indexName);
845
+ if (!definition) {
846
+ throw new Error(`unknown search index "${indexName}" on table "${tableName}"`);
847
+ }
848
+ onIndexUse(tableName, indexName, "search");
849
+ const searchStage = {
850
+ definition,
851
+ field: definition.field,
852
+ filters: [],
853
+ hasQuery: false,
854
+ indexName,
855
+ query: ""
856
+ };
857
+ stage.search = searchStage;
858
+ search(createSearchBuilder(searchStage, tableName));
859
+ if (!searchStage.hasQuery) {
860
+ throw new Error(`search index "${indexName}" on table "${tableName}" requires a .search(field, query) call`);
861
+ }
862
+ return reader;
863
+ }
864
+ };
865
+ return reader;
866
+ };
867
+ const applyInsertDefaults = (definition, document, auth) => {
868
+ const result = { ...document };
869
+ for (const [field, column] of tableColumns(definition)) {
870
+ if (column.serverDefault) {
871
+ result[field] = column.serverDefault({ auth });
872
+ continue;
873
+ }
874
+ if (result[field] !== void 0) {
875
+ continue;
876
+ }
877
+ if (column.defaultFn) {
878
+ result[field] = column.defaultFn();
879
+ } else if ("defaultValue" in column) {
880
+ result[field] = column.defaultValue;
881
+ }
882
+ }
883
+ return result;
884
+ };
885
+ const applyOnUpdate = (definition, provided, target, auth) => {
886
+ const out = target;
887
+ for (const [field, column] of tableColumns(definition)) {
888
+ if (column.serverDefault) {
889
+ if (field in provided) {
890
+ out[field] = column.serverDefault({ auth });
891
+ }
892
+ continue;
893
+ }
894
+ if (column.onUpdateFn && !(field in provided)) {
895
+ out[field] = column.onUpdateFn();
896
+ }
897
+ }
898
+ };
899
+ const assertNoExplicitUndefined = (op, document) => {
900
+ for (const field of Object.keys(document)) {
901
+ if (document[field] === void 0) {
902
+ throw new Error(`Cannot ${op} field '${field}' to undefined — use null to clear a nullable field, or omit the key to leave it unchanged.`);
903
+ }
904
+ }
905
+ };
906
+ const UNIQUE_VIOLATION_RE = /unique constraint failed/i;
907
+ const isUniqueViolation = (error) => error instanceof Error && UNIQUE_VIOLATION_RE.test(error.message);
908
+ const runWrite = (sql, table, query) => {
909
+ try {
910
+ runDrizzle(sql, query);
911
+ } catch (error) {
912
+ if (isUniqueViolation(error)) {
913
+ throw new ConflictError(`unique constraint violation on "${table}"`, "unique");
914
+ }
915
+ throw error;
916
+ }
917
+ };
918
+ const runGuardedWrite = (sql$1, table, query) => {
919
+ runWrite(sql$1, table, query);
920
+ const changedRow = runDrizzle(sql$1, sql`SELECT changes() AS changed`).one();
921
+ if (changedRow.changed === 0) {
922
+ throw new ConflictError(`optimistic concurrency conflict on "${table}" — the row changed during this mutation; refetch and retry`, "occ");
923
+ }
924
+ };
925
+ const countRankBefore = (sql$1, rankTable, sortColumns, sortBy, partitionKey, serializedSortValues, rowId) => {
926
+ const beforeBranches = [];
927
+ for (let pivot = 0; pivot < sortColumns.length + 1; pivot += 1) {
928
+ const conditions = [];
929
+ for (let prefix = 0; prefix < pivot; prefix += 1) {
930
+ conditions.push(sql`${sql.identifier(sortColumns[prefix])} IS ${serializedSortValues[prefix]}`);
931
+ }
932
+ const column = sortColumns[pivot];
933
+ const sortKey = sortBy[pivot];
934
+ if (column !== void 0 && sortKey !== void 0) {
935
+ const operator = sortKey.direction === "desc" ? ">" : "<";
936
+ conditions.push(sql`${sql.identifier(column)} ${sql.raw(operator)} ${serializedSortValues[pivot]}`);
937
+ } else {
938
+ conditions.push(sql`${sql.identifier(RANK_TIEBREAK)} < ${rowId}`);
939
+ }
940
+ const [firstCondition] = conditions;
941
+ beforeBranches.push(conditions.length === 1 && firstCondition !== void 0 ? firstCondition : sql`(${sql.join(conditions, sql` AND `)})`);
942
+ }
943
+ const beforeWhere = sql.join(beforeBranches, sql` OR `);
944
+ const beforeRow = runDrizzle(
945
+ sql$1,
946
+ sql`SELECT COUNT(*) AS c FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__partition__")} = ${partitionKey} AND (${beforeWhere})`
947
+ ).one();
948
+ const totalRow = runDrizzle(
949
+ sql$1,
950
+ sql`SELECT COUNT(*) AS c FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__partition__")} = ${partitionKey}`
951
+ ).one();
952
+ return { before: beforeRow.c, total: totalRow.c };
953
+ };
954
+ const createShardCtxDb = (options) => {
955
+ const { sql: sql$1 } = options;
956
+ const { schema } = options;
957
+ const broadcast = options.broadcast ?? (() => void 0);
958
+ const onRead = options.onRead ?? (() => void 0);
959
+ const onIndexUse = options.onIndexUse ?? (() => void 0);
960
+ const onWrite = options.onWrite ?? (() => void 0);
961
+ const { cache } = options;
962
+ const clock = options.clock ?? (() => Date.now());
963
+ const generateId = options.idGenerator ?? (() => crypto.randomUUID());
964
+ const scheduler = options.scheduler ?? throwingScheduler;
965
+ const { globalDb } = options;
966
+ const auth = options.auth ?? { identity: null, userId: null };
967
+ const cdcEnabled = options.cdc ?? false;
968
+ const systemScheduler = scheduler;
969
+ const system = createSystemReader({
970
+ scheduler: typeof systemScheduler.list === "function" && typeof systemScheduler.get === "function" ? systemScheduler : void 0,
971
+ storage: options.storage
972
+ });
973
+ const recordCdc = (table, id, op, doc) => {
974
+ if (cdcEnabled) {
975
+ appendCdcChange(sql$1, clock(), table, id, op, doc);
976
+ }
977
+ };
978
+ const isGlobalTable = (tableName) => schema.tables[tableName]?.shardMode?.kind === "global";
979
+ const routeBackend = (table, op) => {
980
+ if (isGlobalTable(table)) {
981
+ if (!globalDb) {
982
+ throw new Error(`cross-backend ${op} for global table '${table}' requires a globalDb writer — pass one to createShardCtxDb({ globalDb })`);
983
+ }
984
+ return globalDb;
985
+ }
986
+ return writer;
987
+ };
988
+ const routeForHolder = (holderTable) => routeBackend(holderTable, "cascade");
989
+ const globalWriterFor = (tableName, op) => {
990
+ if (!isGlobalTable(tableName)) {
991
+ return void 0;
992
+ }
993
+ if (!globalDb) {
994
+ throw new Error(`${op} on global table '${tableName}' requires a globalDb writer — pass one to createShardCtxDb({ globalDb })`);
995
+ }
996
+ return globalDb;
997
+ };
998
+ const globalFallback = () => globalDb;
999
+ const relationFetcher = (relationTable, relationArgs) => routeBackend(relationTable, "relation load").findMany(relationTable, relationArgs);
1000
+ const relationCounter = (relationTable, relationWhere) => routeBackend(relationTable, "relation load").count(relationTable, relationWhere);
1001
+ const relationPredicateFetcher = (relationTable, relationArgs) => {
1002
+ if (isGlobalTable(relationTable)) {
1003
+ onRead(relationTable, SCAN_DEP);
1004
+ }
1005
+ return relationFetcher(relationTable, relationArgs);
1006
+ };
1007
+ const canPushRelationExists = (relation) => !isGlobalTable(relation.table);
1008
+ const relationExistsPushDown = options.relationExistsPushDown ?? "auto";
1009
+ const relationExistsPushDownEnabled = relationExistsPushDown !== "never";
1010
+ const { maxRelationKeys } = options;
1011
+ const resolveAggregateRelations = (where, predicateTable, relationBaseWhere) => resolveRelationPredicates(where, {
1012
+ fetcher: relationPredicateFetcher,
1013
+ maxRelationKeys,
1014
+ relationBaseWhere,
1015
+ schema,
1016
+ tableName: predicateTable
1017
+ });
1018
+ let triggerDepth = 0;
1019
+ const triggerMatchers = /* @__PURE__ */ new Set();
1020
+ for (const [tableName, definition] of Object.entries(schema.tables)) {
1021
+ for (const trigger of Object.values(definition.triggerMap ?? {})) {
1022
+ triggerMatchers.add(`${tableName} ${trigger.timing} ${trigger.op}`);
1023
+ }
1024
+ }
1025
+ const hasMatchingTrigger = (tableName, timing, op) => triggerMatchers.has(`${tableName} ${timing} ${op}`);
1026
+ const fireTriggers = async (timing, op, event) => {
1027
+ triggerDepth += 1;
1028
+ if (triggerDepth > MAX_TRIGGER_DEPTH) {
1029
+ triggerDepth -= 1;
1030
+ throw new ConflictError(
1031
+ `trigger recursion exceeded ${String(MAX_TRIGGER_DEPTH)} levels on "${event.table}" — check for a self-triggering write`,
1032
+ "trigger"
1033
+ );
1034
+ }
1035
+ try {
1036
+ await runTriggers({ ctx: triggerContext, event, op, schema, tableName: event.table, timing });
1037
+ } finally {
1038
+ triggerDepth -= 1;
1039
+ }
1040
+ };
1041
+ const {
1042
+ ensureBackfilledForTable,
1043
+ ensureBackfilledIndex: ensureBackfilled,
1044
+ ensureRankBackfilled,
1045
+ ensureRankBackfilledForTable,
1046
+ syncAggregates,
1047
+ syncCompanionsForInsert,
1048
+ syncRanks,
1049
+ syncSearch
1050
+ } = createCompanionSync({
1051
+ broadcast,
1052
+ invalidateCache: (table, id) => cache?.invalidate(table, id),
1053
+ recordCdc,
1054
+ schema,
1055
+ sql: sql$1
1056
+ });
1057
+ const assertRankPartitionLocal = (tableName, definition, index) => {
1058
+ const { shardMode } = definition;
1059
+ if (shardMode?.kind !== "shardBy") {
1060
+ return;
1061
+ }
1062
+ if (shardMode.field !== void 0 && (index.partitionBy ?? []).includes(shardMode.field)) {
1063
+ return;
1064
+ }
1065
+ throw Object.assign(
1066
+ new Error(
1067
+ `rank index "${index.name}" on "${tableName}" partitions across shards (shard key "${shardMode.field ?? "?"}" is not in partitionBy) — a shard-local rank()/rankPage() would be wrong; roll it up through the Query Coordinator instead`
1068
+ ),
1069
+ { code: "CROSS_SHARD_RANK_UNSUPPORTED", name: "LunoraError", status: 400 }
1070
+ );
1071
+ };
1072
+ const locateRowById = (id, expectedTable) => {
1073
+ const nonGlobalTables = Object.entries(schema.tables).filter(([, definition]) => definition.shardMode?.kind !== "global").map(([tableName2]) => tableName2).filter((tableName2) => expectedTable === void 0 || tableName2 === expectedTable);
1074
+ if (nonGlobalTables.length === 0) {
1075
+ return void 0;
1076
+ }
1077
+ const branches = nonGlobalTables.map(
1078
+ (tableName2) => (
1079
+ // The table-name discriminator stays an inline literal (escaped) rather than a bound param so it reads as `'<table>' AS __t__`.
1080
+ sql`SELECT ${sql.raw(`'${tableName2.replaceAll("'", "''")}'`)} AS __t__, id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName2)} WHERE id = ${id}`
1081
+ )
1082
+ );
1083
+ const probeQuery = sql`${sql.join(branches, sql` UNION ALL `)} LIMIT 1`;
1084
+ const [firstRow] = runDrizzle(sql$1, probeQuery).toArray();
1085
+ if (!firstRow) {
1086
+ return void 0;
1087
+ }
1088
+ const tableName = firstRow["__t__"];
1089
+ const row = rowToDocument(firstRow);
1090
+ if (typeof tableName !== "string" || !row) {
1091
+ return void 0;
1092
+ }
1093
+ const rawDocument = firstRow[DOC_COLUMN$1];
1094
+ const documentJson = typeof rawDocument === "string" ? rawDocument : JSON.stringify(rawDocument ?? {});
1095
+ return { docJson: documentJson, row, tableName };
1096
+ };
1097
+ const rankPageDeps = {
1098
+ assertRankPartitionLocal,
1099
+ ensureRankBackfilled,
1100
+ onRead,
1101
+ rowToDocument,
1102
+ schema,
1103
+ sql: sql$1
1104
+ };
1105
+ const writer = {
1106
+ // `ctx.db.system` — best-effort read-only system tables. Assigned here so
1107
+ // it rides along on the same `DatabaseWriterLike` the generated ctx hands
1108
+ // to query/mutation/action handlers. See {@link SystemDatabaseReader}.
1109
+ system,
1110
+ async aggregate(tableName, aggOptions) {
1111
+ const global = globalWriterFor(tableName, "aggregate");
1112
+ if (global) {
1113
+ onRead(tableName, SCAN_DEP);
1114
+ return global.aggregate(tableName, aggOptions);
1115
+ }
1116
+ const definition = schema.tables[tableName];
1117
+ if (!definition) {
1118
+ throw new Error(`unknown table: ${tableName}`);
1119
+ }
1120
+ aggregateSqlFunction(aggOptions.op);
1121
+ if (aggOptions.op === "count") {
1122
+ return writer.count(tableName, {
1123
+ baseWhere: aggOptions.baseWhere,
1124
+ relationBaseWhere: aggOptions.relationBaseWhere,
1125
+ restrictsCounts: aggOptions.restrictsCounts,
1126
+ where: aggOptions.where
1127
+ });
1128
+ }
1129
+ if (!aggOptions.field) {
1130
+ throw new Error(`aggregate(${tableName}, { op: "${aggOptions.op}" }): "field" is required for non-count reducers`);
1131
+ }
1132
+ onRead(tableName, SCAN_DEP);
1133
+ const effective = mergeWhere(aggOptions.baseWhere, aggOptions.where);
1134
+ const resolved = await resolveAggregateRelations(effective, tableName, aggOptions.relationBaseWhere);
1135
+ const hasRelation = resolved !== effective;
1136
+ if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation) {
1137
+ const planned = selectIndexForAggregate(definition.aggregateIndexes, aggOptions.op, aggOptions.field, aggOptions.where);
1138
+ if (planned) {
1139
+ ensureBackfilled(tableName, planned.index);
1140
+ const encoded = encodeAggregateKey(planned.index.by ?? [], planned.key);
1141
+ const aggTable = aggregateTableName(tableName, planned.index.name);
1142
+ const indexed = runDrizzle(
1143
+ sql$1,
1144
+ sql`SELECT ${AGG_VALUE} AS value, ${AGG_COUNT} AS count FROM ${sql.identifier(aggTable)} WHERE ${AGG_KEY} = ${encoded}`
1145
+ ).toArray()[0];
1146
+ return readAggregateValue(aggOptions.op, indexed);
1147
+ }
1148
+ }
1149
+ const whereCondition = compileWhereSql(resolved, doWhereSqlStrategy);
1150
+ const aggregateSql = aggregateSqlFunction(aggOptions.op);
1151
+ const ref = jsonPathSql(aggOptions.field);
1152
+ let query = sql`SELECT ${sql.raw(aggregateSql)}(${ref}) AS value FROM ${sql.identifier(tableName)}`;
1153
+ if (whereCondition) {
1154
+ query = sql`${query} WHERE ${whereCondition}`;
1155
+ }
1156
+ const row = runDrizzle(sql$1, query).toArray();
1157
+ const value = row[0]?.value;
1158
+ if (value === null || value === void 0) {
1159
+ return null;
1160
+ }
1161
+ return value;
1162
+ },
1163
+ async count(tableName, whereOrOptions) {
1164
+ const global = globalWriterFor(tableName, "count");
1165
+ if (global) {
1166
+ onRead(tableName, SCAN_DEP);
1167
+ return global.count(tableName, whereOrOptions);
1168
+ }
1169
+ const definition = schema.tables[tableName];
1170
+ if (!definition) {
1171
+ throw new Error(`unknown table: ${tableName}`);
1172
+ }
1173
+ const countOptions = normalizeCountArgument(whereOrOptions);
1174
+ if (countOptions.restrictsCounts) {
1175
+ throw new CountRlsUnsupportedError(tableName);
1176
+ }
1177
+ onRead(tableName, SCAN_DEP);
1178
+ const effective = mergeWhere(countOptions.baseWhere, countOptions.where);
1179
+ const resolved = await resolveAggregateRelations(effective, tableName, countOptions.relationBaseWhere);
1180
+ const hasRelation = resolved !== effective;
1181
+ if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation) {
1182
+ const planned = selectIndexForCount(definition.aggregateIndexes, countOptions.where);
1183
+ if (planned) {
1184
+ ensureBackfilled(tableName, planned.index);
1185
+ const encoded = encodeAggregateKey(planned.index.by ?? [], planned.key);
1186
+ const aggTable = aggregateTableName(tableName, planned.index.name);
1187
+ const row2 = runDrizzle(
1188
+ sql$1,
1189
+ sql`SELECT ${AGG_VALUE} AS value FROM ${sql.identifier(aggTable)} WHERE ${AGG_KEY} = ${encoded}`
1190
+ ).toArray();
1191
+ return row2[0] === void 0 ? 0 : row2[0].value ?? 0;
1192
+ }
1193
+ }
1194
+ const whereCondition = compileWhereSql(resolved, doWhereSqlStrategy);
1195
+ let query = sql`SELECT COUNT(*) AS count FROM ${sql.identifier(tableName)}`;
1196
+ if (whereCondition) {
1197
+ query = sql`${query} WHERE ${whereCondition}`;
1198
+ }
1199
+ const row = runDrizzle(sql$1, query).one();
1200
+ return row.count;
1201
+ },
1202
+ async delete(id, expectedTable) {
1203
+ const located = locateRowById(id, expectedTable);
1204
+ if (!located) {
1205
+ const global = expectedTable === void 0 ? globalFallback() : void 0;
1206
+ if (global) {
1207
+ await global.delete(id);
1208
+ }
1209
+ return;
1210
+ }
1211
+ const { docJson: existingJson, row: existing, tableName } = located;
1212
+ if (hasMatchingTrigger(tableName, "before", "delete")) {
1213
+ await fireTriggers("before", "delete", { id, op: "delete", previous: existing, table: tableName });
1214
+ }
1215
+ await applyOnDelete({
1216
+ deletedId: id,
1217
+ deletedReference: (references) => existing[references],
1218
+ findHolders: async (holderTable, field, value) => {
1219
+ const holders = await routeForHolder(holderTable).findMany(holderTable, { where: { [field]: value } });
1220
+ return holders.page;
1221
+ },
1222
+ onCascade: (holderTable, holderId) => routeForHolder(holderTable).delete(holderId),
1223
+ onRestrict: (message) => {
1224
+ throw new ConflictError(message, "restrict");
1225
+ },
1226
+ // eslint-disable-next-line unicorn/no-null -- `set null` onDelete: the FK column is set to SQL NULL, the documented semantics of the action
1227
+ onSetNull: (holderTable, holderId, field) => routeForHolder(holderTable).patch(holderId, { [field]: null }),
1228
+ schema,
1229
+ tableName
1230
+ });
1231
+ ensureBackfilledForTable(tableName);
1232
+ ensureRankBackfilledForTable(tableName);
1233
+ runGuardedWrite(
1234
+ sql$1,
1235
+ tableName,
1236
+ sql`DELETE FROM ${sql.identifier(tableName)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
1237
+ );
1238
+ syncSearch(tableName, id, void 0);
1239
+ syncAggregates(tableName, existing, void 0);
1240
+ syncRanks(tableName, id, existing, void 0);
1241
+ cache?.invalidate(tableName, id);
1242
+ recordCdc(tableName, id, "delete");
1243
+ broadcast({ key: id, op: "delete", table: tableName });
1244
+ if (hasMatchingTrigger(tableName, "after", "delete")) {
1245
+ await fireTriggers("after", "delete", { id, op: "delete", previous: existing, table: tableName });
1246
+ }
1247
+ await onWrite({ id, op: "delete", table: tableName });
1248
+ },
1249
+ async deleteMany(ids, batchOptions, expectedTable) {
1250
+ assertBatchLimit(ids.length, batchOptions?.limit, "deleteMany");
1251
+ for (const id of ids) {
1252
+ await writer.delete(id, expectedTable);
1253
+ }
1254
+ return { deleted: ids.length };
1255
+ },
1256
+ async findFirst(tableName, args = {}) {
1257
+ const result = await writer.findMany(tableName, { ...args, limit: 1 });
1258
+ return result.page[0] ?? null;
1259
+ },
1260
+ async findFirstOrThrow(tableName, args = {}) {
1261
+ const document = await writer.findFirst(tableName, args);
1262
+ if (document === null) {
1263
+ throw new NotFoundError(`findFirstOrThrow: no "${tableName}" document matched`);
1264
+ }
1265
+ return document;
1266
+ },
1267
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- reader method closed over the writer ctx (sql/schema/onRead/strategy/cache/resolveWith); splitting would thread that shared state through every helper and read worse (see data-migration.ts)
1268
+ async findMany(tableName, args = {}) {
1269
+ const global = globalWriterFor(tableName, "findMany");
1270
+ if (global) {
1271
+ onRead(tableName, SCAN_DEP);
1272
+ return global.findMany(tableName, args);
1273
+ }
1274
+ if (!schema.tables[tableName]) {
1275
+ throw new Error(`unknown table: ${tableName}`);
1276
+ }
1277
+ const isFullScan = !args.where && !args.baseWhere;
1278
+ if (isFullScan) {
1279
+ onRead(tableName, SCAN_DEP);
1280
+ } else {
1281
+ onRead(tableName);
1282
+ }
1283
+ const orderKeys = normalizeOrderKeys(args.orderBy);
1284
+ const seek = args.cursor ? buildSeekWhere(orderKeys, decodeCursor(args.cursor)) : void 0;
1285
+ let predicate = mergeWhere(args.baseWhere, args.where);
1286
+ predicate = await resolveRelationPredicates(predicate, {
1287
+ canPushExists: relationExistsPushDownEnabled ? canPushRelationExists : void 0,
1288
+ existsPushMode: relationExistsPushDown === "always" ? "always" : "auto",
1289
+ fetcher: relationPredicateFetcher,
1290
+ maxRelationKeys,
1291
+ relationBaseWhere: args.relationBaseWhere,
1292
+ schema,
1293
+ tableName
1294
+ });
1295
+ if (seek) {
1296
+ predicate = predicate ? { AND: [predicate, seek] } : seek;
1297
+ }
1298
+ const whereStrategy = relationExistsPushDownEnabled ? makeRelationExistsSqlStrategy(onRead) : doWhereSqlStrategy;
1299
+ const whereCondition = compileWhereSql(predicate, whereStrategy);
1300
+ let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
1301
+ if (whereCondition) {
1302
+ query = sql`${query} WHERE ${whereCondition}`;
1303
+ }
1304
+ query = sql`${query} ORDER BY ${compileOrderBySql(orderKeys)}`;
1305
+ const limit = typeof args.limit === "number" ? Math.max(0, Math.floor(args.limit)) : void 0;
1306
+ if (limit !== void 0) {
1307
+ query = sql`${query} LIMIT ${sql.raw(String(limit + 1))}`;
1308
+ }
1309
+ const rows = runDrizzle(sql$1, query).toArray();
1310
+ const docs = [];
1311
+ for (const row of rows) {
1312
+ const record = rowToDocument(row);
1313
+ if (record) {
1314
+ docs.push(record);
1315
+ if (!isFullScan && typeof record["_id"] === "string") {
1316
+ onRead(tableName, record["_id"]);
1317
+ }
1318
+ }
1319
+ }
1320
+ if (limit === void 0) {
1321
+ if (args.with) {
1322
+ await resolveWith({
1323
+ counter: relationCounter,
1324
+ fetcher: relationFetcher,
1325
+ parents: docs,
1326
+ relationBaseWhere: args.relationBaseWhere,
1327
+ schema,
1328
+ tableName,
1329
+ with: args.with
1330
+ });
1331
+ }
1332
+ return { continueCursor: null, isDone: true, page: docs };
1333
+ }
1334
+ const hasMore = docs.length > limit;
1335
+ const page = hasMore ? docs.slice(0, limit) : docs;
1336
+ const last = page.at(-1);
1337
+ if (args.with) {
1338
+ await resolveWith({
1339
+ counter: relationCounter,
1340
+ fetcher: relationFetcher,
1341
+ parents: page,
1342
+ relationBaseWhere: args.relationBaseWhere,
1343
+ schema,
1344
+ tableName,
1345
+ with: args.with
1346
+ });
1347
+ }
1348
+ return {
1349
+ // eslint-disable-next-line unicorn/no-null -- QueryPage.continueCursor is `null | string`: null is the documented "no further page" cursor on the wire
1350
+ continueCursor: hasMore && last ? encodeCursor(last, orderKeys) : null,
1351
+ isDone: !hasMore,
1352
+ page
1353
+ };
1354
+ },
1355
+ async get(id, expectedTable) {
1356
+ const located = locateRowById(id, expectedTable);
1357
+ if (!located) {
1358
+ const global = expectedTable === void 0 ? globalFallback() : void 0;
1359
+ if (global) {
1360
+ return global.get(id);
1361
+ }
1362
+ return null;
1363
+ }
1364
+ onRead(located.tableName, id);
1365
+ return located.row;
1366
+ },
1367
+ // eslint-disable-next-line @typescript-eslint/require-await -- async to satisfy the optional seam's Promise contract; the underlying lookup is sync
1368
+ async lookupById(id, expectedTable) {
1369
+ const located = locateRowById(id, expectedTable);
1370
+ if (!located) {
1371
+ return null;
1372
+ }
1373
+ onRead(located.tableName, id);
1374
+ return { row: located.row, tableName: located.tableName };
1375
+ },
1376
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- the indexed/scan branching is closed over the writer ctx and reads worse when split
1377
+ async groupBy(tableName, groupOptions) {
1378
+ const global = globalWriterFor(tableName, "groupBy");
1379
+ if (global) {
1380
+ onRead(tableName, SCAN_DEP);
1381
+ return global.groupBy(tableName, groupOptions);
1382
+ }
1383
+ const definition = schema.tables[tableName];
1384
+ if (!definition) {
1385
+ throw new Error(`unknown table: ${tableName}`);
1386
+ }
1387
+ onRead(tableName, SCAN_DEP);
1388
+ const agg = groupOptions.agg ?? { op: "count" };
1389
+ aggregateSqlFunction(agg.op);
1390
+ if (agg.op !== "count" && !agg.field) {
1391
+ throw new Error(`groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
1392
+ }
1393
+ const effective = mergeWhere(groupOptions.baseWhere, groupOptions.where);
1394
+ const resolved = await resolveAggregateRelations(effective, tableName, groupOptions.relationBaseWhere);
1395
+ const hasRelation = resolved !== effective;
1396
+ if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation) {
1397
+ const planned = selectIndexForGroupBy(definition.aggregateIndexes, agg.op, agg.field, groupOptions.by, groupOptions.where);
1398
+ if (planned) {
1399
+ ensureBackfilled(tableName, planned.index);
1400
+ const aggTable = aggregateTableName(tableName, planned.index.name);
1401
+ const partialKeys = Object.keys(planned.partial);
1402
+ const indexedResult = [];
1403
+ if (partialKeys.length === (planned.index.by ?? []).length && partialKeys.length > 0) {
1404
+ const encoded = encodeAggregateKey(planned.index.by ?? [], planned.partial);
1405
+ const rowsIndexed2 = runDrizzle(
1406
+ sql$1,
1407
+ sql`SELECT ${AGG_VALUE} AS value, ${AGG_COUNT} AS count FROM ${sql.identifier(aggTable)} WHERE ${AGG_KEY} = ${encoded}`
1408
+ ).toArray();
1409
+ if (rowsIndexed2.length > 0) {
1410
+ indexedResult.push({
1411
+ key: { ...planned.partial },
1412
+ value: readAggregateValue(agg.op, rowsIndexed2[0])
1413
+ });
1414
+ }
1415
+ return indexedResult;
1416
+ }
1417
+ const rowsIndexed = runDrizzle(
1418
+ sql$1,
1419
+ sql`SELECT ${AGG_KEY} AS key, ${AGG_VALUE} AS value, ${AGG_COUNT} AS count FROM ${sql.identifier(aggTable)}`
1420
+ ).toArray();
1421
+ for (const row of rowsIndexed) {
1422
+ const decoded = JSON.parse(row.key);
1423
+ indexedResult.push({ key: decoded, value: readAggregateValue(agg.op, row) });
1424
+ }
1425
+ return indexedResult;
1426
+ }
1427
+ }
1428
+ const whereCondition = compileWhereSql(resolved, doWhereSqlStrategy);
1429
+ const select = groupOptions.by.map((field) => sql`${jsonPathSql(field)} AS ${sql.identifier(field)}`);
1430
+ if (agg.op === "count") {
1431
+ select.push(sql`COUNT(*) AS value`);
1432
+ } else {
1433
+ const { field } = agg;
1434
+ if (field === void 0) {
1435
+ throw new Error(`groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
1436
+ }
1437
+ select.push(sql`${sql.raw(aggregateSqlFunction(agg.op))}(${jsonPathSql(field)}) AS value`);
1438
+ }
1439
+ let query = sql`SELECT ${sql.join(select, sql`, `)} FROM ${sql.identifier(tableName)}`;
1440
+ if (whereCondition) {
1441
+ query = sql`${query} WHERE ${whereCondition}`;
1442
+ }
1443
+ query = sql`${query} GROUP BY ${sql.join(
1444
+ groupOptions.by.map((field) => jsonPathSql(field)),
1445
+ sql`, `
1446
+ )}`;
1447
+ const rows = runDrizzle(sql$1, query).toArray();
1448
+ const result = [];
1449
+ for (const row of rows) {
1450
+ const key = {};
1451
+ for (const field of groupOptions.by) {
1452
+ key[field] = row[field] ?? null;
1453
+ }
1454
+ const { value } = row;
1455
+ result.push({ key, value: value === null || value === void 0 ? null : Number(value) });
1456
+ }
1457
+ return result;
1458
+ },
1459
+ async insert(tableName, document, insertOptions) {
1460
+ const global = globalWriterFor(tableName, "insert");
1461
+ if (global) {
1462
+ const id2 = await global.insert(tableName, document, insertOptions);
1463
+ broadcast({ key: id2, op: "insert", row: { ...document, _id: id2 }, table: tableName });
1464
+ return id2;
1465
+ }
1466
+ const definition = schema.tables[tableName];
1467
+ if (!definition) {
1468
+ throw new Error(`unknown table: ${tableName}`);
1469
+ }
1470
+ const withDefaults = applyInsertDefaults(definition, document, auth);
1471
+ runRowValidators(definition, withDefaults);
1472
+ let id;
1473
+ if (insertOptions?.clientId !== void 0) {
1474
+ assertValidClientId(insertOptions.clientId);
1475
+ id = insertOptions.clientId;
1476
+ } else if (insertOptions?.allowExplicitId && typeof withDefaults["_id"] === "string") {
1477
+ id = withDefaults["_id"];
1478
+ } else {
1479
+ id = generateId();
1480
+ }
1481
+ const creationTime = typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
1482
+ const documentWithMeta = { ...withDefaults, _creationTime: creationTime, _id: id };
1483
+ if (hasMatchingTrigger(tableName, "before", "insert")) {
1484
+ await fireTriggers("before", "insert", { doc: { ...documentWithMeta }, id, op: "insert", table: tableName });
1485
+ }
1486
+ ensureBackfilledForTable(tableName);
1487
+ ensureRankBackfilledForTable(tableName);
1488
+ runWrite(
1489
+ sql$1,
1490
+ tableName,
1491
+ sql`INSERT INTO ${sql.identifier(tableName)} (id, _creationTime, ${sql.identifier(DOC_COLUMN$1)}) VALUES (${id}, ${creationTime}, ${JSON.stringify(documentWithMeta)})`
1492
+ );
1493
+ syncCompanionsForInsert(tableName, id, documentWithMeta);
1494
+ if (hasMatchingTrigger(tableName, "after", "insert")) {
1495
+ await fireTriggers("after", "insert", { doc: documentWithMeta, id, op: "insert", table: tableName });
1496
+ }
1497
+ await onWrite({ doc: documentWithMeta, id, op: "insert", table: tableName });
1498
+ return id;
1499
+ },
1500
+ async insertManyUnsafe(tableName, documents, batchOptions) {
1501
+ assertBatchLimit(documents.length, batchOptions?.limit, "insertManyUnsafe");
1502
+ if (documents.length === 0) {
1503
+ return [];
1504
+ }
1505
+ const global = globalWriterFor(tableName, "insert");
1506
+ if (global) {
1507
+ const globalIds = [];
1508
+ for (const document of documents) {
1509
+ const globalId = await global.insert(tableName, document, { allowExplicitId: batchOptions?.allowExplicitId });
1510
+ broadcast({ key: globalId, op: "insert", row: { ...document, _id: globalId }, table: tableName });
1511
+ globalIds.push(globalId);
1512
+ }
1513
+ return globalIds;
1514
+ }
1515
+ const definition = schema.tables[tableName];
1516
+ if (!definition) {
1517
+ throw new Error(`unknown table: ${tableName}`);
1518
+ }
1519
+ ensureBackfilledForTable(tableName);
1520
+ ensureRankBackfilledForTable(tableName);
1521
+ const rows = documents.map((document) => {
1522
+ const withDefaults = applyInsertDefaults(definition, document, auth);
1523
+ const id = batchOptions?.allowExplicitId === true && typeof withDefaults["_id"] === "string" ? withDefaults["_id"] : generateId();
1524
+ const creationTime = typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
1525
+ return { creationTime, document: { ...withDefaults, _creationTime: creationTime, _id: id }, id };
1526
+ });
1527
+ const valuesSql = sql.join(
1528
+ rows.map((row) => sql`(${row.id}, ${row.creationTime}, ${JSON.stringify(row.document)})`),
1529
+ sql`, `
1530
+ );
1531
+ runWrite(sql$1, tableName, sql`INSERT INTO ${sql.identifier(tableName)} (id, _creationTime, ${sql.identifier(DOC_COLUMN$1)}) VALUES ${valuesSql}`);
1532
+ for (const { document, id } of rows) {
1533
+ syncCompanionsForInsert(tableName, id, document);
1534
+ await onWrite({ doc: document, id, op: "insert", table: tableName });
1535
+ }
1536
+ return rows.map((row) => row.id);
1537
+ },
1538
+ async insertMany(tableName, documents, batchOptions) {
1539
+ assertBatchLimit(documents.length, batchOptions?.limit, "insertMany");
1540
+ const ids = [];
1541
+ for (const document of documents) {
1542
+ ids.push(await writer.insert(tableName, document));
1543
+ }
1544
+ return ids;
1545
+ },
1546
+ normalizeId(tableName, id) {
1547
+ return normalizeIdStructurally(schema, tableName, id);
1548
+ },
1549
+ async patch(id, patch, expectedTable) {
1550
+ const located = locateRowById(id, expectedTable);
1551
+ if (!located) {
1552
+ const global = expectedTable === void 0 ? globalFallback() : void 0;
1553
+ if (global) {
1554
+ await global.patch(id, patch);
1555
+ return;
1556
+ }
1557
+ throw new Error(`document not found: ${id}`);
1558
+ }
1559
+ const { docJson: existingJson, row: existing, tableName } = located;
1560
+ const tableDefinition = schema.tables[tableName];
1561
+ if (!tableDefinition) {
1562
+ throw new Error(`unknown table: ${tableName}`);
1563
+ }
1564
+ onRead(tableName, id);
1565
+ assertNoExplicitUndefined("patch", patch);
1566
+ const merged = { ...existing, ...patch, _id: id };
1567
+ applyOnUpdate(tableDefinition, patch, merged, auth);
1568
+ runRowValidators(tableDefinition, merged);
1569
+ if (hasMatchingTrigger(tableName, "before", "update")) {
1570
+ await fireTriggers("before", "update", { doc: { ...merged }, id, op: "update", previous: existing, table: tableName });
1571
+ }
1572
+ ensureBackfilledForTable(tableName);
1573
+ ensureRankBackfilledForTable(tableName);
1574
+ runGuardedWrite(
1575
+ sql$1,
1576
+ tableName,
1577
+ sql`UPDATE ${sql.identifier(tableName)} SET ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(merged)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
1578
+ );
1579
+ syncSearch(tableName, id, merged);
1580
+ syncAggregates(tableName, existing, merged);
1581
+ syncRanks(tableName, id, existing, merged);
1582
+ cache?.invalidate(tableName, id);
1583
+ recordCdc(tableName, id, "update", merged);
1584
+ broadcast({ key: id, op: "update", row: merged, table: tableName });
1585
+ if (hasMatchingTrigger(tableName, "after", "update")) {
1586
+ await fireTriggers("after", "update", { doc: merged, id, op: "update", previous: existing, table: tableName });
1587
+ }
1588
+ await onWrite({ doc: merged, id, op: "update", table: tableName });
1589
+ },
1590
+ async patchMany(patches, batchOptions, expectedTable) {
1591
+ assertBatchLimit(patches.length, batchOptions?.limit, "patchMany");
1592
+ for (const entry of patches) {
1593
+ await writer.patch(entry.id, entry.patch, expectedTable);
1594
+ }
1595
+ },
1596
+ query(tableName) {
1597
+ const global = globalWriterFor(tableName, "query");
1598
+ if (global) {
1599
+ onRead(tableName, SCAN_DEP);
1600
+ return global.query(tableName);
1601
+ }
1602
+ onRead(tableName, SCAN_DEP);
1603
+ return buildReader(sql$1, schema, tableName, onIndexUse);
1604
+ },
1605
+ async rank(tableName, indexName, rankOptions) {
1606
+ const global = globalWriterFor(tableName, "rank");
1607
+ if (global) {
1608
+ onRead(tableName, SCAN_DEP);
1609
+ return global.rank(tableName, indexName, rankOptions);
1610
+ }
1611
+ onIndexUse(tableName, indexName, "rank");
1612
+ const definition = schema.tables[tableName];
1613
+ if (!definition) {
1614
+ throw new Error(`unknown table: ${tableName}`);
1615
+ }
1616
+ const index = definition.rankIndexes?.find((i) => i.name === indexName);
1617
+ if (!index) {
1618
+ throw new Error(`unknown rankIndex "${indexName}" on table "${tableName}"`);
1619
+ }
1620
+ assertRankPartitionLocal(tableName, definition, index);
1621
+ if (rankOptions.restrictsCounts) {
1622
+ throw new CountRlsUnsupportedError(tableName);
1623
+ }
1624
+ onRead(tableName, SCAN_DEP);
1625
+ ensureRankBackfilled(tableName, index);
1626
+ const rowId = typeof rankOptions.row === "string" ? rankOptions.row : rankOptions.row["_id"];
1627
+ if (!rowId) {
1628
+ return null;
1629
+ }
1630
+ const rankTable = rankTableName(tableName, index.name);
1631
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
1632
+ const sortColumnList = sortColumns.map((column) => quoteIdentifier(column)).join(", ");
1633
+ const ownRows = runDrizzle(
1634
+ sql$1,
1635
+ sql`SELECT ${sql.identifier("__partition__")}, ${sql.raw(sortColumnList)} FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__id__")} = ${rowId}`
1636
+ ).toArray();
1637
+ const [own] = ownRows;
1638
+ if (own === void 0) {
1639
+ return null;
1640
+ }
1641
+ let partitionKey = own["__partition__"];
1642
+ const effective = mergeWhere(rankOptions.baseWhere, rankOptions.where);
1643
+ assertFlatPredicate(effective, schema, tableName, "rank");
1644
+ const partitionFromWhere = resolveRankPartition(index, effective);
1645
+ if (partitionFromWhere) {
1646
+ const requestedKey = encodePartitionKey(index.partitionBy ?? [], partitionFromWhere);
1647
+ if (requestedKey !== partitionKey) {
1648
+ return null;
1649
+ }
1650
+ partitionKey = requestedKey;
1651
+ }
1652
+ const ownSortValues = sortColumns.map((column) => own[column]);
1653
+ const { before, total } = countRankBefore(sql$1, rankTable, sortColumns, index.sortBy, partitionKey, ownSortValues, rowId);
1654
+ return { position: before + 1, total };
1655
+ },
1656
+ // eslint-disable-next-line @typescript-eslint/require-await -- DatabaseWriterLike returns Promises (the D1 twin awaits I/O); the body is synchronous SQLite
1657
+ async rankBefore(tableName, indexName, rankBeforeOptions) {
1658
+ if (isGlobalTable(tableName)) {
1659
+ throw new Error(
1660
+ `rankBefore is not supported on the global (.global()) table '${tableName}' — cross-shard rank cursors apply only to sharded tables`
1661
+ );
1662
+ }
1663
+ const definition = schema.tables[tableName];
1664
+ if (!definition) {
1665
+ throw new Error(`unknown table: ${tableName}`);
1666
+ }
1667
+ const index = definition.rankIndexes?.find((i) => i.name === indexName);
1668
+ if (!index) {
1669
+ throw new Error(`unknown rankIndex "${indexName}" on table "${tableName}"`);
1670
+ }
1671
+ if (rankBeforeOptions.restrictsCounts) {
1672
+ throw new CountRlsUnsupportedError(tableName);
1673
+ }
1674
+ onRead(tableName, SCAN_DEP);
1675
+ ensureRankBackfilled(tableName, index);
1676
+ const rankTable = rankTableName(tableName, index.name);
1677
+ const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
1678
+ const serialized = index.sortBy.map((_, i) => serializeSqlValue(rankBeforeOptions.sortValues[i] ?? null));
1679
+ return countRankBefore(sql$1, rankTable, sortColumns, index.sortBy, rankBeforeOptions.partitionKey, serialized, rankBeforeOptions.rowId);
1680
+ },
1681
+ async rankPage(tableName, indexName, rankPageOptions = {}) {
1682
+ assertFlatPredicate(mergeWhere(rankPageOptions.baseWhere, rankPageOptions.where), schema, tableName, "rankPage");
1683
+ const global = globalWriterFor(tableName, "rankPage");
1684
+ if (global) {
1685
+ onRead(tableName, SCAN_DEP);
1686
+ return global.rankPage(tableName, indexName, rankPageOptions);
1687
+ }
1688
+ onIndexUse(tableName, indexName, "rank");
1689
+ const { continueCursor, hasMore, rows } = computeRankPage(rankPageDeps, tableName, indexName, rankPageOptions);
1690
+ return { continueCursor, isDone: !hasMore, page: rows.map((row) => row.doc) };
1691
+ },
1692
+ // eslint-disable-next-line @typescript-eslint/require-await -- DatabaseWriterLike returns Promises (the D1 twin awaits I/O); the body is synchronous SQLite
1693
+ async rankPageRows(tableName, indexName, rankPageOptions = {}) {
1694
+ assertFlatPredicate(mergeWhere(rankPageOptions.baseWhere, rankPageOptions.where), schema, tableName, "rankPage");
1695
+ onIndexUse(tableName, indexName, "rank");
1696
+ const { directions, hasMore, rows } = computeRankPage(rankPageDeps, tableName, indexName, rankPageOptions);
1697
+ return { directions, hasMore, rows };
1698
+ },
1699
+ async replace(id, document, expectedTable) {
1700
+ const located = locateRowById(id, expectedTable);
1701
+ if (!located) {
1702
+ const global = expectedTable === void 0 ? globalFallback() : void 0;
1703
+ if (global) {
1704
+ await global.replace(id, document);
1705
+ return;
1706
+ }
1707
+ throw new Error(`document not found: ${id}`);
1708
+ }
1709
+ const { docJson: existingJson, row: previous, tableName } = located;
1710
+ const tableDefinition = schema.tables[tableName];
1711
+ if (!tableDefinition) {
1712
+ throw new Error(`unknown table: ${tableName}`);
1713
+ }
1714
+ assertNoExplicitUndefined("replace", document);
1715
+ const creationTime = typeof document["_creationTime"] === "number" ? document["_creationTime"] : clock();
1716
+ const replaced = { ...document, _creationTime: creationTime, _id: id };
1717
+ applyOnUpdate(tableDefinition, document, replaced, auth);
1718
+ runRowValidators(tableDefinition, replaced);
1719
+ if (hasMatchingTrigger(tableName, "before", "update")) {
1720
+ await fireTriggers("before", "update", { doc: { ...replaced }, id, op: "update", previous, table: tableName });
1721
+ }
1722
+ ensureBackfilledForTable(tableName);
1723
+ ensureRankBackfilledForTable(tableName);
1724
+ runGuardedWrite(
1725
+ sql$1,
1726
+ tableName,
1727
+ sql`UPDATE ${sql.identifier(tableName)} SET _creationTime = ${creationTime}, ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(replaced)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
1728
+ );
1729
+ syncSearch(tableName, id, replaced);
1730
+ syncAggregates(tableName, previous, replaced);
1731
+ syncRanks(tableName, id, previous, replaced);
1732
+ cache?.invalidate(tableName, id);
1733
+ recordCdc(tableName, id, "update", replaced);
1734
+ broadcast({ key: id, op: "update", row: replaced, table: tableName });
1735
+ if (hasMatchingTrigger(tableName, "after", "update")) {
1736
+ await fireTriggers("after", "update", { doc: replaced, id, op: "update", previous, table: tableName });
1737
+ }
1738
+ await onWrite({ doc: replaced, id, op: "update", table: tableName });
1739
+ }
1740
+ };
1741
+ const triggerContext = { db: writer, scheduler };
1742
+ return options.enforceRls === true ? guardWriter(writer, schema, (id, expectedTable) => locateRowById(id, expectedTable)?.tableName) : writer;
1743
+ };
1744
+
1745
+ export { NotUniqueError, appendCdcChange, assertValidClientId, createShardCtxDb, normalizeIdStructurally };