@typicalday/firegraph 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +23 -3
  2. package/dist/{backend-DuvHGgK1.d.cts → backend-BpYLdwCW.d.cts} +1 -1
  3. package/dist/{backend-DuvHGgK1.d.ts → backend-BpYLdwCW.d.ts} +1 -1
  4. package/dist/backend-CvImIwTY.d.cts +137 -0
  5. package/dist/backend-YH5HtawN.d.ts +137 -0
  6. package/dist/backend.d.cts +2 -2
  7. package/dist/backend.d.ts +2 -2
  8. package/dist/{chunk-3AHHXMWX.js → chunk-5HIRYV2S.js} +12 -35
  9. package/dist/chunk-5HIRYV2S.js.map +1 -0
  10. package/dist/{chunk-DJI3VXXA.js → chunk-7IEZ6IYY.js} +2 -2
  11. package/dist/chunk-7IEZ6IYY.js.map +1 -0
  12. package/dist/chunk-FODIMIWY.js +721 -0
  13. package/dist/chunk-FODIMIWY.js.map +1 -0
  14. package/dist/chunk-NGAJCALM.js +34 -0
  15. package/dist/chunk-NGAJCALM.js.map +1 -0
  16. package/dist/chunk-ULRDQ6HZ.js +862 -0
  17. package/dist/chunk-ULRDQ6HZ.js.map +1 -0
  18. package/dist/{client-BKi3vk0Q.d.ts → client-B5o39X79.d.ts} +1 -1
  19. package/dist/{client-BrsaXtDV.d.cts → client-BGHwxwPg.d.cts} +1 -1
  20. package/dist/{client-Bk2Cm6xv.d.cts → client-DoyEdJ5w.d.cts} +1 -1
  21. package/dist/{client-Bk2Cm6xv.d.ts → client-DoyEdJ5w.d.ts} +1 -1
  22. package/dist/cloudflare/index.cjs +148 -158
  23. package/dist/cloudflare/index.cjs.map +1 -1
  24. package/dist/cloudflare/index.d.cts +73 -70
  25. package/dist/cloudflare/index.d.ts +73 -70
  26. package/dist/cloudflare/index.js +53 -588
  27. package/dist/cloudflare/index.js.map +1 -1
  28. package/dist/codegen/index.d.cts +1 -1
  29. package/dist/codegen/index.d.ts +1 -1
  30. package/dist/firestore-enterprise/index.cjs.map +1 -1
  31. package/dist/firestore-enterprise/index.d.cts +3 -3
  32. package/dist/firestore-enterprise/index.d.ts +3 -3
  33. package/dist/firestore-enterprise/index.js +5 -3
  34. package/dist/firestore-enterprise/index.js.map +1 -1
  35. package/dist/firestore-standard/index.cjs.map +1 -1
  36. package/dist/firestore-standard/index.d.cts +3 -3
  37. package/dist/firestore-standard/index.d.ts +3 -3
  38. package/dist/firestore-standard/index.js +3 -2
  39. package/dist/firestore-standard/index.js.map +1 -1
  40. package/dist/index.d.cts +5 -5
  41. package/dist/index.d.ts +5 -5
  42. package/dist/index.js +6 -4
  43. package/dist/index.js.map +1 -1
  44. package/dist/query-client/index.d.cts +2 -2
  45. package/dist/query-client/index.d.ts +2 -2
  46. package/dist/{registry-Bc7h6WTM.d.cts → registry-BGh7Jqpb.d.cts} +2 -2
  47. package/dist/{registry-C2KUPVZj.d.ts → registry-tKTb5Kx1.d.ts} +2 -2
  48. package/dist/sqlite/index.cjs +578 -371
  49. package/dist/sqlite/index.cjs.map +1 -1
  50. package/dist/sqlite/index.d.cts +4 -110
  51. package/dist/sqlite/index.d.ts +4 -110
  52. package/dist/sqlite/index.js +7 -1144
  53. package/dist/sqlite/index.js.map +1 -1
  54. package/dist/sqlite/local.cjs +1835 -0
  55. package/dist/sqlite/local.cjs.map +1 -0
  56. package/dist/sqlite/local.d.cts +83 -0
  57. package/dist/sqlite/local.d.ts +83 -0
  58. package/dist/sqlite/local.js +121 -0
  59. package/dist/sqlite/local.js.map +1 -0
  60. package/package.json +15 -1
  61. package/dist/chunk-3AHHXMWX.js.map +0 -1
  62. package/dist/chunk-DJI3VXXA.js.map +0 -1
  63. package/dist/chunk-NNBSUOOF.js +0 -289
  64. package/dist/chunk-NNBSUOOF.js.map +0 -1
