@typicalday/firegraph 0.14.1 → 0.16.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 (72) hide show
  1. package/README.md +62 -20
  2. package/dist/backend-CE3pM9-T.d.ts +167 -0
  3. package/dist/{backend-DuvHGgK1.d.cts → backend-DNzv8KSR.d.cts} +34 -20
  4. package/dist/{backend-DuvHGgK1.d.ts → backend-DNzv8KSR.d.ts} +34 -20
  5. package/dist/backend-EjFfw9yO.d.cts +167 -0
  6. package/dist/backend.cjs.map +1 -1
  7. package/dist/backend.d.cts +2 -2
  8. package/dist/backend.d.ts +2 -2
  9. package/dist/backend.js +1 -1
  10. package/dist/chunk-5JBNLH5W.js +732 -0
  11. package/dist/chunk-5JBNLH5W.js.map +1 -0
  12. package/dist/{chunk-3AHHXMWX.js → chunk-6IO74NKD.js} +23 -44
  13. package/dist/chunk-6IO74NKD.js.map +1 -0
  14. package/dist/{chunk-DJI3VXXA.js → chunk-7IEZ6IYY.js} +2 -2
  15. package/dist/chunk-7IEZ6IYY.js.map +1 -0
  16. package/dist/chunk-NGAJCALM.js +34 -0
  17. package/dist/chunk-NGAJCALM.js.map +1 -0
  18. package/dist/chunk-NZVSLWNY.js +867 -0
  19. package/dist/chunk-NZVSLWNY.js.map +1 -0
  20. package/dist/{chunk-N5HFDWQX.js → chunk-PWIO46RT.js} +1 -1
  21. package/dist/{chunk-N5HFDWQX.js.map → chunk-PWIO46RT.js.map} +1 -1
  22. package/dist/{client-BKi3vk0Q.d.ts → client-CNAwJayO.d.ts} +1 -1
  23. package/dist/{client-BrsaXtDV.d.cts → client-CaXH5D5C.d.cts} +1 -1
  24. package/dist/{client-Bk2Cm6xv.d.cts → client-DoyEdJ5w.d.cts} +1 -1
  25. package/dist/{client-Bk2Cm6xv.d.ts → client-DoyEdJ5w.d.ts} +1 -1
  26. package/dist/cloudflare/index.cjs +159 -167
  27. package/dist/cloudflare/index.cjs.map +1 -1
  28. package/dist/cloudflare/index.d.cts +73 -70
  29. package/dist/cloudflare/index.d.ts +73 -70
  30. package/dist/cloudflare/index.js +54 -589
  31. package/dist/cloudflare/index.js.map +1 -1
  32. package/dist/codegen/index.d.cts +1 -1
  33. package/dist/codegen/index.d.ts +1 -1
  34. package/dist/firestore-enterprise/index.cjs +11 -9
  35. package/dist/firestore-enterprise/index.cjs.map +1 -1
  36. package/dist/firestore-enterprise/index.d.cts +3 -3
  37. package/dist/firestore-enterprise/index.d.ts +3 -3
  38. package/dist/firestore-enterprise/index.js +6 -4
  39. package/dist/firestore-enterprise/index.js.map +1 -1
  40. package/dist/firestore-standard/index.cjs +11 -9
  41. package/dist/firestore-standard/index.cjs.map +1 -1
  42. package/dist/firestore-standard/index.d.cts +3 -3
  43. package/dist/firestore-standard/index.d.ts +3 -3
  44. package/dist/firestore-standard/index.js +4 -3
  45. package/dist/firestore-standard/index.js.map +1 -1
  46. package/dist/index.cjs +11 -9
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +5 -5
  49. package/dist/index.d.ts +5 -5
  50. package/dist/index.js +6 -4
  51. package/dist/index.js.map +1 -1
  52. package/dist/query-client/index.d.cts +2 -2
  53. package/dist/query-client/index.d.ts +2 -2
  54. package/dist/{registry-C2KUPVZj.d.ts → registry-By1i-zge.d.ts} +2 -2
  55. package/dist/{registry-Bc7h6WTM.d.cts → registry-CNToyEra.d.cts} +2 -2
  56. package/dist/sqlite/index.cjs +599 -380
  57. package/dist/sqlite/index.cjs.map +1 -1
  58. package/dist/sqlite/index.d.cts +4 -110
  59. package/dist/sqlite/index.d.ts +4 -110
  60. package/dist/sqlite/index.js +7 -1144
  61. package/dist/sqlite/index.js.map +1 -1
  62. package/dist/sqlite/local.cjs +2262 -0
  63. package/dist/sqlite/local.cjs.map +1 -0
  64. package/dist/sqlite/local.d.cts +109 -0
  65. package/dist/sqlite/local.d.ts +109 -0
  66. package/dist/sqlite/local.js +546 -0
  67. package/dist/sqlite/local.js.map +1 -0
  68. package/package.json +15 -1
  69. package/dist/chunk-3AHHXMWX.js.map +0 -1
  70. package/dist/chunk-DJI3VXXA.js.map +0 -1
  71. package/dist/chunk-NNBSUOOF.js +0 -289
  72. package/dist/chunk-NNBSUOOF.js.map +0 -1