@@ -0,0 +1,721 @@
1
+ import {
2
+ buildSchemaStatements,
3
+ compileAggregate,
4
+ compileBulkDelete,
5
+ compileBulkUpdate,
6
+ compileDelete,
7
+ compileExpand,
8
+ compileExpandHydrate,
9
+ compileFindEdgesProjected,
10
+ compileSelect,
11
+ compileSelectByDocId,
12
+ compileSet,
13
+ compileUpdate,
14
+ decodeProjectedRow,
15
+ quoteIdent,
16
+ rowToRecord,
17
+ validateTableName
18
+ } from "./chunk-ULRDQ6HZ.js";
19
+ import {
20
+ createCapabilities
21
+ } from "./chunk-N5HFDWQX.js";
22
+ import {
23
+ NODE_RELATION,
24
+ computeEdgeDocId,
25
+ computeNodeDocId
26
+ } from "./chunk-NGAJCALM.js";
27
+ import {
28
+ FiregraphError
29
+ } from "./chunk-SIHE4UY4.js";
30
+
31
+ // src/sqlite/catalog.ts
32
+ function catalogTableName(rootTable) {
33
+ validateTableName(rootTable);
34
+ return `${rootTable}_graphs`;
35
+ }
36
+ function mangleStorageScope(scope) {
37
+ let out = "";
38
+ for (const ch of scope) {
39
+ if (/[A-Za-z0-9]/.test(ch)) out += ch;
40
+ else if (ch === "_") out += "__";
41
+ else if (ch === "-") out += "_h";
42
+ else if (ch === "/") out += "_s";
43
+ else out += `_u${ch.codePointAt(0).toString(16)}_`;
44
+ }
45
+ return out;
46
+ }
47
+ function tableForScope(rootTable, storageScope) {
48
+ validateTableName(rootTable);
49
+ if (storageScope === "") return rootTable;
50
+ return `${rootTable}_g_${mangleStorageScope(storageScope)}`;
51
+ }
52
+ function escapeLikePrefix(prefix) {
53
+ return prefix.replace(/[\\%_]/g, (c) => `\\${c}`);
54
+ }
55
+ function buildCatalogDDL(rootTable) {
56
+ const t = quoteIdent(catalogTableName(rootTable));
57
+ return `CREATE TABLE IF NOT EXISTS ${t} (
58
+ storage_scope TEXT NOT NULL PRIMARY KEY,
59
+ table_name TEXT NOT NULL UNIQUE,
60
+ scope_path TEXT NOT NULL
61
+ )`;
62
+ }
63
+ function compileCatalogRegister(rootTable, storageScope, tableName, scopePath) {
64
+ const t = quoteIdent(catalogTableName(rootTable));
65
+ return {
66
+ sql: `INSERT OR IGNORE INTO ${t} (storage_scope, table_name, scope_path) VALUES (?, ?, ?)`,
67
+ params: [storageScope, tableName, scopePath]
68
+ };
69
+ }
70
+ function compileCatalogDescendants(rootTable, scopePrefix) {
71
+ const t = quoteIdent(catalogTableName(rootTable));
72
+ return {
73
+ sql: `SELECT storage_scope, table_name FROM ${t} WHERE storage_scope LIKE ? ESCAPE '\\' ORDER BY storage_scope`,
74
+ params: [`${escapeLikePrefix(scopePrefix)}/%`]
75
+ };
76
+ }
77
+ function compileCatalogDelete(rootTable, storageScope) {
78
+ const t = quoteIdent(catalogTableName(rootTable));
79
+ return {
80
+ sql: `DELETE FROM ${t} WHERE storage_scope = ?`,
81
+ params: [storageScope]
82
+ };
83
+ }
84
+
85
+ // src/sqlite/backend.ts
86
+ var DEFAULT_MAX_RETRIES = 3;
87
+ var BASE_RETRY_DELAY_MS = 200;
88
+ var MAX_RETRY_DELAY_MS = 5e3;
89
+ function sleep(ms) {
90
+ return new Promise((resolve) => setTimeout(resolve, ms));
91
+ }
92
+ function minDefined(a, b) {
93
+ if (a === void 0) return b;
94
+ if (b === void 0) return a;
95
+ return Math.min(a, b);
96
+ }
97
+ function chunkStatements(statements, maxStatements, maxParams) {
98
+ const stmtCap = maxStatements && maxStatements > 0 && Number.isFinite(maxStatements) ? Math.floor(maxStatements) : Infinity;
99
+ const paramCap = maxParams && maxParams > 0 && Number.isFinite(maxParams) ? Math.floor(maxParams) : Infinity;
100
+ if (stmtCap === Infinity && paramCap === Infinity) {
101
+ return [statements];
102
+ }
103
+ const chunks = [];
104
+ let current = [];
105
+ let currentParamCount = 0;
106
+ for (const stmt of statements) {
107
+ const stmtParams = stmt.params.length;
108
+ const wouldExceedStmt = current.length + 1 > stmtCap;
109
+ const wouldExceedParam = currentParamCount + stmtParams > paramCap;
110
+ if (current.length > 0 && (wouldExceedStmt || wouldExceedParam)) {
111
+ chunks.push(current);
112
+ current = [];
113
+ currentParamCount = 0;
114
+ }
115
+ current.push(stmt);
116
+ currentParamCount += stmtParams;
117
+ }
118
+ if (current.length > 0) chunks.push(current);
119
+ return chunks;
120
+ }
121
+ var SqliteTransactionBackendImpl = class {
122
+ constructor(tx, tableName) {
123
+ this.tx = tx;
124
+ this.tableName = tableName;
125
+ }
126
+ async getDoc(docId) {
127
+ const stmt = compileSelectByDocId(this.tableName, docId);
128
+ const rows = await this.tx.all(stmt.sql, stmt.params);
129
+ return rows.length === 0 ? null : rowToRecord(rows[0]);
130
+ }
131
+ async query(filters, options) {
132
+ const stmt = compileSelect(this.tableName, filters, options);
133
+ const rows = await this.tx.all(stmt.sql, stmt.params);
134
+ return rows.map(rowToRecord);
135
+ }
136
+ async setDoc(docId, record, mode) {
137
+ const stmt = compileSet(this.tableName, docId, record, Date.now(), mode);
138
+ await this.tx.run(stmt.sql, stmt.params);
139
+ }
140
+ async updateDoc(docId, update) {
141
+ const stmt = compileUpdate(this.tableName, docId, update, Date.now());
142
+ const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
143
+ const rows = await this.tx.all(sqlWithReturning, stmt.params);
144
+ if (rows.length === 0) {
145
+ throw new FiregraphError(
146
+ `updateDoc: no document found for doc_id=${docId} (table=${this.tableName})`,
147
+ "NOT_FOUND"
148
+ );
149
+ }
150
+ }
151
+ async deleteDoc(docId) {
152
+ const stmt = compileDelete(this.tableName, docId);
153
+ await this.tx.run(stmt.sql, stmt.params);
154
+ }
155
+ };
156
+ var SqliteBatchBackendImpl = class {
157
+ constructor(executor, tableName, ensureSchema) {
158
+ this.executor = executor;
159
+ this.tableName = tableName;
160
+ this.ensureSchema = ensureSchema;
161
+ }
162
+ statements = [];
163
+ setDoc(docId, record, mode) {
164
+ this.statements.push(compileSet(this.tableName, docId, record, Date.now(), mode));
165
+ }
166
+ updateDoc(docId, update) {
167
+ this.statements.push(compileUpdate(this.tableName, docId, update, Date.now()));
168
+ }
169
+ deleteDoc(docId) {
170
+ this.statements.push(compileDelete(this.tableName, docId));
171
+ }
172
+ async commit() {
173
+ if (this.statements.length === 0) return;
174
+ await this.ensureSchema();
175
+ await this.executor.batch(this.statements);
176
+ this.statements.length = 0;
177
+ }
178
+ };
179
+ var SQLITE_CORE_CAPS = [
180
+ "core.read",
181
+ "core.write",
182
+ "core.batch",
183
+ "core.subgraph",
184
+ "query.aggregate",
185
+ "query.dml",
186
+ "query.join",
187
+ "query.select",
188
+ "raw.sql"
189
+ ];
190
+ var SqliteBackendImpl = class _SqliteBackendImpl {
191
+ constructor(executor, rootTable, storageScope, scopePath, registry, coreIndexes) {
192
+ this.executor = executor;
193
+ validateTableName(rootTable);
194
+ this.rootTable = rootTable;
195
+ this.collectionPath = tableForScope(rootTable, storageScope);
196
+ this.storageScope = storageScope;
197
+ this.scopePath = scopePath;
198
+ this.registry = registry;
199
+ this.coreIndexes = coreIndexes;
200
+ const caps = new Set(SQLITE_CORE_CAPS);
201
+ if (typeof executor.transaction === "function") {
202
+ caps.add("core.transactions");
203
+ }
204
+ this.capabilities = createCapabilities(caps);
205
+ }
206
+ capabilities;
207
+ /** Physical table holding this graph's triples. */
208
+ collectionPath;
209
+ scopePath;
210
+ /** Storage scope (interleaved parent UIDs + subgraph names) — `''` at root. */
211
+ storageScope;
212
+ /** Root graph's table name — prefix for subgraph tables and the catalog. */
213
+ rootTable;
214
+ registry;
215
+ coreIndexes;
216
+ ensured = null;
217
+ /**
218
+ * Lazily create this graph's table + indexes + the catalog, and register
219
+ * the graph in the catalog. Runs once per backend instance; the DDL is
220
+ * all `IF NOT EXISTS` / `INSERT OR IGNORE`, so concurrent instances over
221
+ * the same database converge safely.
222
+ */
223
+ ensureSchema() {
224
+ if (!this.ensured) {
225
+ this.ensured = this.doEnsureSchema().catch((err) => {
226
+ this.ensured = null;
227
+ throw err;
228
+ });
229
+ }
230
+ return this.ensured;
231
+ }
232
+ async doEnsureSchema() {
233
+ const ddl = [
234
+ ...buildSchemaStatements(this.collectionPath, {
235
+ coreIndexes: this.coreIndexes,
236
+ registry: this.registry
237
+ }),
238
+ buildCatalogDDL(this.rootTable)
239
+ ];
240
+ const statements = ddl.map((sql) => ({ sql, params: [] }));
241
+ statements.push(
242
+ compileCatalogRegister(
243
+ this.rootTable,
244
+ this.storageScope,
245
+ this.collectionPath,
246
+ this.scopePath
247
+ )
248
+ );
249
+ const chunks = chunkStatements(
250
+ statements,
251
+ this.executor.maxBatchSize,
252
+ this.executor.maxBatchParams
253
+ );
254
+ for (const chunk of chunks) {
255
+ await this.executor.batch(chunk);
256
+ }
257
+ }
258
+ /**
259
+ * Run `op` with the schema bootstrap applied, self-healing when this
260
+ * graph's table was dropped out from under the instance — a parent's
261
+ * cascade delete DROPs descendant tables, but subgraph handles created
262
+ * before the cascade still point at this (now missing) table with a
263
+ * resolved bootstrap cache. On a "no such table: <own table>" error the
264
+ * cache resets, the empty graph is recreated, and the op retries once.
265
+ * This matches Firestore semantics, where a deleted subcollection reads
266
+ * as empty and writes recreate it.
267
+ */
268
+ async withSchema(op) {
269
+ await this.ensureSchema();
270
+ try {
271
+ return await op();
272
+ } catch (err) {
273
+ if (!this.isMissingOwnTable(err)) throw err;
274
+ this.ensured = null;
275
+ await this.ensureSchema();
276
+ return op();
277
+ }
278
+ }
279
+ /** True when `err` is SQLite's missing-table error naming OUR table. */
280
+ isMissingOwnTable(err) {
281
+ const message = err instanceof Error ? err.message : String(err);
282
+ return message.includes(`no such table: ${this.collectionPath}`);
283
+ }
284
+ // --- Reads ---
285
+ async getDoc(docId) {
286
+ return this.withSchema(async () => {
287
+ const stmt = compileSelectByDocId(this.collectionPath, docId);
288
+ const rows = await this.executor.all(stmt.sql, stmt.params);
289
+ return rows.length === 0 ? null : rowToRecord(rows[0]);
290
+ });
291
+ }
292
+ async query(filters, options) {
293
+ return this.withSchema(async () => {
294
+ const stmt = compileSelect(this.collectionPath, filters, options);
295
+ const rows = await this.executor.all(stmt.sql, stmt.params);
296
+ return rows.map(rowToRecord);
297
+ });
298
+ }
299
+ // --- Writes ---
300
+ async setDoc(docId, record, mode) {
301
+ return this.withSchema(async () => {
302
+ const stmt = compileSet(this.collectionPath, docId, record, Date.now(), mode);
303
+ await this.executor.run(stmt.sql, stmt.params);
304
+ });
305
+ }
306
+ async updateDoc(docId, update) {
307
+ return this.withSchema(async () => {
308
+ const stmt = compileUpdate(this.collectionPath, docId, update, Date.now());
309
+ const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
310
+ const rows = await this.executor.all(sqlWithReturning, stmt.params);
311
+ if (rows.length === 0) {
312
+ throw new FiregraphError(
313
+ `updateDoc: no document found for doc_id=${docId} (table=${this.collectionPath})`,
314
+ "NOT_FOUND"
315
+ );
316
+ }
317
+ });
318
+ }
319
+ async deleteDoc(docId) {
320
+ return this.withSchema(async () => {
321
+ const stmt = compileDelete(this.collectionPath, docId);
322
+ await this.executor.run(stmt.sql, stmt.params);
323
+ });
324
+ }
325
+ // --- Transactions / Batches ---
326
+ async runTransaction(fn) {
327
+ if (!this.executor.transaction) {
328
+ throw new FiregraphError(
329
+ "Interactive transactions are not supported by this SQLite driver. D1 in particular has no read-then-conditional-write transactions; use a Durable Object SQLite client instead, or rewrite the code path as a batch().",
330
+ "UNSUPPORTED_OPERATION"
331
+ );
332
+ }
333
+ await this.ensureSchema();
334
+ return this.executor.transaction(async (tx) => {
335
+ const txBackend = new SqliteTransactionBackendImpl(tx, this.collectionPath);
336
+ return fn(txBackend);
337
+ });
338
+ }
339
+ createBatch() {
340
+ return new SqliteBatchBackendImpl(
341
+ this.executor,
342
+ this.collectionPath,
343
+ () => this.ensureSchema()
344
+ );
345
+ }
346
+ // --- Subgraphs ---
347
+ subgraph(parentNodeUid, name) {
348
+ if (!parentNodeUid || parentNodeUid.includes("/")) {
349
+ throw new FiregraphError(
350
+ `Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
351
+ "INVALID_SUBGRAPH"
352
+ );
353
+ }
354
+ if (!name || name.includes("/")) {
355
+ throw new FiregraphError(
356
+ `Subgraph name must not contain "/" and must be non-empty: got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
357
+ "INVALID_SUBGRAPH"
358
+ );
359
+ }
360
+ const newStorageScope = this.storageScope ? `${this.storageScope}/${parentNodeUid}/${name}` : `${parentNodeUid}/${name}`;
361
+ const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
362
+ return new _SqliteBackendImpl(
363
+ this.executor,
364
+ this.rootTable,
365
+ newStorageScope,
366
+ newScope,
367
+ this.registry,
368
+ this.coreIndexes
369
+ );
370
+ }
371
+ // --- Cascade & bulk ---
372
+ async removeNodeCascade(uid, reader, options) {
373
+ await this.ensureSchema();
374
+ const [outgoingRaw, incomingRaw] = await Promise.all([
375
+ reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
376
+ reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
377
+ ]);
378
+ const seen = /* @__PURE__ */ new Set();
379
+ const edgeDocIds = [];
380
+ for (const edge of [...outgoingRaw, ...incomingRaw]) {
381
+ if (edge.axbType === NODE_RELATION) continue;
382
+ const docId = computeEdgeDocId(edge.aUid, edge.axbType, edge.bUid);
383
+ if (!seen.has(docId)) {
384
+ seen.add(docId);
385
+ edgeDocIds.push(docId);
386
+ }
387
+ }
388
+ const nodeDocId = computeNodeDocId(uid);
389
+ const shouldDeleteSubgraphs = options?.deleteSubcollections !== false;
390
+ const descendants = [];
391
+ let subgraphRowCount = 0;
392
+ if (shouldDeleteSubgraphs) {
393
+ const prefix = this.storageScope ? `${this.storageScope}/${uid}` : uid;
394
+ const descStmt = compileCatalogDescendants(this.rootTable, prefix);
395
+ const rows = await this.executor.all(descStmt.sql, descStmt.params);
396
+ for (const row of rows) {
397
+ const tableName = String(row.table_name);
398
+ validateTableName(tableName);
399
+ descendants.push({ storageScope: String(row.storage_scope), tableName });
400
+ }
401
+ for (const d of descendants) {
402
+ const countRows = await this.executor.all(
403
+ `SELECT COUNT(*) AS n FROM ${quoteIdent(d.tableName)}`,
404
+ []
405
+ );
406
+ const n = countRows[0]?.n;
407
+ subgraphRowCount += typeof n === "bigint" ? Number(n) : Number(n ?? 0);
408
+ }
409
+ }
410
+ const writeStatements = edgeDocIds.map(
411
+ (id) => compileDelete(this.collectionPath, id)
412
+ );
413
+ writeStatements.push(compileDelete(this.collectionPath, nodeDocId));
414
+ for (const d of descendants) {
415
+ writeStatements.push({ sql: `DROP TABLE IF EXISTS ${quoteIdent(d.tableName)}`, params: [] });
416
+ writeStatements.push(compileCatalogDelete(this.rootTable, d.storageScope));
417
+ }
418
+ const {
419
+ deleted: stmtDeleted,
420
+ batches,
421
+ errors
422
+ } = await this.executeChunkedBatches(writeStatements, options);
423
+ const allOk = errors.length === 0;
424
+ const edgesDeleted = allOk ? edgeDocIds.length : 0;
425
+ const nodeDeleted = allOk;
426
+ const bookkeepingContribution = allOk ? descendants.length * 2 : 0;
427
+ const deleted = stmtDeleted - bookkeepingContribution + (allOk ? subgraphRowCount : 0);
428
+ return { deleted, batches, errors, edgesDeleted, nodeDeleted };
429
+ }
430
+ async bulkRemoveEdges(params, reader, options) {
431
+ await this.ensureSchema();
432
+ const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
433
+ const edges = await reader.findEdges(effectiveParams);
434
+ const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
435
+ if (docIds.length === 0) {
436
+ return { deleted: 0, batches: 0, errors: [] };
437
+ }
438
+ const statements = docIds.map((id) => compileDelete(this.collectionPath, id));
439
+ return this.executeChunkedBatches(statements, options);
440
+ }
441
+ /**
442
+ * Submit `statements` to the executor as one or more `batch()` calls,
443
+ * chunking by `executor.maxBatchSize` (e.g. D1's ~100-statement cap).
444
+ * Drivers that don't advertise a cap submit everything in one batch,
445
+ * preserving cross-batch atomicity.
446
+ *
447
+ * Each chunk is retried with exponential backoff up to `maxRetries`
448
+ * (default 3) before being recorded in `errors`. The loop continues past
449
+ * a permanently failed chunk so the caller still gets partial progress
450
+ * visibility — to halt on first failure, set `maxRetries: 0` and check
451
+ * `result.errors.length` after the call.
452
+ *
453
+ * Returns `BulkResult`-shaped fields. `deleted` reflects only the
454
+ * statement count of *successfully committed* batches — a DROP TABLE
455
+ * statement contributes 1 to that total even though it may remove many
456
+ * rows; `removeNodeCascade` patches that up with pre-counted row totals.
457
+ *
458
+ * **Atomicity caveat (D1):** when chunking kicks in, atomicity is lost
459
+ * across chunk boundaries — one chunk may commit while a later one fails.
460
+ * `removeNodeCascade` is idempotent (deleting the same docs again is a
461
+ * no-op) so a caller can simply retry on partial failure. `bulkRemoveEdges`
462
+ * is also idempotent for the same reason. DO SQLite leaves `maxBatchSize`
463
+ * unset, so everything funnels through one atomic `transactionSync` and
464
+ * this caveat does not apply.
465
+ */
466
+ async executeChunkedBatches(statements, options) {
467
+ if (statements.length === 0) {
468
+ return { deleted: 0, batches: 0, errors: [] };
469
+ }
470
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
471
+ const callerBatchSize = options?.batchSize;
472
+ const stmtCap = minDefined(callerBatchSize, this.executor.maxBatchSize);
473
+ const chunks = chunkStatements(statements, stmtCap, this.executor.maxBatchParams);
474
+ const errors = [];
475
+ let deleted = 0;
476
+ let batches = 0;
477
+ const totalBatches = chunks.length;
478
+ const driverParamCap = this.executor.maxBatchParams;
479
+ for (let batchIndex = 0; batchIndex < chunks.length; batchIndex++) {
480
+ const chunk = chunks[batchIndex];
481
+ const isUnretriableOversize = chunk.length === 1 && driverParamCap !== void 0 && chunk[0].params.length > driverParamCap;
482
+ let committed = false;
483
+ let lastError = null;
484
+ const effectiveRetries = isUnretriableOversize ? 0 : maxRetries;
485
+ for (let attempt = 0; attempt <= effectiveRetries; attempt++) {
486
+ try {
487
+ await this.executor.batch(chunk);
488
+ committed = true;
489
+ break;
490
+ } catch (err) {
491
+ lastError = err instanceof Error ? err : new Error(String(err));
492
+ if (attempt < effectiveRetries) {
493
+ const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), MAX_RETRY_DELAY_MS);
494
+ await sleep(delay);
495
+ }
496
+ }
497
+ }
498
+ if (committed) {
499
+ deleted += chunk.length;
500
+ batches += 1;
501
+ } else if (lastError) {
502
+ errors.push({
503
+ batchIndex,
504
+ error: lastError,
505
+ operationCount: chunk.length
506
+ });
507
+ }
508
+ if (options?.onProgress) {
509
+ options.onProgress({
510
+ completedBatches: batches,
511
+ totalBatches,
512
+ deletedSoFar: deleted
513
+ });
514
+ }
515
+ }
516
+ return { deleted, batches, errors };
517
+ }
518
+ // `findEdgesGlobal` is deliberately NOT defined on this class. Each graph
519
+ // is its own table, so a "collection group" query would mean scanning every
520
+ // table listed in the catalog — an unbounded fan-out the cross-backend
521
+ // contract treats as unsupported (the Cloudflare DO edition makes the same
522
+ // call: no cross-DO index, no `findEdgesGlobal`). The client surfaces
523
+ // `UNSUPPORTED_OPERATION` when the method is absent.
524
+ // --- Aggregate ---
525
+ /**
526
+ * Run an aggregate query in a single SQL statement. Supports the full
527
+ * count/sum/avg/min/max set — the SQLite engine evaluates each aggregate
528
+ * function over the filtered row set and the executor returns one row
529
+ * with one column per alias. SUM/MIN/MAX of an empty set returns 0
530
+ * (SQLite's `SUM(NULL) = NULL` is mapped to a clean number for the
531
+ * cross-backend contract); AVG returns NaN, matching the mathematical
532
+ * convention and the Firestore Standard helper.
533
+ */
534
+ async aggregate(spec, filters) {
535
+ const { stmt, aliases } = compileAggregate(this.collectionPath, spec, filters);
536
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
537
+ const row = rows[0] ?? {};
538
+ const out = {};
539
+ for (const alias of aliases) {
540
+ const v = row[alias];
541
+ if (v === null || v === void 0) {
542
+ const op = spec[alias].op;
543
+ out[alias] = op === "avg" ? Number.NaN : 0;
544
+ } else if (typeof v === "bigint") {
545
+ out[alias] = Number(v);
546
+ } else if (typeof v === "number") {
547
+ out[alias] = v;
548
+ } else {
549
+ out[alias] = Number(v);
550
+ }
551
+ }
552
+ return out;
553
+ }
554
+ // --- Server-side DML ---
555
+ /**
556
+ * Delete every row matching `filters` in a single SQL DELETE statement.
557
+ *
558
+ * Uses `RETURNING "doc_id"` to count rows touched — the SQLite executor's
559
+ * `run` returns void, so RETURNING + `all()` is the portable way to learn
560
+ * how many rows the engine actually deleted. SQLite ≥ 3.35 supports
561
+ * `DELETE … RETURNING`; better-sqlite3, D1, and DO SQLite all run on a
562
+ * recent enough engine.
563
+ *
564
+ * Single-statement DML doesn't chunk: the engine handles N rows in one
565
+ * shot, so `BulkOptions.batchSize` is intentionally ignored. The retry
566
+ * loop here exists only for transient driver errors (e.g. D1 surface
567
+ * congestion); a permanent failure is surfaced via the `errors` array
568
+ * with `batchIndex: 0` so callers see the same shape as `bulkRemoveEdges`.
569
+ *
570
+ * Subgraph isolation is physical — the statement only ever touches this
571
+ * graph's table, so no scoping predicate is needed.
572
+ */
573
+ async bulkDelete(filters, options) {
574
+ await this.ensureSchema();
575
+ const stmt = compileBulkDelete(this.collectionPath, filters);
576
+ return this.executeDmlWithReturning(stmt, options);
577
+ }
578
+ /**
579
+ * Update every row matching `filters` with `patch.data` in a single SQL
580
+ * UPDATE statement. The patch is deep-merged into each row's `data`
581
+ * column via the same `flattenPatch` → `compileDataOpsExpr` pipeline that
582
+ * `compileUpdate` (single-row) uses.
583
+ *
584
+ * Same contract notes as `bulkDelete` apply: single-statement, no
585
+ * chunking, `RETURNING "doc_id"` for the affected count, retry loop for
586
+ * transient driver errors.
587
+ */
588
+ async bulkUpdate(filters, patch, options) {
589
+ await this.ensureSchema();
590
+ const stmt = compileBulkUpdate(this.collectionPath, filters, patch.data, Date.now());
591
+ return this.executeDmlWithReturning(stmt, options);
592
+ }
593
+ /**
594
+ * Multi-source fan-out — `query.join` capability.
595
+ *
596
+ * Issues a single `SELECT … WHERE "aUid" IN (?, ?, …)` statement that
597
+ * matches every edge from every source UID in one round trip. When
598
+ * `params.hydrate === true`, follows up with a second statement that
599
+ * fetches the target node rows; both queries hit the same table so
600
+ * the executor amortises connection / parsing cost across them.
601
+ *
602
+ * Empty `params.sources` short-circuits to an empty result without
603
+ * touching the executor — `IN ()` is not valid SQL.
604
+ *
605
+ * Per-source ordering / strict per-source LIMIT enforcement is NOT
606
+ * implemented here; see the `ExpandParams.limitPerSource` JSDoc and
607
+ * `compileExpand` for the cap semantics. Strict per-source caps would
608
+ * require window functions and were judged out of scope for the
609
+ * round-trip-collapse goal.
610
+ */
611
+ async expand(params) {
612
+ if (params.sources.length === 0) {
613
+ return params.hydrate ? { edges: [], targets: [] } : { edges: [] };
614
+ }
615
+ const stmt = compileExpand(this.collectionPath, params);
616
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
617
+ const edges = rows.map(rowToRecord);
618
+ if (!params.hydrate) {
619
+ return { edges };
620
+ }
621
+ const direction = params.direction ?? "forward";
622
+ const targetUids = edges.map((e) => direction === "forward" ? e.bUid : e.aUid);
623
+ const uniqueTargets = [...new Set(targetUids)];
624
+ if (uniqueTargets.length === 0) {
625
+ return { edges, targets: [] };
626
+ }
627
+ const hydrateStmt = compileExpandHydrate(this.collectionPath, uniqueTargets);
628
+ const hydrateRows = await this.executor.all(hydrateStmt.sql, hydrateStmt.params);
629
+ const byUid = /* @__PURE__ */ new Map();
630
+ for (const row of hydrateRows) {
631
+ const node = rowToRecord(row);
632
+ byUid.set(node.bUid, node);
633
+ }
634
+ const targets = targetUids.map((uid) => byUid.get(uid) ?? null);
635
+ return { edges, targets };
636
+ }
637
+ /**
638
+ * Server-side projection — `query.select` capability.
639
+ *
640
+ * Issues a single `SELECT json_extract(data, '$.f1'), …` statement that
641
+ * returns only the requested fields. The compiler emits one column per
642
+ * unique field plus a paired `json_type` column for `data.*` projections
643
+ * so the decoder can recover JSON-encoded objects/arrays without a
644
+ * second round trip. Migrations are NOT applied — the caller asked for
645
+ * a partial shape, and rehydrating that into the migration pipeline
646
+ * would require synthesising every absent field.
647
+ *
648
+ * The wire-payload reduction is the entire reason this method exists:
649
+ * a list view that only needs `title` / `date` no longer drags the
650
+ * full `data` JSON across the network. Callers that need the full
651
+ * record should use `findEdges` (with migration support).
652
+ */
653
+ async findEdgesProjected(select, filters, options) {
654
+ const { stmt, columns } = compileFindEdgesProjected(
655
+ this.collectionPath,
656
+ select,
657
+ filters,
658
+ options
659
+ );
660
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
661
+ return rows.map((row) => decodeProjectedRow(row, columns));
662
+ }
663
+ /**
664
+ * Run a DML statement with `RETURNING "doc_id"` so we can count the
665
+ * rows the engine touched, with the same retry/backoff contract as
666
+ * `executeChunkedBatches`. Single statement, single batch.
667
+ */
668
+ async executeDmlWithReturning(stmt, options) {
669
+ const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
670
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
671
+ let lastError = null;
672
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
673
+ try {
674
+ const rows = await this.executor.all(sqlWithReturning, stmt.params);
675
+ const deleted = rows.length;
676
+ if (options?.onProgress) {
677
+ options.onProgress({
678
+ completedBatches: 1,
679
+ totalBatches: 1,
680
+ deletedSoFar: deleted
681
+ });
682
+ }
683
+ return { deleted, batches: 1, errors: [] };
684
+ } catch (err) {
685
+ lastError = err instanceof Error ? err : new Error(String(err));
686
+ if (attempt < maxRetries) {
687
+ const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), MAX_RETRY_DELAY_MS);
688
+ await sleep(delay);
689
+ }
690
+ }
691
+ }
692
+ return {
693
+ deleted: 0,
694
+ batches: 0,
695
+ errors: [
696
+ {
697
+ batchIndex: 0,
698
+ error: lastError ?? new Error("bulk DML failed for unknown reason"),
699
+ operationCount: 0
700
+ }
701
+ ]
702
+ };
703
+ }
704
+ };
705
+ function createSqliteBackend(executor, tableName, options = {}) {
706
+ const storageScope = options.storageScope ?? "";
707
+ const scopePath = options.scopePath ?? "";
708
+ return new SqliteBackendImpl(
709
+ executor,
710
+ tableName,
711
+ storageScope,
712
+ scopePath,
713
+ options.registry,
714
+ options.coreIndexes
715
+ );
716
+ }
717
+
718
+ export {
719
+ createSqliteBackend
720
+ };
721
+ //# sourceMappingURL=chunk-FODIMIWY.js.map