@@ -0,0 +1,546 @@
1
+ import {
2
+ catalogTableName,
3
+ createSqliteBackend
4
+ } from "../chunk-5JBNLH5W.js";
5
+ import {
6
+ compileFilterConditions,
7
+ quoteIdent,
8
+ rowToRecord,
9
+ validateJsonPathKey,
10
+ validateTableName
11
+ } from "../chunk-NZVSLWNY.js";
12
+ import "../chunk-2DHMNTV6.js";
13
+ import {
14
+ createCapabilities
15
+ } from "../chunk-PWIO46RT.js";
16
+ import "../chunk-NGAJCALM.js";
17
+ import {
18
+ FiregraphError
19
+ } from "../chunk-SIHE4UY4.js";
20
+ import "../chunk-EQJUUVFG.js";
21
+
22
+ // src/internal/sqlite-search.ts
23
+ var VECTOR_DISTANCE_UDF = "firegraph_vector_distance";
24
+ var DISTANCE_ALIAS = "__fg_distance";
25
+ var BACKEND_ERR_LABEL = "SQLite backend";
26
+ var ENVELOPE_FIELDS = /* @__PURE__ */ new Set([
27
+ "aType",
28
+ "aUid",
29
+ "axbType",
30
+ "bType",
31
+ "bUid",
32
+ "createdAt",
33
+ "updatedAt",
34
+ "v"
35
+ ]);
36
+ function ftsTableName(table) {
37
+ return `${table}_fts`;
38
+ }
39
+ function ftsMapTableName(table) {
40
+ return `${table}_fts_map`;
41
+ }
42
+ function textExtractionExpr(dataRef) {
43
+ return `(SELECT coalesce(group_concat("value", ' '), '') FROM json_tree(coalesce(${dataRef}, '{}')) WHERE "type" = 'text')`;
44
+ }
45
+ function buildFtsDDL(table) {
46
+ const t = quoteIdent(table);
47
+ const fts = quoteIdent(ftsTableName(table));
48
+ const map = quoteIdent(ftsMapTableName(table));
49
+ const mappedId = `(SELECT "id" FROM ${map} WHERE "doc_id" = new."doc_id")`;
50
+ const reindexBody = ` INSERT INTO ${map} ("doc_id") SELECT new."doc_id" WHERE NOT EXISTS (SELECT 1 FROM ${map} WHERE "doc_id" = new."doc_id");
51
+ DELETE FROM ${fts} WHERE rowid = ${mappedId};
52
+ INSERT INTO ${fts} (rowid, "text") VALUES (${mappedId}, ${textExtractionExpr('new."data"')});
53
+ `;
54
+ return [
55
+ `CREATE TABLE IF NOT EXISTS ${map} (
56
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ "doc_id" TEXT NOT NULL UNIQUE
58
+ )`,
59
+ `CREATE VIRTUAL TABLE IF NOT EXISTS ${fts} USING fts5("text")`,
60
+ `CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${table}_fts_ai`)} AFTER INSERT ON ${t} BEGIN
61
+ ${reindexBody}END`,
62
+ `CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${table}_fts_au`)} AFTER UPDATE ON ${t} BEGIN
63
+ ${reindexBody}END`,
64
+ `CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${table}_fts_ad`)} AFTER DELETE ON ${t} BEGIN
65
+ DELETE FROM ${fts} WHERE rowid = (SELECT "id" FROM ${map} WHERE "doc_id" = old."doc_id");
66
+ DELETE FROM ${map} WHERE "doc_id" = old."doc_id";
67
+ END`
68
+ ];
69
+ }
70
+ function buildFtsSyncStatements(table) {
71
+ const t = quoteIdent(table);
72
+ const fts = quoteIdent(ftsTableName(table));
73
+ const map = quoteIdent(ftsMapTableName(table));
74
+ return [
75
+ `DELETE FROM ${fts} WHERE rowid IN (
76
+ SELECT m."id" FROM ${map} m LEFT JOIN ${t} t ON t."doc_id" = m."doc_id"
77
+ WHERE t."doc_id" IS NULL
78
+ )`,
79
+ `DELETE FROM ${map} WHERE "doc_id" NOT IN (SELECT "doc_id" FROM ${t})`,
80
+ `INSERT OR IGNORE INTO ${map} ("doc_id") SELECT "doc_id" FROM ${t}`,
81
+ `INSERT INTO ${fts} (rowid, "text")
82
+ SELECT m."id", ${textExtractionExpr('t."data"')}
83
+ FROM ${t} t JOIN ${map} m ON m."doc_id" = t."doc_id"
84
+ WHERE m."id" NOT IN (SELECT rowid FROM ${fts})`
85
+ ];
86
+ }
87
+ function buildLocalSearchDDL(table) {
88
+ return [...buildFtsDDL(table), ...buildFtsSyncStatements(table)];
89
+ }
90
+ function normalizeVectorFieldPath(label, field) {
91
+ if (ENVELOPE_FIELDS.has(field)) {
92
+ throw new FiregraphError(
93
+ `findNearest(): ${label} '${field}' is a built-in envelope field \u2014 vectors must live under \`data.*\`. Use a path like 'data.${field}' if you really meant a nested data field.`,
94
+ "INVALID_QUERY"
95
+ );
96
+ }
97
+ if (field === "data" || field.startsWith("data.")) return field;
98
+ return `data.${field}`;
99
+ }
100
+ function normalizeFullTextFieldPath(field) {
101
+ if (ENVELOPE_FIELDS.has(field)) {
102
+ throw new FiregraphError(
103
+ `fullTextSearch(): field '${field}' is a built-in envelope field \u2014 text-indexed fields must live under \`data.*\`. Use a path like 'data.${field}' if you really meant a nested data field.`,
104
+ "INVALID_QUERY"
105
+ );
106
+ }
107
+ if (field === "data" || field.startsWith("data.")) return field;
108
+ return `data.${field}`;
109
+ }
110
+ function buildSearchFilters(params) {
111
+ const filters = [];
112
+ if (params.aType) filters.push({ field: "aType", op: "==", value: params.aType });
113
+ if (params.axbType) filters.push({ field: "axbType", op: "==", value: params.axbType });
114
+ if (params.bType) filters.push({ field: "bType", op: "==", value: params.bType });
115
+ for (const clause of params.where ?? []) {
116
+ const field = ENVELOPE_FIELDS.has(clause.field) || clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
117
+ filters.push({ field, op: clause.op, value: clause.value });
118
+ }
119
+ return filters;
120
+ }
121
+ function compileFullTextSearch(table, params) {
122
+ if (typeof params.query !== "string" || params.query.length === 0) {
123
+ throw new FiregraphError(
124
+ "fullTextSearch(): query must be a non-empty string.",
125
+ "INVALID_QUERY"
126
+ );
127
+ }
128
+ if (!Number.isInteger(params.limit) || params.limit <= 0) {
129
+ throw new FiregraphError(
130
+ `fullTextSearch(): limit must be a positive integer (got ${params.limit}).`,
131
+ "INVALID_QUERY"
132
+ );
133
+ }
134
+ const normalizedFields = params.fields?.map((f) => normalizeFullTextFieldPath(f));
135
+ if (normalizedFields !== void 0 && normalizedFields.length > 0) {
136
+ throw new FiregraphError(
137
+ "fullTextSearch(): the `fields` option is not yet supported \u2014 the local SQLite FTS index stores one combined text column per record. Omit `fields` to search all string values.",
138
+ "INVALID_QUERY"
139
+ );
140
+ }
141
+ const t = quoteIdent(table);
142
+ const fts = quoteIdent(ftsTableName(table));
143
+ const map = quoteIdent(ftsMapTableName(table));
144
+ const sqlParams = [params.query];
145
+ const conditions = [`${fts} MATCH ?`];
146
+ conditions.push(...compileFilterConditions(buildSearchFilters(params), sqlParams));
147
+ sqlParams.push(params.limit);
148
+ const sql = `SELECT ${t}.* FROM ${fts} JOIN ${map} ON ${map}."id" = ${fts}.rowid JOIN ${t} ON ${t}."doc_id" = ${map}."doc_id" WHERE ${conditions.join(" AND ")} ORDER BY bm25(${fts}) ASC, ${t}."doc_id" ASC LIMIT ?`;
149
+ return { sql, params: sqlParams };
150
+ }
151
+ var DISTANCE_MEASURES = /* @__PURE__ */ new Set(["EUCLIDEAN", "COSINE", "DOT_PRODUCT"]);
152
+ function toNumberArray(qv) {
153
+ if (Array.isArray(qv)) return qv;
154
+ if (typeof qv.toArray === "function") {
155
+ return qv.toArray();
156
+ }
157
+ throw new FiregraphError(
158
+ "findNearest(): queryVector must be a number[] or a Firestore VectorValue.",
159
+ "INVALID_QUERY"
160
+ );
161
+ }
162
+ function compileFindNearest(table, params) {
163
+ const vec = toNumberArray(params.queryVector);
164
+ if (vec.length === 0) {
165
+ throw new FiregraphError(
166
+ "findNearest(): queryVector is empty \u2014 at least one dimension is required.",
167
+ "INVALID_QUERY"
168
+ );
169
+ }
170
+ if (!Number.isInteger(params.limit) || params.limit <= 0 || params.limit > 1e3) {
171
+ throw new FiregraphError(
172
+ `findNearest(): limit must be a positive integer \u2264 1000 (got ${params.limit}).`,
173
+ "INVALID_QUERY"
174
+ );
175
+ }
176
+ if (!DISTANCE_MEASURES.has(params.distanceMeasure)) {
177
+ throw new FiregraphError(
178
+ `findNearest(): unknown distanceMeasure '${String(params.distanceMeasure)}' \u2014 expected EUCLIDEAN, COSINE, or DOT_PRODUCT.`,
179
+ "INVALID_QUERY"
180
+ );
181
+ }
182
+ const vectorField = normalizeVectorFieldPath("vectorField", params.vectorField);
183
+ let vectorExpr;
184
+ if (vectorField === "data") {
185
+ vectorExpr = '"data"';
186
+ } else {
187
+ const suffix = vectorField.slice("data.".length);
188
+ for (const part of suffix.split(".")) {
189
+ validateJsonPathKey(part, BACKEND_ERR_LABEL);
190
+ }
191
+ vectorExpr = `json_extract("data", '$.${suffix}')`;
192
+ }
193
+ let distancePath = null;
194
+ if (params.distanceResultField !== void 0) {
195
+ const normalized = normalizeVectorFieldPath("distanceResultField", params.distanceResultField);
196
+ if (normalized === "data") {
197
+ throw new FiregraphError(
198
+ `findNearest(): distanceResultField 'data' would replace the entire data payload \u2014 use a nested path like 'data.distance'.`,
199
+ "INVALID_QUERY"
200
+ );
201
+ }
202
+ distancePath = normalized.slice("data.".length).split(".");
203
+ for (const part of distancePath) {
204
+ validateJsonPathKey(part, BACKEND_ERR_LABEL);
205
+ }
206
+ }
207
+ const sqlParams = [JSON.stringify(vec), params.distanceMeasure];
208
+ const conditions = compileFilterConditions(buildSearchFilters(params), sqlParams);
209
+ const innerWhere = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
210
+ const dist = quoteIdent(DISTANCE_ALIAS);
211
+ const descending = params.distanceMeasure === "DOT_PRODUCT";
212
+ let sql = `SELECT * FROM (SELECT *, ${VECTOR_DISTANCE_UDF}(${vectorExpr}, ?, ?) AS ${dist} FROM ${quoteIdent(table)}${innerWhere}) WHERE ${dist} IS NOT NULL`;
213
+ if (params.distanceThreshold !== void 0) {
214
+ sql += ` AND ${dist} ${descending ? ">=" : "<="} ?`;
215
+ sqlParams.push(params.distanceThreshold);
216
+ }
217
+ sql += ` ORDER BY ${dist} ${descending ? "DESC" : "ASC"}, "doc_id" ASC LIMIT ?`;
218
+ sqlParams.push(params.limit);
219
+ return { stmt: { sql, params: sqlParams }, distancePath };
220
+ }
221
+ var memoQueryJson = null;
222
+ var memoQueryVec = null;
223
+ function computeVectorDistance(storedJson, queryJson, measure) {
224
+ if (typeof storedJson !== "string" || typeof queryJson !== "string" || typeof measure !== "string") {
225
+ return null;
226
+ }
227
+ let query;
228
+ if (memoQueryJson === queryJson && memoQueryVec !== null) {
229
+ query = memoQueryVec;
230
+ } else {
231
+ let parsed;
232
+ try {
233
+ parsed = JSON.parse(queryJson);
234
+ } catch {
235
+ return null;
236
+ }
237
+ if (!Array.isArray(parsed)) return null;
238
+ query = parsed;
239
+ memoQueryJson = queryJson;
240
+ memoQueryVec = query;
241
+ }
242
+ let stored;
243
+ try {
244
+ stored = JSON.parse(storedJson);
245
+ } catch {
246
+ return null;
247
+ }
248
+ if (!Array.isArray(stored) || stored.length !== query.length) return null;
249
+ let dot = 0;
250
+ let sumSq = 0;
251
+ let normStored = 0;
252
+ let normQuery = 0;
253
+ for (let i = 0; i < query.length; i++) {
254
+ const a = stored[i];
255
+ const b = query[i];
256
+ if (typeof a !== "number" || !Number.isFinite(a)) return null;
257
+ if (typeof b !== "number" || !Number.isFinite(b)) return null;
258
+ dot += a * b;
259
+ const diff = a - b;
260
+ sumSq += diff * diff;
261
+ normStored += a * a;
262
+ normQuery += b * b;
263
+ }
264
+ let result;
265
+ switch (measure) {
266
+ case "EUCLIDEAN":
267
+ result = Math.sqrt(sumSq);
268
+ break;
269
+ case "COSINE": {
270
+ const denom = Math.sqrt(normStored) * Math.sqrt(normQuery);
271
+ if (denom === 0) return null;
272
+ result = 1 - dot / denom;
273
+ break;
274
+ }
275
+ case "DOT_PRODUCT":
276
+ result = dot;
277
+ break;
278
+ default:
279
+ return null;
280
+ }
281
+ return Number.isFinite(result) ? result : null;
282
+ }
283
+ function setDataPath(data, path, value) {
284
+ let cursor = data;
285
+ for (let i = 0; i < path.length - 1; i++) {
286
+ const key = path[i];
287
+ const next = cursor[key];
288
+ if (typeof next !== "object" || next === null || Array.isArray(next)) {
289
+ const created = {};
290
+ cursor[key] = created;
291
+ cursor = created;
292
+ } else {
293
+ cursor = next;
294
+ }
295
+ }
296
+ cursor[path[path.length - 1]] = value;
297
+ }
298
+ function findOrphanedFtsTables(allTables, catalogTables, rootTable) {
299
+ const names = new Set(allTables);
300
+ const liveGraphTables = new Set(catalogTables);
301
+ const subgraphPrefix = `${rootTable}_g_`;
302
+ const orphans = [];
303
+ for (const name of names) {
304
+ let base = null;
305
+ if (name.endsWith("_fts_map")) base = name.slice(0, -"_fts_map".length);
306
+ else if (name.endsWith("_fts")) base = name.slice(0, -"_fts".length);
307
+ if (base === null || !base.startsWith(subgraphPrefix)) continue;
308
+ if (liveGraphTables.has(name)) continue;
309
+ if (names.has(base)) continue;
310
+ orphans.push(name);
311
+ }
312
+ return orphans.sort();
313
+ }
314
+
315
+ // src/sqlite/local.ts
316
+ function createBetterSqliteExecutor(db) {
317
+ return {
318
+ async all(sql, params) {
319
+ return db.prepare(sql).all(...params);
320
+ },
321
+ async run(sql, params) {
322
+ db.prepare(sql).run(...params);
323
+ },
324
+ async batch(statements) {
325
+ const tx = db.transaction((stmts) => {
326
+ for (const s of stmts) {
327
+ db.prepare(s.sql).run(...s.params);
328
+ }
329
+ });
330
+ tx(statements);
331
+ },
332
+ async transaction(fn) {
333
+ db.exec("BEGIN IMMEDIATE");
334
+ try {
335
+ const result = await fn({
336
+ async all(sql, params) {
337
+ return db.prepare(sql).all(...params);
338
+ },
339
+ async run(sql, params) {
340
+ db.prepare(sql).run(...params);
341
+ }
342
+ });
343
+ db.exec("COMMIT");
344
+ return result;
345
+ } catch (err) {
346
+ db.exec("ROLLBACK");
347
+ throw err;
348
+ }
349
+ }
350
+ };
351
+ }
352
+ function isDatabase(value) {
353
+ return typeof value === "object" && value !== null && typeof value.prepare === "function" && typeof value.exec === "function";
354
+ }
355
+ var PRAGMA_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
356
+ var PRAGMA_VALUE_PATTERN = /^-?[A-Za-z0-9_]+$/;
357
+ function applyPragmas(db, pragmas) {
358
+ for (const [key, value] of Object.entries(pragmas)) {
359
+ if (!PRAGMA_KEY_PATTERN.test(key)) {
360
+ throw new FiregraphError(`Invalid pragma name: ${JSON.stringify(key)}`, "INVALID_ARGUMENT");
361
+ }
362
+ if (!PRAGMA_VALUE_PATTERN.test(String(value)) || typeof value === "number" && !Number.isFinite(value)) {
363
+ throw new FiregraphError(
364
+ `Invalid pragma value for ${key}: ${JSON.stringify(value)}`,
365
+ "INVALID_ARGUMENT"
366
+ );
367
+ }
368
+ db.pragma(`${key} = ${value}`);
369
+ }
370
+ }
371
+ function registerVectorUdf(db) {
372
+ try {
373
+ db.function(
374
+ VECTOR_DISTANCE_UDF,
375
+ { deterministic: true },
376
+ (stored, query, measure) => computeVectorDistance(stored, query, measure)
377
+ );
378
+ } catch {
379
+ }
380
+ }
381
+ async function sweepOrphanedFtsArtifacts(executor, rootTable) {
382
+ const tableRows = await executor.all(
383
+ `SELECT "name" FROM sqlite_master WHERE "type" = 'table'`,
384
+ []
385
+ );
386
+ const allTables = tableRows.map((r) => String(r.name));
387
+ const catalogRows = await executor.all(
388
+ `SELECT "table_name" FROM ${quoteIdent(catalogTableName(rootTable))}`,
389
+ []
390
+ );
391
+ const catalogTables = catalogRows.map((r) => String(r.table_name));
392
+ for (const name of findOrphanedFtsTables(allTables, catalogTables, rootTable)) {
393
+ validateTableName(name);
394
+ await executor.run(`DROP TABLE IF EXISTS ${quoteIdent(name)}`, []);
395
+ }
396
+ }
397
+ function wrapLocalSearchBackend(inner, executor, rootTable) {
398
+ const caps = /* @__PURE__ */ new Set([
399
+ ...inner.capabilities.values(),
400
+ "search.fullText",
401
+ "search.vector"
402
+ ]);
403
+ const healableTables = /* @__PURE__ */ new Set([
404
+ inner.collectionPath,
405
+ ftsTableName(inner.collectionPath),
406
+ ftsMapTableName(inner.collectionPath)
407
+ ]);
408
+ const runWithSchema = async (op) => {
409
+ await inner.ensureReady();
410
+ try {
411
+ return await op();
412
+ } catch (err) {
413
+ const message = err instanceof Error ? err.message : String(err);
414
+ const missing = /no such table: (\S+)/.exec(message)?.[1];
415
+ if (missing === void 0 || !healableTables.has(missing)) throw err;
416
+ await inner.ensureReady(true);
417
+ return op();
418
+ }
419
+ };
420
+ const wrapper = {
421
+ capabilities: createCapabilities(caps),
422
+ collectionPath: inner.collectionPath,
423
+ scopePath: inner.scopePath,
424
+ getDoc: (docId) => inner.getDoc(docId),
425
+ query: (filters, options) => inner.query(filters, options),
426
+ setDoc: (docId, record, mode) => inner.setDoc(docId, record, mode),
427
+ updateDoc: (docId, update) => inner.updateDoc(docId, update),
428
+ deleteDoc: (docId) => inner.deleteDoc(docId),
429
+ runTransaction: (fn) => inner.runTransaction(fn),
430
+ createBatch: () => inner.createBatch(),
431
+ subgraph: (parentNodeUid, name) => wrapLocalSearchBackend(inner.subgraph(parentNodeUid, name), executor, rootTable),
432
+ removeNodeCascade: async (uid, reader, options) => {
433
+ const result = await inner.removeNodeCascade(uid, reader, options);
434
+ if (result.errors.length === 0) {
435
+ await sweepOrphanedFtsArtifacts(executor, rootTable);
436
+ }
437
+ return result;
438
+ },
439
+ bulkRemoveEdges: (params, reader, options) => inner.bulkRemoveEdges(params, reader, options),
440
+ aggregate: (spec, filters) => inner.aggregate(spec, filters),
441
+ bulkDelete: (filters, options) => inner.bulkDelete(filters, options),
442
+ bulkUpdate: (filters, patch, options) => inner.bulkUpdate(filters, patch, options),
443
+ expand: (params) => inner.expand(params),
444
+ findEdgesProjected: (select, filters, options) => inner.findEdgesProjected(select, filters, options),
445
+ // `findEdgesGlobal` stays absent, same as the inner backend — each graph
446
+ // is its own table; there is no cross-table index.
447
+ async findNearest(params) {
448
+ const { stmt, distancePath } = compileFindNearest(inner.collectionPath, params);
449
+ const rows = await runWithSchema(() => executor.all(stmt.sql, stmt.params));
450
+ return rows.map((row) => {
451
+ const record = rowToRecord(row);
452
+ if (distancePath) {
453
+ const distance = row[DISTANCE_ALIAS];
454
+ setDataPath(
455
+ record.data,
456
+ distancePath,
457
+ typeof distance === "number" ? distance : Number(distance)
458
+ );
459
+ }
460
+ return record;
461
+ });
462
+ },
463
+ async fullTextSearch(params) {
464
+ const stmt = compileFullTextSearch(inner.collectionPath, params);
465
+ let rows;
466
+ try {
467
+ rows = await runWithSchema(() => executor.all(stmt.sql, stmt.params));
468
+ } catch (err) {
469
+ const message = err instanceof Error ? err.message : String(err);
470
+ if (message.includes("fts5") || message.includes("unknown special query")) {
471
+ throw new FiregraphError(
472
+ `fullTextSearch(): invalid FTS5 query syntax \u2014 ${message}`,
473
+ "INVALID_QUERY"
474
+ );
475
+ }
476
+ throw err;
477
+ }
478
+ return rows.map(rowToRecord);
479
+ }
480
+ };
481
+ return wrapper;
482
+ }
483
+ async function createLocalSqliteBackend(pathOrDb, options = {}) {
484
+ const {
485
+ tableName = "firegraph",
486
+ busyTimeoutMs = 5e3,
487
+ pragmas,
488
+ fileMustExist,
489
+ ...backendOptions
490
+ } = options;
491
+ let db;
492
+ let ownsDb;
493
+ if (typeof pathOrDb === "string") {
494
+ let Database;
495
+ try {
496
+ Database = (await import("better-sqlite3")).default;
497
+ } catch (err) {
498
+ throw new FiregraphError(
499
+ `createLocalSqliteBackend requires the optional peer dependency 'better-sqlite3' \u2014 install it to use the local SQLite backend (${err instanceof Error ? err.message : String(err)})`,
500
+ "MISSING_DEPENDENCY"
501
+ );
502
+ }
503
+ db = new Database(pathOrDb, fileMustExist ? { fileMustExist: true } : {});
504
+ ownsDb = true;
505
+ db.pragma("journal_mode = WAL");
506
+ } else if (isDatabase(pathOrDb)) {
507
+ db = pathOrDb;
508
+ ownsDb = false;
509
+ } else {
510
+ throw new FiregraphError(
511
+ "createLocalSqliteBackend expects a file path or an open better-sqlite3 Database",
512
+ "INVALID_ARGUMENT"
513
+ );
514
+ }
515
+ db.pragma(`busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
516
+ if (pragmas) {
517
+ applyPragmas(db, pragmas);
518
+ }
519
+ registerVectorUdf(db);
520
+ const userExtraDDL = backendOptions.extraTableDDL;
521
+ const optionsWithSearch = {
522
+ ...backendOptions,
523
+ extraTableDDL: (table) => [
524
+ ...userExtraDDL ? userExtraDDL(table) : [],
525
+ ...buildLocalSearchDDL(table)
526
+ ]
527
+ };
528
+ const executor = createBetterSqliteExecutor(db);
529
+ const inner = createSqliteBackend(executor, tableName, optionsWithSearch);
530
+ const backend = wrapLocalSearchBackend(inner, executor, tableName);
531
+ let closed = false;
532
+ return {
533
+ backend,
534
+ db,
535
+ close() {
536
+ if (closed || !ownsDb) return;
537
+ closed = true;
538
+ db.close();
539
+ }
540
+ };
541
+ }
542
+ export {
543
+ createBetterSqliteExecutor,
544
+ createLocalSqliteBackend
545
+ };
546
+ //# sourceMappingURL=local.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/internal/sqlite-search.ts","../../src/sqlite/local.ts"],"sourcesContent":["/**\n * Search compilation for the local SQLite backend (`firegraph/sqlite-local`).\n *\n * Two capabilities are compiled here:\n *\n * - **`search.fullText`** — an FTS5 index table per graph table, kept in\n * sync by pure-SQL triggers. Text is extracted from the `data` JSON via\n * `json_tree(...) WHERE type = 'text'`, so the triggers work from ANY\n * connection or process touching the file — no user-defined function\n * required on the write path. Queries rank with `bm25()` (lower =\n * better, so `ORDER BY bm25 ASC` is relevance-descending).\n *\n * - **`search.vector`** — brute-force k-NN via a deterministic scalar UDF\n * (`firegraph_vector_distance`) registered on the better-sqlite3\n * connection by `createLocalSqliteBackend`. There is no ANN index; the\n * engine evaluates the distance per candidate row, which is the right\n * trade-off for the local-file use case (thousands to low millions of\n * rows, zero infrastructure). UDFs are connection-local: vector search\n * only works through a connection that registered the function.\n *\n * ## FTS row keying\n *\n * The FTS5 table's `rowid` is keyed through a dedicated mapping table\n * (`<t>_fts_map`, `INTEGER PRIMARY KEY AUTOINCREMENT` → `doc_id`) rather\n * than the graph table's own rowid. The graph table has a TEXT primary key,\n * so its raw rowids are NOT stable — `VACUUM` may renumber them, silently\n * detaching every FTS entry. AUTOINCREMENT ids survive VACUUM. Storing\n * `doc_id` UNINDEXED inside the FTS table was also rejected: FTS5 can't\n * index UNINDEXED columns, making the per-write delete a full scan.\n *\n * Validation parity: error messages and codes mirror the Firestore helpers\n * (`firestore-vector.ts` / `firestore-fulltext.ts`) so a caller migrating\n * between backends sees the same failures. This module must stay free of\n * `@google-cloud/firestore` imports — it is bundled into the\n * `firegraph/sqlite-local` entry.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type { FindNearestParams, FullTextSearchParams, QueryFilter } from '../types.js';\nimport { validateJsonPathKey } from './sqlite-data-ops.js';\nimport { quoteIdent } from './sqlite-schema.js';\nimport type { CompiledStatement } from './sqlite-sql.js';\nimport { compileFilterConditions } from './sqlite-sql.js';\n\n/** Name of the connection-local vector-distance UDF. */\nexport const VECTOR_DISTANCE_UDF = 'firegraph_vector_distance';\n\n/** Column alias carrying the computed distance through the vector query. */\nexport const DISTANCE_ALIAS = '__fg_distance';\n\nconst BACKEND_ERR_LABEL = 'SQLite backend';\n\n/**\n * Built-in envelope fields that must NOT be passed as search field paths.\n * Mirrors the Firestore helpers' rejection list.\n */\nconst ENVELOPE_FIELDS: ReadonlySet<string> = new Set([\n 'aType',\n 'aUid',\n 'axbType',\n 'bType',\n 'bUid',\n 'createdAt',\n 'updatedAt',\n 'v',\n]);\n\n/** FTS5 index table for a graph table. */\nexport function ftsTableName(table: string): string {\n return `${table}_fts`;\n}\n\n/** Stable-rowid mapping table for a graph table's FTS index. */\nexport function ftsMapTableName(table: string): string {\n return `${table}_fts_map`;\n}\n\n/**\n * SQL fragment extracting every string value in a `data` JSON payload as\n * one space-joined text blob. Pure SQL (`json_tree`), so it is evaluatable\n * inside triggers from any connection.\n */\nfunction textExtractionExpr(dataRef: string): string {\n return (\n `(SELECT coalesce(group_concat(\"value\", ' '), '') ` +\n `FROM json_tree(coalesce(${dataRef}, '{}')) WHERE \"type\" = 'text')`\n );\n}\n\n/**\n * DDL installing the FTS5 infrastructure for one graph table: the mapping\n * table, the FTS5 virtual table, and three sync triggers. All statements\n * are `IF NOT EXISTS` — safe to re-run on every bootstrap.\n *\n * The AFTER INSERT trigger also fires for the INSERT arm of the backend's\n * upsert (`INSERT … ON CONFLICT DO UPDATE`); the conflict arm fires AFTER\n * UPDATE. Both re-derive the indexed text from `new.\"data\"`, and both\n * start with a defensive delete of any stale FTS row so replayed writes\n * never double-index.\n */\nexport function buildFtsDDL(table: string): string[] {\n const t = quoteIdent(table);\n const fts = quoteIdent(ftsTableName(table));\n const map = quoteIdent(ftsMapTableName(table));\n const mappedId = `(SELECT \"id\" FROM ${map} WHERE \"doc_id\" = new.\"doc_id\")`;\n // The map insert must be conflict-free rather than `INSERT OR IGNORE`:\n // when the outer statement is the backend's upsert (`INSERT … ON CONFLICT\n // DO UPDATE`), SQLite replaces conflict handling inside trigger programs\n // with the outer statement's algorithm, turning the IGNORE into an abort.\n const reindexBody =\n ` INSERT INTO ${map} (\"doc_id\") SELECT new.\"doc_id\" ` +\n `WHERE NOT EXISTS (SELECT 1 FROM ${map} WHERE \"doc_id\" = new.\"doc_id\");\\n` +\n ` DELETE FROM ${fts} WHERE rowid = ${mappedId};\\n` +\n ` INSERT INTO ${fts} (rowid, \"text\") VALUES (${mappedId}, ${textExtractionExpr('new.\"data\"')});\\n`;\n return [\n `CREATE TABLE IF NOT EXISTS ${map} (\n \"id\" INTEGER PRIMARY KEY AUTOINCREMENT,\n \"doc_id\" TEXT NOT NULL UNIQUE\n )`,\n `CREATE VIRTUAL TABLE IF NOT EXISTS ${fts} USING fts5(\"text\")`,\n `CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${table}_fts_ai`)} AFTER INSERT ON ${t} BEGIN\\n${reindexBody}END`,\n `CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${table}_fts_au`)} AFTER UPDATE ON ${t} BEGIN\\n${reindexBody}END`,\n `CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${table}_fts_ad`)} AFTER DELETE ON ${t} BEGIN\n DELETE FROM ${fts} WHERE rowid = (SELECT \"id\" FROM ${map} WHERE \"doc_id\" = old.\"doc_id\");\n DELETE FROM ${map} WHERE \"doc_id\" = old.\"doc_id\";\nEND`,\n ];\n}\n\n/**\n * Idempotent reconciliation statements run at every schema bootstrap,\n * after `buildFtsDDL`:\n *\n * 1–2. Purge FTS/map rows whose `doc_id` no longer exists in the graph\n * table. Covers the recreate-after-cascade path: a parent cascade\n * DROPs the graph table (taking the triggers with it) but leaves\n * the FTS artifacts; without the purge, a recreated subgraph would\n * surface ghost matches and hit UNIQUE violations on the map.\n * 3–4. Backfill map/FTS rows for graph rows that predate the FTS\n * infrastructure (e.g. a database written by an older firegraph).\n */\nexport function buildFtsSyncStatements(table: string): string[] {\n const t = quoteIdent(table);\n const fts = quoteIdent(ftsTableName(table));\n const map = quoteIdent(ftsMapTableName(table));\n return [\n `DELETE FROM ${fts} WHERE rowid IN (\n SELECT m.\"id\" FROM ${map} m LEFT JOIN ${t} t ON t.\"doc_id\" = m.\"doc_id\"\n WHERE t.\"doc_id\" IS NULL\n )`,\n `DELETE FROM ${map} WHERE \"doc_id\" NOT IN (SELECT \"doc_id\" FROM ${t})`,\n `INSERT OR IGNORE INTO ${map} (\"doc_id\") SELECT \"doc_id\" FROM ${t}`,\n `INSERT INTO ${fts} (rowid, \"text\")\n SELECT m.\"id\", ${textExtractionExpr('t.\"data\"')}\n FROM ${t} t JOIN ${map} m ON m.\"doc_id\" = t.\"doc_id\"\n WHERE m.\"id\" NOT IN (SELECT rowid FROM ${fts})`,\n ];\n}\n\n/**\n * Full `extraTableDDL` payload for `firegraph/sqlite-local`: FTS\n * infrastructure plus the reconciliation pass.\n */\nexport function buildLocalSearchDDL(table: string): string[] {\n return [...buildFtsDDL(table), ...buildFtsSyncStatements(table)];\n}\n\n/**\n * Normalise a caller-supplied vector / distance-result field path. Bare\n * names rewrite to `data.<name>`; `'data'` and `'data.*'` pass through;\n * envelope fields are rejected. Same contract and message shape as\n * `normalizeVectorFieldPath` in `firestore-vector.ts`.\n */\nexport function normalizeVectorFieldPath(label: string, field: string): string {\n if (ENVELOPE_FIELDS.has(field)) {\n throw new FiregraphError(\n `findNearest(): ${label} '${field}' is a built-in envelope field — ` +\n `vectors must live under \\`data.*\\`. Use a path like 'data.${field}' ` +\n `if you really meant a nested data field.`,\n 'INVALID_QUERY',\n );\n }\n if (field === 'data' || field.startsWith('data.')) return field;\n return `data.${field}`;\n}\n\n/**\n * Normalise a caller-supplied FTS field path. Same contract as\n * `normalizeFullTextFieldPath` in `firestore-fulltext.ts`.\n */\nexport function normalizeFullTextFieldPath(field: string): string {\n if (ENVELOPE_FIELDS.has(field)) {\n throw new FiregraphError(\n `fullTextSearch(): field '${field}' is a built-in envelope field — ` +\n `text-indexed fields must live under \\`data.*\\`. Use a path like ` +\n `'data.${field}' if you really meant a nested data field.`,\n 'INVALID_QUERY',\n );\n }\n if (field === 'data' || field.startsWith('data.')) return field;\n return `data.${field}`;\n}\n\n/**\n * Identifying filters (`aType` / `axbType` / `bType`) plus optional `where`.\n * Bare `where` field names rewrite to `data.<name>` — the same convention\n * `buildEdgeQueryPlan` applies for `findEdges({ where })`.\n */\nfunction buildSearchFilters(params: {\n aType?: string;\n axbType?: string;\n bType?: string;\n where?: QueryFilter[];\n}): QueryFilter[] {\n const filters: QueryFilter[] = [];\n if (params.aType) filters.push({ field: 'aType', op: '==', value: params.aType });\n if (params.axbType) filters.push({ field: 'axbType', op: '==', value: params.axbType });\n if (params.bType) filters.push({ field: 'bType', op: '==', value: params.bType });\n for (const clause of params.where ?? []) {\n const field =\n ENVELOPE_FIELDS.has(clause.field) || clause.field.startsWith('data.')\n ? clause.field\n : `data.${clause.field}`;\n filters.push({ field, op: clause.op, value: clause.value });\n }\n return filters;\n}\n\n/**\n * Compile a `fullTextSearch()` call into one SELECT over the FTS5 index.\n *\n * Validation parity with `runFirestoreFullTextSearch`: non-empty string\n * query, positive integer limit, and a non-empty `fields` list is rejected\n * with `INVALID_QUERY` (\"not yet supported\") — FTS5 column filters could\n * support per-field search later, but the single-blob index built today\n * has one `text` column, so the option is reserved rather than silently\n * mis-honoured.\n *\n * Results order by `bm25()` ascending (best match first), with `doc_id`\n * as a deterministic tie-break.\n */\nexport function compileFullTextSearch(\n table: string,\n params: FullTextSearchParams,\n): CompiledStatement {\n if (typeof params.query !== 'string' || params.query.length === 0) {\n throw new FiregraphError(\n 'fullTextSearch(): query must be a non-empty string.',\n 'INVALID_QUERY',\n );\n }\n if (!Number.isInteger(params.limit) || params.limit <= 0) {\n throw new FiregraphError(\n `fullTextSearch(): limit must be a positive integer (got ${params.limit}).`,\n 'INVALID_QUERY',\n );\n }\n const normalizedFields = params.fields?.map((f) => normalizeFullTextFieldPath(f));\n if (normalizedFields !== undefined && normalizedFields.length > 0) {\n throw new FiregraphError(\n 'fullTextSearch(): the `fields` option is not yet supported — ' +\n 'the local SQLite FTS index stores one combined text column per record. ' +\n 'Omit `fields` to search all string values.',\n 'INVALID_QUERY',\n );\n }\n\n const t = quoteIdent(table);\n const fts = quoteIdent(ftsTableName(table));\n const map = quoteIdent(ftsMapTableName(table));\n\n const sqlParams: unknown[] = [params.query];\n const conditions: string[] = [`${fts} MATCH ?`];\n conditions.push(...compileFilterConditions(buildSearchFilters(params), sqlParams));\n sqlParams.push(params.limit);\n\n const sql =\n `SELECT ${t}.* FROM ${fts} ` +\n `JOIN ${map} ON ${map}.\"id\" = ${fts}.rowid ` +\n `JOIN ${t} ON ${t}.\"doc_id\" = ${map}.\"doc_id\" ` +\n `WHERE ${conditions.join(' AND ')} ` +\n `ORDER BY bm25(${fts}) ASC, ${t}.\"doc_id\" ASC LIMIT ?`;\n return { sql, params: sqlParams };\n}\n\nconst DISTANCE_MEASURES: ReadonlySet<string> = new Set(['EUCLIDEAN', 'COSINE', 'DOT_PRODUCT']);\n\nexport interface CompiledVectorQuery {\n stmt: CompiledStatement;\n /**\n * `data`-relative path segments to write the computed distance into on\n * each result record, or `null` when `distanceResultField` was not set.\n */\n distancePath: string[] | null;\n}\n\n/** Resolve a `queryVector` argument to a plain `number[]`. */\nfunction toNumberArray(qv: number[] | { toArray(): number[] }): number[] {\n if (Array.isArray(qv)) return qv;\n if (typeof (qv as { toArray?: unknown }).toArray === 'function') {\n return (qv as { toArray(): number[] }).toArray();\n }\n throw new FiregraphError(\n 'findNearest(): queryVector must be a number[] or a Firestore VectorValue.',\n 'INVALID_QUERY',\n );\n}\n\n/**\n * Compile a `findNearest()` call into one SELECT that scores every\n * candidate row via the `firegraph_vector_distance` UDF.\n *\n * Shape (subquery because SQLite forbids referencing a SELECT alias in\n * the same level's WHERE):\n *\n * SELECT * FROM (\n * SELECT *, firegraph_vector_distance(json_extract(\"data\", '$.<path>'), ?, ?) AS \"__fg_distance\"\n * FROM \"<t>\" [WHERE <identifiers + where>]\n * ) WHERE \"__fg_distance\" IS NOT NULL [AND \"__fg_distance\" <=|>= ?]\n * ORDER BY \"__fg_distance\" ASC|DESC, \"doc_id\" ASC LIMIT ?\n *\n * `NULL` distances (missing field, non-array value, dimension mismatch)\n * drop out of the result, mirroring Firestore's behaviour of silently\n * skipping non-conforming documents. Threshold and ordering semantics\n * follow the `FindNearestParams.distanceThreshold` contract: `<=` /\n * ascending for EUCLIDEAN and COSINE, `>=` / descending for DOT_PRODUCT.\n *\n * Validation parity with `runFirestoreFindNearest`: envelope-field\n * rejection on both field params, non-empty query vector, positive\n * integer limit ≤ 1000.\n */\nexport function compileFindNearest(table: string, params: FindNearestParams): CompiledVectorQuery {\n const vec = toNumberArray(params.queryVector);\n if (vec.length === 0) {\n throw new FiregraphError(\n 'findNearest(): queryVector is empty — at least one dimension is required.',\n 'INVALID_QUERY',\n );\n }\n if (!Number.isInteger(params.limit) || params.limit <= 0 || params.limit > 1000) {\n throw new FiregraphError(\n `findNearest(): limit must be a positive integer ≤ 1000 (got ${params.limit}).`,\n 'INVALID_QUERY',\n );\n }\n if (!DISTANCE_MEASURES.has(params.distanceMeasure)) {\n throw new FiregraphError(\n `findNearest(): unknown distanceMeasure '${String(params.distanceMeasure)}' — ` +\n `expected EUCLIDEAN, COSINE, or DOT_PRODUCT.`,\n 'INVALID_QUERY',\n );\n }\n\n const vectorField = normalizeVectorFieldPath('vectorField', params.vectorField);\n let vectorExpr: string;\n if (vectorField === 'data') {\n vectorExpr = '\"data\"';\n } else {\n const suffix = vectorField.slice('data.'.length);\n for (const part of suffix.split('.')) {\n validateJsonPathKey(part, BACKEND_ERR_LABEL);\n }\n vectorExpr = `json_extract(\"data\", '$.${suffix}')`;\n }\n\n let distancePath: string[] | null = null;\n if (params.distanceResultField !== undefined) {\n const normalized = normalizeVectorFieldPath('distanceResultField', params.distanceResultField);\n if (normalized === 'data') {\n throw new FiregraphError(\n `findNearest(): distanceResultField 'data' would replace the entire data ` +\n `payload — use a nested path like 'data.distance'.`,\n 'INVALID_QUERY',\n );\n }\n distancePath = normalized.slice('data.'.length).split('.');\n for (const part of distancePath) {\n validateJsonPathKey(part, BACKEND_ERR_LABEL);\n }\n }\n\n // Bound-parameter order tracks placeholder order in the statement text:\n // the two UDF arguments in the SELECT list come first, then the inner\n // WHERE filters, then threshold and limit.\n const sqlParams: unknown[] = [JSON.stringify(vec), params.distanceMeasure];\n const conditions = compileFilterConditions(buildSearchFilters(params), sqlParams);\n const innerWhere = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';\n const dist = quoteIdent(DISTANCE_ALIAS);\n const descending = params.distanceMeasure === 'DOT_PRODUCT';\n\n let sql =\n `SELECT * FROM (` +\n `SELECT *, ${VECTOR_DISTANCE_UDF}(${vectorExpr}, ?, ?) AS ${dist} ` +\n `FROM ${quoteIdent(table)}${innerWhere}` +\n `) WHERE ${dist} IS NOT NULL`;\n if (params.distanceThreshold !== undefined) {\n sql += ` AND ${dist} ${descending ? '>=' : '<='} ?`;\n sqlParams.push(params.distanceThreshold);\n }\n sql += ` ORDER BY ${dist} ${descending ? 'DESC' : 'ASC'}, \"doc_id\" ASC LIMIT ?`;\n sqlParams.push(params.limit);\n\n return { stmt: { sql, params: sqlParams }, distancePath };\n}\n\n// One-entry memo for the parsed query vector: the UDF runs once per\n// candidate row with the identical query-vector JSON, so re-parsing it\n// every call would dominate the scan cost.\nlet memoQueryJson: string | null = null;\nlet memoQueryVec: number[] | null = null;\n\n/**\n * Scalar UDF body for `firegraph_vector_distance(storedJson, queryJson,\n * measure)`. Returns the distance as a REAL, or `null` when the stored\n * value is missing, not a JSON array, dimension-mismatched, or contains\n * non-finite/non-numeric entries — NULL rows are filtered out by the\n * query, mirroring Firestore's silent skip of non-conforming documents.\n *\n * COSINE returns `1 − cos(a, b)` (Firestore's distance convention) and\n * `null` when either vector has zero norm (cosine undefined).\n *\n * Exported for direct unit testing and registered on the connection by\n * `createLocalSqliteBackend` with `deterministic: true`.\n */\nexport function computeVectorDistance(\n storedJson: unknown,\n queryJson: unknown,\n measure: unknown,\n): number | null {\n if (\n typeof storedJson !== 'string' ||\n typeof queryJson !== 'string' ||\n typeof measure !== 'string'\n ) {\n return null;\n }\n let query: number[];\n if (memoQueryJson === queryJson && memoQueryVec !== null) {\n query = memoQueryVec;\n } else {\n let parsed: unknown;\n try {\n parsed = JSON.parse(queryJson);\n } catch {\n return null;\n }\n if (!Array.isArray(parsed)) return null;\n query = parsed as number[];\n memoQueryJson = queryJson;\n memoQueryVec = query;\n }\n\n let stored: unknown;\n try {\n stored = JSON.parse(storedJson);\n } catch {\n return null;\n }\n if (!Array.isArray(stored) || stored.length !== query.length) return null;\n\n let dot = 0;\n let sumSq = 0;\n let normStored = 0;\n let normQuery = 0;\n for (let i = 0; i < query.length; i++) {\n const a = stored[i];\n const b = query[i];\n if (typeof a !== 'number' || !Number.isFinite(a)) return null;\n if (typeof b !== 'number' || !Number.isFinite(b)) return null;\n dot += a * b;\n const diff = a - b;\n sumSq += diff * diff;\n normStored += a * a;\n normQuery += b * b;\n }\n\n let result: number;\n switch (measure) {\n case 'EUCLIDEAN':\n result = Math.sqrt(sumSq);\n break;\n case 'COSINE': {\n const denom = Math.sqrt(normStored) * Math.sqrt(normQuery);\n if (denom === 0) return null;\n result = 1 - dot / denom;\n break;\n }\n case 'DOT_PRODUCT':\n result = dot;\n break;\n default:\n return null;\n }\n return Number.isFinite(result) ? result : null;\n}\n\n/**\n * Set a nested value inside a record's `data` payload, creating\n * intermediate objects along the way (replacing non-object intermediates,\n * matching Firestore's `distanceResultField` write semantics).\n */\nexport function setDataPath(\n data: Record<string, unknown>,\n path: ReadonlyArray<string>,\n value: unknown,\n): void {\n let cursor = data;\n for (let i = 0; i < path.length - 1; i++) {\n const key = path[i];\n const next = cursor[key];\n if (typeof next !== 'object' || next === null || Array.isArray(next)) {\n const created: Record<string, unknown> = {};\n cursor[key] = created;\n cursor = created;\n } else {\n cursor = next as Record<string, unknown>;\n }\n }\n cursor[path[path.length - 1]] = value;\n}\n\n/**\n * Identify orphaned FTS artifacts (`<t>_fts` / `<t>_fts_map`) whose base\n * graph table no longer exists — left behind when a parent cascade DROPs a\n * descendant subgraph table (triggers die with the table; the FTS\n * artifacts do not).\n *\n * Safety against false positives: only names under the subgraph prefix\n * (`<rootTable>_g_`) are considered, a candidate must NOT itself be a\n * registered graph table (`catalogTables` — covers a real graph whose\n * mangled scope happens to end in `_fts`), and its base table must be\n * absent from `allTables`. FTS5 shadow tables (`<t>_fts_data`,\n * `<t>_fts_idx`, …) never match the suffix patterns and are dropped\n * implicitly with their parent virtual table.\n */\nexport function findOrphanedFtsTables(\n allTables: ReadonlyArray<string>,\n catalogTables: ReadonlyArray<string>,\n rootTable: string,\n): string[] {\n const names = new Set(allTables);\n const liveGraphTables = new Set(catalogTables);\n const subgraphPrefix = `${rootTable}_g_`;\n const orphans: string[] = [];\n for (const name of names) {\n let base: string | null = null;\n if (name.endsWith('_fts_map')) base = name.slice(0, -'_fts_map'.length);\n else if (name.endsWith('_fts')) base = name.slice(0, -'_fts'.length);\n if (base === null || !base.startsWith(subgraphPrefix)) continue;\n if (liveGraphTables.has(name)) continue;\n if (names.has(base)) continue;\n orphans.push(name);\n }\n return orphans.sort();\n}\n","/**\n * Local SQLite backend over `better-sqlite3`.\n *\n * This entry point is published as `firegraph/sqlite-local` and is the only\n * module in the library that references `better-sqlite3` — keep it out of\n * `firegraph/sqlite` so that D1 / workerd bundles never see the native\n * dependency. `better-sqlite3` is loaded via dynamic `import()` at factory\n * call time, so merely importing this module stays side-effect free.\n *\n * The factory accepts either a database file path (`':memory:'` works) or an\n * already-open `better-sqlite3` Database. Path-opened databases get\n * `journal_mode = WAL` and a `busy_timeout` applied; caller-provided\n * databases are used as-is (only `busy_timeout` is set) since the caller\n * owns their pragma configuration.\n *\n * ## Search capabilities\n *\n * On top of the shared SQLite capability set, the local backend declares\n * `search.fullText` and `search.vector` (see `src/internal/sqlite-search.ts`\n * for the mechanics):\n *\n * - **Full-text search** is backed by one FTS5 table per graph table,\n * kept in sync by pure-SQL triggers installed with the table's DDL.\n * Because the triggers live in the database file, writes from ANY\n * process or connection stay indexed. The trade-off is a per-write\n * overhead (text extraction via `json_tree` + an FTS index update) on\n * every insert/update/delete.\n * - **Vector search** is a brute-force scan scored by a deterministic\n * scalar UDF registered on this connection. UDFs are connection-local:\n * `findNearest` only works through a backend created by this factory\n * (other connections to the same file can read/write normally — only\n * vector *search* needs the UDF).\n */\n\nimport type { Database as BetterSqliteDb, default as BetterSqliteDatabase } from 'better-sqlite3';\n\nimport { FiregraphError } from '../errors.js';\nimport type { StorageBackend } from '../internal/backend.js';\nimport { createCapabilities } from '../internal/backend.js';\nimport type { SqliteExecutor, SqliteTxExecutor } from '../internal/sqlite-executor.js';\nimport { quoteIdent, validateTableName } from '../internal/sqlite-schema.js';\nimport {\n buildLocalSearchDDL,\n compileFindNearest,\n compileFullTextSearch,\n computeVectorDistance,\n DISTANCE_ALIAS,\n findOrphanedFtsTables,\n ftsMapTableName,\n ftsTableName,\n setDataPath,\n VECTOR_DISTANCE_UDF,\n} from '../internal/sqlite-search.js';\nimport { rowToRecord } from '../internal/sqlite-sql.js';\nimport type { FindNearestParams, FullTextSearchParams, StoredGraphRecord } from '../types.js';\nimport type { SqliteBackendOptions, SqliteCapability, SqliteStorageBackend } from './backend.js';\nimport { createSqliteBackend } from './backend.js';\nimport { catalogTableName } from './catalog.js';\n\n/**\n * Capability union for the local better-sqlite3 backend: everything the\n * shared SQLite edition declares, plus native FTS5 full-text search and\n * brute-force vector search. `search.geo` stays out — there is no geo\n * index in stock SQLite, and a UDF-scored scan without a haversine\n * contract pinned by Firestore parity tests would be guesswork.\n */\nexport type LocalSqliteCapability = SqliteCapability | 'search.fullText' | 'search.vector';\n\nexport interface LocalSqliteBackendOptions extends SqliteBackendOptions {\n /** Root graph table name. Defaults to `'firegraph'`. */\n tableName?: string;\n /**\n * `PRAGMA busy_timeout` in milliseconds — how long a connection waits on a\n * lock held by another process before erroring. Defaults to 5000.\n */\n busyTimeoutMs?: number;\n /**\n * Extra pragmas applied after the defaults, e.g.\n * `{ synchronous: 'NORMAL', cache_size: -64000 }`. Applied in object\n * order via `PRAGMA <key> = <value>`.\n */\n pragmas?: Record<string, string | number>;\n /**\n * When opening by path: throw if the file does not already exist instead\n * of creating it. Defaults to false.\n */\n fileMustExist?: boolean;\n}\n\nexport interface LocalSqliteBackend {\n /** The graph storage backend — pass to `createGraphClient`. */\n backend: StorageBackend<LocalSqliteCapability>;\n /** The underlying better-sqlite3 database, for raw access. */\n db: BetterSqliteDb;\n /**\n * Close the database. No-op when the factory was given an already-open\n * Database (the caller owns its lifecycle).\n */\n close(): void;\n}\n\n/**\n * Build a transaction-capable `SqliteExecutor` over a better-sqlite3\n * Database. Interactive transactions use manual `BEGIN IMMEDIATE` /\n * `COMMIT` / `ROLLBACK` because `db.transaction()` requires a synchronous\n * callback while `SqliteExecutor.transaction` callbacks are async.\n *\n * Exported for callers that want to wire `createSqliteBackend` directly\n * (e.g. to share one executor across several root tables).\n */\nexport function createBetterSqliteExecutor(db: BetterSqliteDb): SqliteExecutor {\n return {\n async all(sql: string, params: unknown[]): Promise<Record<string, unknown>[]> {\n return db.prepare(sql).all(...params) as Record<string, unknown>[];\n },\n async run(sql: string, params: unknown[]): Promise<void> {\n db.prepare(sql).run(...params);\n },\n async batch(statements): Promise<void> {\n const tx = db.transaction((stmts: typeof statements) => {\n for (const s of stmts) {\n db.prepare(s.sql).run(...s.params);\n }\n });\n tx(statements);\n },\n async transaction<T>(fn: (tx: SqliteTxExecutor) => Promise<T>): Promise<T> {\n db.exec('BEGIN IMMEDIATE');\n try {\n const result = await fn({\n async all(sql: string, params: unknown[]) {\n return db.prepare(sql).all(...params) as Record<string, unknown>[];\n },\n async run(sql: string, params: unknown[]) {\n db.prepare(sql).run(...params);\n },\n });\n db.exec('COMMIT');\n return result;\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n },\n };\n}\n\nfunction isDatabase(value: unknown): value is BetterSqliteDb {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { prepare?: unknown }).prepare === 'function' &&\n typeof (value as { exec?: unknown }).exec === 'function'\n );\n}\n\nconst PRAGMA_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;\n// Pragma values are identifiers (WAL, NORMAL) or integers — never compound\n// expressions, so anything else is rejected rather than interpolated.\nconst PRAGMA_VALUE_PATTERN = /^-?[A-Za-z0-9_]+$/;\n\nfunction applyPragmas(db: BetterSqliteDb, pragmas: Record<string, string | number>): void {\n for (const [key, value] of Object.entries(pragmas)) {\n if (!PRAGMA_KEY_PATTERN.test(key)) {\n throw new FiregraphError(`Invalid pragma name: ${JSON.stringify(key)}`, 'INVALID_ARGUMENT');\n }\n if (\n !PRAGMA_VALUE_PATTERN.test(String(value)) ||\n (typeof value === 'number' && !Number.isFinite(value))\n ) {\n throw new FiregraphError(\n `Invalid pragma value for ${key}: ${JSON.stringify(value)}`,\n 'INVALID_ARGUMENT',\n );\n }\n db.pragma(`${key} = ${value}`);\n }\n}\n\n/**\n * Register the vector-distance UDF on a connection. Idempotent across\n * multiple factory calls over the same caller-provided Database —\n * better-sqlite3 raises on duplicate registration, which we swallow since\n * re-registering the identical pure function changes nothing.\n */\nfunction registerVectorUdf(db: BetterSqliteDb): void {\n try {\n db.function(VECTOR_DISTANCE_UDF, { deterministic: true }, (stored, query, measure) =>\n computeVectorDistance(stored, query, measure),\n );\n } catch {\n // Already registered on this connection.\n }\n}\n\n/**\n * After a cascade DROPs descendant graph tables, their FTS artifacts\n * (`<t>_fts`, `<t>_fts_map`) survive — triggers die with the base table\n * but separate tables do not. Sweep and drop any artifact whose base\n * graph table is gone. Stale rows in a *recreated* subgraph are handled\n * independently by the bootstrap reconciliation pass\n * (`buildFtsSyncStatements`); this sweep is what reclaims the space for\n * graphs that never come back.\n */\nasync function sweepOrphanedFtsArtifacts(\n executor: SqliteExecutor,\n rootTable: string,\n): Promise<void> {\n const tableRows = await executor.all(\n `SELECT \"name\" FROM sqlite_master WHERE \"type\" = 'table'`,\n [],\n );\n const allTables = tableRows.map((r) => String(r.name));\n const catalogRows = await executor.all(\n `SELECT \"table_name\" FROM ${quoteIdent(catalogTableName(rootTable))}`,\n [],\n );\n const catalogTables = catalogRows.map((r) => String(r.table_name));\n for (const name of findOrphanedFtsTables(allTables, catalogTables, rootTable)) {\n validateTableName(name);\n await executor.run(`DROP TABLE IF EXISTS ${quoteIdent(name)}`, []);\n }\n}\n\n/**\n * Wrap the shared SQLite backend with the two search capabilities. Every\n * core method delegates to the inner backend unchanged; `subgraph()`\n * re-wraps so children search too, and `removeNodeCascade` follows the\n * inner cascade with the orphaned-FTS sweep.\n */\nfunction wrapLocalSearchBackend(\n inner: SqliteStorageBackend,\n executor: SqliteExecutor,\n rootTable: string,\n): StorageBackend<LocalSqliteCapability> {\n const caps = new Set<LocalSqliteCapability>([\n ...(inner.capabilities.values() as IterableIterator<SqliteCapability>),\n 'search.fullText',\n 'search.vector',\n ]);\n\n // Same self-heal contract as SqliteBackendImpl.withSchema: a stale handle\n // whose table — or whose FTS artifacts, which bootstrap alongside it — was\n // dropped by a parent cascade recreates the empty graph and retries once.\n // The missing table name is matched exactly (not by prefix) so an unrelated\n // table that merely shares the prefix never triggers a re-bootstrap.\n const healableTables = new Set([\n inner.collectionPath,\n ftsTableName(inner.collectionPath),\n ftsMapTableName(inner.collectionPath),\n ]);\n const runWithSchema = async <T>(op: () => Promise<T>): Promise<T> => {\n await inner.ensureReady();\n try {\n return await op();\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const missing = /no such table: (\\S+)/.exec(message)?.[1];\n if (missing === undefined || !healableTables.has(missing)) throw err;\n await inner.ensureReady(true);\n return op();\n }\n };\n\n const wrapper: StorageBackend<LocalSqliteCapability> = {\n capabilities: createCapabilities(caps),\n collectionPath: inner.collectionPath,\n scopePath: inner.scopePath,\n\n getDoc: (docId) => inner.getDoc(docId),\n query: (filters, options) => inner.query(filters, options),\n setDoc: (docId, record, mode) => inner.setDoc(docId, record, mode),\n updateDoc: (docId, update) => inner.updateDoc(docId, update),\n deleteDoc: (docId) => inner.deleteDoc(docId),\n runTransaction: (fn) => inner.runTransaction(fn),\n createBatch: () => inner.createBatch(),\n\n subgraph: (parentNodeUid, name) =>\n wrapLocalSearchBackend(inner.subgraph(parentNodeUid, name), executor, rootTable),\n\n removeNodeCascade: async (uid, reader, options) => {\n const result = await inner.removeNodeCascade(uid, reader, options);\n if (result.errors.length === 0) {\n await sweepOrphanedFtsArtifacts(executor, rootTable);\n }\n return result;\n },\n bulkRemoveEdges: (params, reader, options) => inner.bulkRemoveEdges(params, reader, options),\n\n aggregate: (spec, filters) => inner.aggregate!(spec, filters),\n bulkDelete: (filters, options) => inner.bulkDelete!(filters, options),\n bulkUpdate: (filters, patch, options) => inner.bulkUpdate!(filters, patch, options),\n expand: (params) => inner.expand!(params),\n findEdgesProjected: (select, filters, options) =>\n inner.findEdgesProjected!(select, filters, options),\n\n // `findEdgesGlobal` stays absent, same as the inner backend — each graph\n // is its own table; there is no cross-table index.\n\n async findNearest(params: FindNearestParams): Promise<StoredGraphRecord[]> {\n const { stmt, distancePath } = compileFindNearest(inner.collectionPath, params);\n const rows = await runWithSchema(() => executor.all(stmt.sql, stmt.params));\n return rows.map((row) => {\n const record = rowToRecord(row);\n if (distancePath) {\n const distance = row[DISTANCE_ALIAS];\n setDataPath(\n record.data as Record<string, unknown>,\n distancePath,\n typeof distance === 'number' ? distance : Number(distance),\n );\n }\n return record;\n });\n },\n\n async fullTextSearch(params: FullTextSearchParams): Promise<StoredGraphRecord[]> {\n const stmt = compileFullTextSearch(inner.collectionPath, params);\n let rows: Record<string, unknown>[];\n try {\n rows = await runWithSchema(() => executor.all(stmt.sql, stmt.params));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // FTS5 reports malformed MATCH expressions at query time; surface\n // them as INVALID_QUERY rather than a raw driver error.\n if (message.includes('fts5') || message.includes('unknown special query')) {\n throw new FiregraphError(\n `fullTextSearch(): invalid FTS5 query syntax — ${message}`,\n 'INVALID_QUERY',\n );\n }\n throw err;\n }\n return rows.map(rowToRecord);\n },\n };\n return wrapper;\n}\n\n/**\n * Open (or wrap) a local SQLite database and return a graph storage backend\n * over it.\n *\n * ```typescript\n * import { createLocalSqliteBackend } from 'firegraph/sqlite-local';\n * import { createGraphClient } from 'firegraph/sqlite';\n *\n * const { backend, close } = await createLocalSqliteBackend('./graph.db');\n * const client = createGraphClient(backend);\n * // ... use the client — including fullTextSearch() and findNearest() ...\n * close();\n * ```\n *\n * Requires `better-sqlite3` to be installed (declared as an optional peer\n * dependency). The factory is async because the driver is loaded via\n * dynamic `import()`.\n */\nexport async function createLocalSqliteBackend(\n pathOrDb: string | BetterSqliteDb,\n options: LocalSqliteBackendOptions = {},\n): Promise<LocalSqliteBackend> {\n const {\n tableName = 'firegraph',\n busyTimeoutMs = 5000,\n pragmas,\n fileMustExist,\n ...backendOptions\n } = options;\n\n let db: BetterSqliteDb;\n let ownsDb: boolean;\n if (typeof pathOrDb === 'string') {\n let Database: typeof BetterSqliteDatabase;\n try {\n Database = (await import('better-sqlite3')).default;\n } catch (err) {\n throw new FiregraphError(\n `createLocalSqliteBackend requires the optional peer dependency 'better-sqlite3' — install it to use the local SQLite backend (${\n err instanceof Error ? err.message : String(err)\n })`,\n 'MISSING_DEPENDENCY',\n );\n }\n db = new Database(pathOrDb, fileMustExist ? { fileMustExist: true } : {});\n ownsDb = true;\n // WAL lets concurrent readers coexist with a writer — the right default\n // for a long-lived local graph file. On ':memory:' databases SQLite\n // reports 'memory' and ignores the request, which is fine.\n db.pragma('journal_mode = WAL');\n } else if (isDatabase(pathOrDb)) {\n db = pathOrDb;\n ownsDb = false;\n } else {\n throw new FiregraphError(\n 'createLocalSqliteBackend expects a file path or an open better-sqlite3 Database',\n 'INVALID_ARGUMENT',\n );\n }\n\n db.pragma(`busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);\n if (pragmas) {\n applyPragmas(db, pragmas);\n }\n registerVectorUdf(db);\n\n // Compose the FTS DDL into the lazy bootstrap so every graph table —\n // root, lazily created subgraphs, and self-heal recreations — gets its\n // FTS infrastructure the moment the table exists.\n const userExtraDDL = backendOptions.extraTableDDL;\n const optionsWithSearch: SqliteBackendOptions = {\n ...backendOptions,\n extraTableDDL: (table) => [\n ...(userExtraDDL ? userExtraDDL(table) : []),\n ...buildLocalSearchDDL(table),\n ],\n };\n\n const executor = createBetterSqliteExecutor(db);\n const inner = createSqliteBackend(executor, tableName, optionsWithSearch);\n const backend = wrapLocalSearchBackend(inner, executor, tableName);\n let closed = false;\n return {\n backend,\n db,\n close(): void {\n if (closed || !ownsDb) return;\n closed = true;\n db.close();\n },\n };\n}\n"],"mappings":"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","names":[]}