@typicalday/firegraph 0.14.0 → 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 (74) 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.cjs +2 -3
  7. package/dist/backend.cjs.map +1 -1
  8. package/dist/backend.d.cts +2 -2
  9. package/dist/backend.d.ts +2 -2
  10. package/dist/backend.js +1 -1
  11. package/dist/{chunk-WRTFC5NG.js → chunk-5HIRYV2S.js} +13 -36
  12. package/dist/chunk-5HIRYV2S.js.map +1 -0
  13. package/dist/{chunk-PAD7WFFU.js → chunk-7IEZ6IYY.js} +36 -10
  14. package/dist/chunk-7IEZ6IYY.js.map +1 -0
  15. package/dist/chunk-FODIMIWY.js +721 -0
  16. package/dist/chunk-FODIMIWY.js.map +1 -0
  17. package/dist/chunk-NGAJCALM.js +34 -0
  18. package/dist/chunk-NGAJCALM.js.map +1 -0
  19. package/dist/{chunk-TK64DNVK.js → chunk-SIHE4UY4.js} +3 -4
  20. package/dist/chunk-SIHE4UY4.js.map +1 -0
  21. package/dist/chunk-ULRDQ6HZ.js +862 -0
  22. package/dist/chunk-ULRDQ6HZ.js.map +1 -0
  23. package/dist/{client-BKi3vk0Q.d.ts → client-B5o39X79.d.ts} +1 -1
  24. package/dist/{client-BrsaXtDV.d.cts → client-BGHwxwPg.d.cts} +1 -1
  25. package/dist/{client-Bk2Cm6xv.d.cts → client-DoyEdJ5w.d.cts} +1 -1
  26. package/dist/{client-Bk2Cm6xv.d.ts → client-DoyEdJ5w.d.ts} +1 -1
  27. package/dist/cloudflare/index.cjs +155 -165
  28. package/dist/cloudflare/index.cjs.map +1 -1
  29. package/dist/cloudflare/index.d.cts +73 -70
  30. package/dist/cloudflare/index.d.ts +73 -70
  31. package/dist/cloudflare/index.js +54 -589
  32. package/dist/cloudflare/index.js.map +1 -1
  33. package/dist/codegen/index.d.cts +1 -1
  34. package/dist/codegen/index.d.ts +1 -1
  35. package/dist/firestore-enterprise/index.cjs +42 -40
  36. package/dist/firestore-enterprise/index.cjs.map +1 -1
  37. package/dist/firestore-enterprise/index.d.cts +3 -3
  38. package/dist/firestore-enterprise/index.d.ts +3 -3
  39. package/dist/firestore-enterprise/index.js +19 -35
  40. package/dist/firestore-enterprise/index.js.map +1 -1
  41. package/dist/firestore-standard/index.cjs +34 -37
  42. package/dist/firestore-standard/index.cjs.map +1 -1
  43. package/dist/firestore-standard/index.d.cts +3 -3
  44. package/dist/firestore-standard/index.d.ts +3 -3
  45. package/dist/firestore-standard/index.js +10 -34
  46. package/dist/firestore-standard/index.js.map +1 -1
  47. package/dist/index.cjs +2 -3
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +5 -5
  50. package/dist/index.d.ts +5 -5
  51. package/dist/index.js +7 -5
  52. package/dist/index.js.map +1 -1
  53. package/dist/query-client/index.d.cts +2 -2
  54. package/dist/query-client/index.d.ts +2 -2
  55. package/dist/{registry-Bc7h6WTM.d.cts → registry-BGh7Jqpb.d.cts} +2 -2
  56. package/dist/{registry-C2KUPVZj.d.ts → registry-tKTb5Kx1.d.ts} +2 -2
  57. package/dist/sqlite/index.cjs +585 -378
  58. package/dist/sqlite/index.cjs.map +1 -1
  59. package/dist/sqlite/index.d.cts +4 -110
  60. package/dist/sqlite/index.d.ts +4 -110
  61. package/dist/sqlite/index.js +7 -1144
  62. package/dist/sqlite/index.js.map +1 -1
  63. package/dist/sqlite/local.cjs +1835 -0
  64. package/dist/sqlite/local.cjs.map +1 -0
  65. package/dist/sqlite/local.d.cts +83 -0
  66. package/dist/sqlite/local.d.ts +83 -0
  67. package/dist/sqlite/local.js +121 -0
  68. package/dist/sqlite/local.js.map +1 -0
  69. package/package.json +15 -1
  70. package/dist/chunk-4MMQ5W74.js +0 -288
  71. package/dist/chunk-4MMQ5W74.js.map +0 -1
  72. package/dist/chunk-PAD7WFFU.js.map +0 -1
  73. package/dist/chunk-TK64DNVK.js.map +0 -1
  74. package/dist/chunk-WRTFC5NG.js.map +0 -1
@@ -235,7 +235,6 @@ function isTerminalValue(value) {
235
235
  if (ctor && typeof ctor.name === "string" && FIRESTORE_TERMINAL_CTOR.has(ctor.name)) return true;
236
236
  return true;
237
237
  }
238
- var SAFE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
239
238
  function assertUpdatePayloadExclusive(update) {
240
239
  if (update.replaceData !== void 0 && update.dataOps !== void 0) {
241
240
  throw new Error(
@@ -274,9 +273,9 @@ function walkForDeleteSentinels(node, path, parent, visit) {
274
273
  }
275
274
  function assertSafePath(path) {
276
275
  for (const seg of path) {
277
- if (!SAFE_KEY_RE.test(seg)) {
276
+ if (seg === "") {
278
277
  throw new Error(
279
- `firegraph: unsafe object key ${JSON.stringify(seg)} at path ${path.map((p) => JSON.stringify(p)).join(" > ")}. Keys used inside update payloads must match /^[A-Za-z_][A-Za-z0-9_-]*$/ so they can be embedded safely in SQLite JSON paths.`
278
+ `firegraph: empty object key at path ${path.map((p) => JSON.stringify(p)).join(" > ")}. Object keys in update payloads must be non-empty.`
280
279
  );
281
280
  }
282
281
  }
@@ -2309,6 +2308,186 @@ function createCapabilities(caps) {
2309
2308
  };
2310
2309
  }
2311
2310
 
2311
+ // src/default-indexes.ts
2312
+ var DEFAULT_CORE_INDEXES = Object.freeze([
2313
+ { fields: ["aUid"] },
2314
+ { fields: ["bUid"] },
2315
+ { fields: ["aType"] },
2316
+ { fields: ["bType"] },
2317
+ { fields: ["aUid", "axbType"] },
2318
+ { fields: ["axbType", "bUid"] },
2319
+ { fields: ["aType", "axbType"] },
2320
+ { fields: ["axbType", "bType"] }
2321
+ ]);
2322
+
2323
+ // src/internal/sqlite-index-ddl.ts
2324
+ var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2325
+ var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
2326
+ function quoteIdent(name) {
2327
+ if (!IDENT_RE.test(name)) {
2328
+ throw new FiregraphError(
2329
+ `Invalid SQL identifier in index DDL: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
2330
+ "INVALID_INDEX"
2331
+ );
2332
+ }
2333
+ return `"${name}"`;
2334
+ }
2335
+ function fnv1a32(str) {
2336
+ let h = 2166136261;
2337
+ for (let i = 0; i < str.length; i++) {
2338
+ h ^= str.charCodeAt(i);
2339
+ h = Math.imul(h, 16777619);
2340
+ }
2341
+ return (h >>> 0).toString(16).padStart(8, "0");
2342
+ }
2343
+ function normalizeFields(fields) {
2344
+ return fields.map((f) => {
2345
+ if (typeof f === "string") return { path: f, desc: false };
2346
+ if (!f.path || typeof f.path !== "string") {
2347
+ throw new FiregraphError(
2348
+ `IndexSpec field must be a string or { path: string, desc?: boolean }; got ${JSON.stringify(f)}`,
2349
+ "INVALID_INDEX"
2350
+ );
2351
+ }
2352
+ return { path: f.path, desc: !!f.desc };
2353
+ });
2354
+ }
2355
+ function specFingerprint(spec) {
2356
+ const normalized = {
2357
+ lead: [],
2358
+ fields: normalizeFields(spec.fields),
2359
+ where: spec.where ?? ""
2360
+ };
2361
+ return fnv1a32(JSON.stringify(normalized));
2362
+ }
2363
+ function compileFieldExpr(path, fieldToColumn) {
2364
+ const col = fieldToColumn[path];
2365
+ if (col) return quoteIdent(col);
2366
+ if (path === "data") {
2367
+ return `json_extract("data", '$')`;
2368
+ }
2369
+ if (path.startsWith("data.")) {
2370
+ const suffix = path.slice(5);
2371
+ const parts = suffix.split(".");
2372
+ for (const part of parts) {
2373
+ if (!JSON_PATH_KEY_RE.test(part)) {
2374
+ throw new FiregraphError(
2375
+ `IndexSpec data path "${path}" has invalid component "${part}". Each component must match /^[A-Za-z_][A-Za-z0-9_-]*$/.`,
2376
+ "INVALID_INDEX"
2377
+ );
2378
+ }
2379
+ }
2380
+ return `json_extract("data", '$.${suffix}')`;
2381
+ }
2382
+ throw new FiregraphError(
2383
+ `IndexSpec field "${path}" is not a known firegraph field. Use a top-level field (aType, aUid, axbType, bType, bUid, createdAt, updatedAt, v) or a dotted data path like 'data.status'.`,
2384
+ "INVALID_INDEX"
2385
+ );
2386
+ }
2387
+ function buildIndexDDL(spec, options) {
2388
+ const { table, fieldToColumn } = options;
2389
+ if (!spec.fields || spec.fields.length === 0) {
2390
+ throw new FiregraphError("IndexSpec.fields must be a non-empty array", "INVALID_INDEX");
2391
+ }
2392
+ const normalized = normalizeFields(spec.fields);
2393
+ const hash = specFingerprint(spec);
2394
+ const indexName = `${table}_idx_${hash}`;
2395
+ const cols = [];
2396
+ for (const f of normalized) {
2397
+ const expr = compileFieldExpr(f.path, fieldToColumn);
2398
+ cols.push(f.desc ? `${expr} DESC` : expr);
2399
+ }
2400
+ let ddl = `CREATE INDEX IF NOT EXISTS ${quoteIdent(indexName)} ON ${quoteIdent(table)}(${cols.join(", ")})`;
2401
+ if (spec.where) {
2402
+ ddl += ` WHERE ${spec.where}`;
2403
+ }
2404
+ return ddl;
2405
+ }
2406
+ function dedupeIndexSpecs(specs) {
2407
+ const seen = /* @__PURE__ */ new Set();
2408
+ const out = [];
2409
+ for (const spec of specs) {
2410
+ const fp = specFingerprint(spec);
2411
+ if (seen.has(fp)) continue;
2412
+ seen.add(fp);
2413
+ out.push(spec);
2414
+ }
2415
+ return out;
2416
+ }
2417
+
2418
+ // src/internal/sqlite-schema.ts
2419
+ var FIELD_TO_COLUMN = {
2420
+ aType: "a_type",
2421
+ aUid: "a_uid",
2422
+ axbType: "axb_type",
2423
+ bType: "b_type",
2424
+ bUid: "b_uid",
2425
+ v: "v",
2426
+ createdAt: "created_at",
2427
+ updatedAt: "updated_at"
2428
+ };
2429
+ function buildSchemaStatements(table, options = {}) {
2430
+ const t = quoteIdent2(table);
2431
+ const statements = [
2432
+ `CREATE TABLE IF NOT EXISTS ${t} (
2433
+ doc_id TEXT NOT NULL PRIMARY KEY,
2434
+ a_type TEXT NOT NULL,
2435
+ a_uid TEXT NOT NULL,
2436
+ axb_type TEXT NOT NULL,
2437
+ b_type TEXT NOT NULL,
2438
+ b_uid TEXT NOT NULL,
2439
+ data TEXT NOT NULL,
2440
+ v INTEGER,
2441
+ created_at INTEGER NOT NULL,
2442
+ updated_at INTEGER NOT NULL
2443
+ )`
2444
+ ];
2445
+ const core = options.coreIndexes ?? [...DEFAULT_CORE_INDEXES];
2446
+ const fromRegistry = options.registry?.entries().flatMap((e) => e.indexes ?? []) ?? [];
2447
+ const deduped = dedupeIndexSpecs([...core, ...fromRegistry]);
2448
+ for (const spec of deduped) {
2449
+ statements.push(buildIndexDDL(spec, { table, fieldToColumn: FIELD_TO_COLUMN }));
2450
+ }
2451
+ return statements;
2452
+ }
2453
+ function quoteIdent2(name) {
2454
+ validateTableName(name);
2455
+ return `"${name}"`;
2456
+ }
2457
+ function validateTableName(name) {
2458
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2459
+ throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
2460
+ }
2461
+ }
2462
+ function quoteColumnAlias(label) {
2463
+ return `"${label.replace(/"/g, '""')}"`;
2464
+ }
2465
+
2466
+ // src/timestamp.ts
2467
+ var GraphTimestampImpl = class _GraphTimestampImpl {
2468
+ constructor(seconds, nanoseconds) {
2469
+ this.seconds = seconds;
2470
+ this.nanoseconds = nanoseconds;
2471
+ }
2472
+ toDate() {
2473
+ return new Date(this.toMillis());
2474
+ }
2475
+ toMillis() {
2476
+ return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
2477
+ }
2478
+ toJSON() {
2479
+ return { seconds: this.seconds, nanoseconds: this.nanoseconds };
2480
+ }
2481
+ static fromMillis(ms) {
2482
+ const seconds = Math.floor(ms / 1e3);
2483
+ const nanoseconds = (ms - seconds * 1e3) * 1e6;
2484
+ return new _GraphTimestampImpl(seconds, nanoseconds);
2485
+ }
2486
+ static now() {
2487
+ return _GraphTimestampImpl.fromMillis(Date.now());
2488
+ }
2489
+ };
2490
+
2312
2491
  // src/internal/sqlite-data-ops.ts
2313
2492
  var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
2314
2493
  "Timestamp",
@@ -2322,7 +2501,7 @@ function isFirestoreSpecialType(value) {
2322
2501
  if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
2323
2502
  return null;
2324
2503
  }
2325
- var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
2504
+ var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
2326
2505
  function validateJsonPathKey(key, backendLabel) {
2327
2506
  if (key.length === 0) {
2328
2507
  throw new FiregraphError(
@@ -2330,13 +2509,16 @@ function validateJsonPathKey(key, backendLabel) {
2330
2509
  "INVALID_QUERY"
2331
2510
  );
2332
2511
  }
2333
- if (!JSON_PATH_KEY_RE.test(key)) {
2512
+ if (!JSON_PATH_KEY_RE2.test(key)) {
2334
2513
  throw new FiregraphError(
2335
2514
  `${backendLabel}: data field path component "${key}" is not a safe JSON-path identifier. Allowed pattern: /^[A-Za-z_][A-Za-z0-9_-]*$/. Use replaceNode/replaceEdge (full-data overwrite) for keys with reserved characters (whitespace, dots, brackets, quotes, etc.).`,
2336
2515
  "INVALID_QUERY"
2337
2516
  );
2338
2517
  }
2339
2518
  }
2519
+ function buildJsonPath(segments) {
2520
+ return "$" + segments.map((seg) => "." + JSON.stringify(seg)).join("");
2521
+ }
2340
2522
  function jsonBind(value, backendLabel) {
2341
2523
  if (value === void 0) return "null";
2342
2524
  if (value !== null && typeof value === "object") {
@@ -2360,16 +2542,14 @@ function compileDataOpsExpr(ops, base, params, backendLabel) {
2360
2542
  const placeholders = deletes.map(() => "?").join(", ");
2361
2543
  expr = `json_remove(${expr}, ${placeholders})`;
2362
2544
  for (const op of deletes) {
2363
- for (const seg of op.path) validateJsonPathKey(seg, backendLabel);
2364
- params.push(`$.${op.path.join(".")}`);
2545
+ params.push(buildJsonPath(op.path));
2365
2546
  }
2366
2547
  }
2367
2548
  if (sets.length > 0) {
2368
2549
  const pieces = sets.map(() => "?, json(?)").join(", ");
2369
2550
  expr = `json_set(${expr}, ${pieces})`;
2370
2551
  for (const op of sets) {
2371
- for (const seg of op.path) validateJsonPathKey(seg, backendLabel);
2372
- params.push(`$.${op.path.join(".")}`);
2552
+ params.push(buildJsonPath(op.path));
2373
2553
  params.push(jsonBind(op.value, backendLabel));
2374
2554
  }
2375
2555
  }
@@ -2457,79 +2637,18 @@ function formatTagValue(value) {
2457
2637
  return typeof value;
2458
2638
  }
2459
2639
 
2460
- // src/default-indexes.ts
2461
- var DEFAULT_CORE_INDEXES = Object.freeze([
2462
- { fields: ["aUid"] },
2463
- { fields: ["bUid"] },
2464
- { fields: ["aType"] },
2465
- { fields: ["bType"] },
2466
- { fields: ["aUid", "axbType"] },
2467
- { fields: ["axbType", "bUid"] },
2468
- { fields: ["aType", "axbType"] },
2469
- { fields: ["axbType", "bType"] }
2470
- ]);
2471
-
2472
- // src/internal/sqlite-schema.ts
2473
- var FIELD_TO_COLUMN = {
2474
- aType: "a_type",
2475
- aUid: "a_uid",
2476
- axbType: "axb_type",
2477
- bType: "b_type",
2478
- bUid: "b_uid",
2479
- v: "v",
2480
- createdAt: "created_at",
2481
- updatedAt: "updated_at"
2482
- };
2483
- function quoteIdent(name) {
2484
- validateTableName(name);
2485
- return `"${name}"`;
2486
- }
2487
- function validateTableName(name) {
2488
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2489
- throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
2490
- }
2491
- }
2492
- function quoteColumnAlias(label) {
2493
- return `"${label.replace(/"/g, '""')}"`;
2494
- }
2495
-
2496
- // src/timestamp.ts
2497
- var GraphTimestampImpl = class _GraphTimestampImpl {
2498
- constructor(seconds, nanoseconds) {
2499
- this.seconds = seconds;
2500
- this.nanoseconds = nanoseconds;
2501
- }
2502
- toDate() {
2503
- return new Date(this.toMillis());
2504
- }
2505
- toMillis() {
2506
- return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
2507
- }
2508
- toJSON() {
2509
- return { seconds: this.seconds, nanoseconds: this.nanoseconds };
2510
- }
2511
- static fromMillis(ms) {
2512
- const seconds = Math.floor(ms / 1e3);
2513
- const nanoseconds = (ms - seconds * 1e3) * 1e6;
2514
- return new _GraphTimestampImpl(seconds, nanoseconds);
2515
- }
2516
- static now() {
2517
- return _GraphTimestampImpl.fromMillis(Date.now());
2518
- }
2519
- };
2520
-
2521
- // src/sqlite/sql.ts
2522
- var SQLITE_BACKEND_LABEL = "shared-table SQLite";
2523
- var SQLITE_BACKEND_ERR_LABEL = "SQLite backend";
2640
+ // src/internal/sqlite-sql.ts
2641
+ var BACKEND_LABEL = "SQLite";
2642
+ var BACKEND_ERR_LABEL = "SQLite backend";
2524
2643
  function compileFieldRef(field) {
2525
2644
  const column = FIELD_TO_COLUMN[field];
2526
2645
  if (column) {
2527
- return { expr: quoteIdent(column) };
2646
+ return { expr: quoteIdent2(column) };
2528
2647
  }
2529
2648
  if (field.startsWith("data.")) {
2530
2649
  const suffix = field.slice(5);
2531
2650
  for (const part of suffix.split(".")) {
2532
- validateJsonPathKey(part, SQLITE_BACKEND_ERR_LABEL);
2651
+ validateJsonPathKey(part, BACKEND_ERR_LABEL);
2533
2652
  }
2534
2653
  return { expr: `json_extract("data", '$.${suffix}')` };
2535
2654
  }
@@ -2624,95 +2743,81 @@ function compileLimit(options, params) {
2624
2743
  params.push(options.limit);
2625
2744
  return ` LIMIT ?`;
2626
2745
  }
2627
- function compileSelect(table, scope, filters, options) {
2746
+ function compileSelect(table, filters, options) {
2628
2747
  const params = [];
2629
- const conditions = ['"scope" = ?'];
2630
- params.push(scope);
2748
+ const conditions = [];
2631
2749
  for (const f of filters) {
2632
2750
  conditions.push(compileFilter(f, params));
2633
2751
  }
2634
- let sql = `SELECT * FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`;
2635
- const orderClause = compileOrderBy(options, params);
2636
- sql += orderClause;
2752
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2753
+ let sql = `SELECT * FROM ${quoteIdent2(table)}${where}`;
2754
+ sql += compileOrderBy(options, params);
2637
2755
  sql += compileLimit(options, params);
2638
2756
  return { sql, params };
2639
2757
  }
2640
- function compileAggregate(table, scope, spec, filters) {
2641
- const aliases = Object.keys(spec);
2642
- if (aliases.length === 0) {
2758
+ function compileExpand(table, params) {
2759
+ if (params.sources.length === 0) {
2643
2760
  throw new FiregraphError(
2644
- "aggregate() requires at least one aggregation in the `aggregates` map.",
2761
+ "compileExpand requires a non-empty sources list \u2014 empty IN () is invalid SQL.",
2645
2762
  "INVALID_QUERY"
2646
2763
  );
2647
2764
  }
2648
- const projections = [];
2649
- for (const alias of aliases) {
2650
- const { op, field } = spec[alias];
2651
- validateJsonPathKey(alias, SQLITE_BACKEND_ERR_LABEL);
2652
- if (op === "count") {
2653
- if (field !== void 0) {
2654
- throw new FiregraphError(
2655
- `Aggregate '${alias}' op 'count' must not specify a field \u2014 count operates on rows, not a column expression.`,
2656
- "INVALID_QUERY"
2657
- );
2658
- }
2659
- projections.push(`COUNT(*) AS ${quoteIdent(alias)}`);
2660
- continue;
2661
- }
2662
- if (!field) {
2663
- throw new FiregraphError(
2664
- `Aggregate '${alias}' op '${op}' requires a field.`,
2665
- "INVALID_QUERY"
2666
- );
2667
- }
2668
- const { expr } = compileFieldRef(field);
2669
- const numeric = `CAST(${expr} AS REAL)`;
2670
- if (op === "sum") projections.push(`SUM(${numeric}) AS ${quoteIdent(alias)}`);
2671
- else if (op === "avg") projections.push(`AVG(${numeric}) AS ${quoteIdent(alias)}`);
2672
- else if (op === "min") projections.push(`MIN(${numeric}) AS ${quoteIdent(alias)}`);
2673
- else if (op === "max") projections.push(`MAX(${numeric}) AS ${quoteIdent(alias)}`);
2674
- else
2675
- throw new FiregraphError(
2676
- `SQLite backend does not support aggregate op: ${String(op)}`,
2677
- "INVALID_QUERY"
2678
- );
2765
+ const direction = params.direction ?? "forward";
2766
+ const aUidCol = compileFieldRef("aUid").expr;
2767
+ const bUidCol = compileFieldRef("bUid").expr;
2768
+ const aTypeCol = compileFieldRef("aType").expr;
2769
+ const bTypeCol = compileFieldRef("bType").expr;
2770
+ const axbTypeCol = compileFieldRef("axbType").expr;
2771
+ const sourceColumn = direction === "forward" ? aUidCol : bUidCol;
2772
+ const sqlParams = [params.axbType];
2773
+ const conditions = [`${axbTypeCol} = ?`];
2774
+ const placeholders = params.sources.map(() => "?").join(", ");
2775
+ conditions.push(`${sourceColumn} IN (${placeholders})`);
2776
+ for (const uid of params.sources) sqlParams.push(uid);
2777
+ if (params.aType !== void 0) {
2778
+ conditions.push(`${aTypeCol} = ?`);
2779
+ sqlParams.push(params.aType);
2679
2780
  }
2680
- const params = [scope];
2681
- const conditions = ['"scope" = ?'];
2682
- for (const f of filters) {
2683
- conditions.push(compileFilter(f, params));
2781
+ if (params.bType !== void 0) {
2782
+ conditions.push(`${bTypeCol} = ?`);
2783
+ sqlParams.push(params.bType);
2684
2784
  }
2685
- const sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`;
2686
- return { stmt: { sql, params }, aliases };
2785
+ if (params.axbType === NODE_RELATION) {
2786
+ conditions.push(`${aUidCol} != ${bUidCol}`);
2787
+ }
2788
+ let sql = `SELECT * FROM ${quoteIdent2(table)} WHERE ${conditions.join(" AND ")}`;
2789
+ if (params.orderBy) {
2790
+ sql += compileOrderBy({ orderBy: params.orderBy }, sqlParams);
2791
+ }
2792
+ if (params.limitPerSource !== void 0) {
2793
+ const totalLimit = params.sources.length * params.limitPerSource;
2794
+ sql += ` LIMIT ?`;
2795
+ sqlParams.push(totalLimit);
2796
+ }
2797
+ return { sql, params: sqlParams };
2687
2798
  }
2688
- function compileSelectGlobal(table, filters, options, scopeNameFilter) {
2689
- if (filters.length === 0) {
2799
+ function compileExpandHydrate(table, targetUids) {
2800
+ if (targetUids.length === 0) {
2690
2801
  throw new FiregraphError(
2691
- "compileSelectGlobal requires at least one filter \u2014 refusing to issue an unbounded SELECT.",
2802
+ "compileExpandHydrate requires a non-empty target list \u2014 empty IN () is invalid SQL.",
2692
2803
  "INVALID_QUERY"
2693
2804
  );
2694
2805
  }
2695
- const params = [];
2696
- const conditions = [];
2697
- if (scopeNameFilter) {
2698
- if (scopeNameFilter.isRoot) {
2699
- conditions.push(`"scope" = ?`);
2700
- params.push("");
2701
- } else {
2702
- conditions.push(`"scope" LIKE ? ESCAPE '\\'`);
2703
- params.push(`%/${escapeLike(scopeNameFilter.name)}`);
2704
- }
2705
- }
2706
- for (const f of filters) {
2707
- conditions.push(compileFilter(f, params));
2708
- }
2709
- const sql = `SELECT * FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}` + compileOrderBy(options, params) + compileLimit(options, params);
2710
- return { sql, params };
2806
+ const placeholders = targetUids.map(() => "?").join(", ");
2807
+ const sqlParams = [NODE_RELATION];
2808
+ for (const uid of targetUids) sqlParams.push(uid);
2809
+ const aUidCol = compileFieldRef("aUid").expr;
2810
+ const bUidCol = compileFieldRef("bUid").expr;
2811
+ const axbTypeCol = compileFieldRef("axbType").expr;
2812
+ return {
2813
+ sql: `SELECT * FROM ${quoteIdent2(table)} WHERE ${axbTypeCol} = ? AND ${aUidCol} = ${bUidCol} AND ${bUidCol} IN (${placeholders})`,
2814
+ params: sqlParams
2815
+ };
2711
2816
  }
2712
- function compileSelectByDocId(table, scope, docId) {
2817
+ function compileSelectByDocId(table, docId) {
2713
2818
  return {
2714
- sql: `SELECT * FROM ${quoteIdent(table)} WHERE "scope" = ? AND "doc_id" = ? LIMIT 1`,
2715
- params: [scope, docId]
2819
+ sql: `SELECT * FROM ${quoteIdent2(table)} WHERE "doc_id" = ? LIMIT 1`,
2820
+ params: [docId]
2716
2821
  };
2717
2822
  }
2718
2823
  function normalizeProjectionField(field) {
@@ -2720,7 +2825,7 @@ function normalizeProjectionField(field) {
2720
2825
  if (field === "data" || field.startsWith("data.")) return field;
2721
2826
  return `data.${field}`;
2722
2827
  }
2723
- function compileFindEdgesProjected(table, scope, select, filters, options) {
2828
+ function compileFindEdgesProjected(table, select, filters, options) {
2724
2829
  if (select.length === 0) {
2725
2830
  throw new FiregraphError(
2726
2831
  "compileFindEdgesProjected requires a non-empty select list \u2014 an empty projection has no SQL representation distinct from `findEdges`.",
@@ -2759,12 +2864,13 @@ function compileFindEdgesProjected(table, scope, select, filters, options) {
2759
2864
  }
2760
2865
  columns.push({ field, kind, typeAlias: typeAliasName });
2761
2866
  }
2762
- const params = [scope];
2763
- const conditions = ['"scope" = ?'];
2867
+ const params = [];
2868
+ const conditions = [];
2764
2869
  for (const f of filters) {
2765
2870
  conditions.push(compileFilter(f, params));
2766
2871
  }
2767
- let sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`;
2872
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2873
+ let sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent2(table)}${where}`;
2768
2874
  sql += compileOrderBy(options, params);
2769
2875
  sql += compileLimit(options, params);
2770
2876
  return { stmt: { sql, params }, columns };
@@ -2789,7 +2895,7 @@ function decodeProjectedRow(row, columns) {
2789
2895
  }
2790
2896
  break;
2791
2897
  case "builtin-timestamp": {
2792
- const ms = toMillis(raw);
2898
+ const ms = rowTimestampToMillis(raw);
2793
2899
  out[c.field] = GraphTimestampImpl.fromMillis(ms);
2794
2900
  break;
2795
2901
  }
@@ -2817,74 +2923,63 @@ function decodeProjectedRow(row, columns) {
2817
2923
  }
2818
2924
  return out;
2819
2925
  }
2820
- function compileExpand(table, scope, params) {
2821
- if (params.sources.length === 0) {
2926
+ function compileAggregate(table, spec, filters) {
2927
+ const aliases = Object.keys(spec);
2928
+ if (aliases.length === 0) {
2822
2929
  throw new FiregraphError(
2823
- "compileExpand requires a non-empty sources list \u2014 empty IN () is invalid SQL. Callers should short-circuit empty input before reaching the compiler.",
2930
+ "aggregate() requires at least one aggregation in the `aggregates` map.",
2824
2931
  "INVALID_QUERY"
2825
2932
  );
2826
2933
  }
2827
- const direction = params.direction ?? "forward";
2828
- const aUidCol = compileFieldRef("aUid").expr;
2829
- const bUidCol = compileFieldRef("bUid").expr;
2830
- const aTypeCol = compileFieldRef("aType").expr;
2831
- const bTypeCol = compileFieldRef("bType").expr;
2832
- const axbTypeCol = compileFieldRef("axbType").expr;
2833
- const sourceColumn = direction === "forward" ? aUidCol : bUidCol;
2834
- const sqlParams = [scope, params.axbType];
2835
- const conditions = ['"scope" = ?', `${axbTypeCol} = ?`];
2836
- const placeholders = params.sources.map(() => "?").join(", ");
2837
- conditions.push(`${sourceColumn} IN (${placeholders})`);
2838
- for (const uid of params.sources) sqlParams.push(uid);
2839
- if (params.aType !== void 0) {
2840
- conditions.push(`${aTypeCol} = ?`);
2841
- sqlParams.push(params.aType);
2842
- }
2843
- if (params.bType !== void 0) {
2844
- conditions.push(`${bTypeCol} = ?`);
2845
- sqlParams.push(params.bType);
2846
- }
2847
- if (params.axbType === NODE_RELATION) {
2848
- conditions.push(`${aUidCol} != ${bUidCol}`);
2849
- }
2850
- let sql = `SELECT * FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`;
2851
- if (params.orderBy) {
2852
- sql += compileOrderBy({ orderBy: params.orderBy }, sqlParams);
2853
- }
2854
- if (params.limitPerSource !== void 0) {
2855
- const totalLimit = params.sources.length * params.limitPerSource;
2856
- sql += ` LIMIT ?`;
2857
- sqlParams.push(totalLimit);
2934
+ const projections = [];
2935
+ for (const alias of aliases) {
2936
+ const { op, field } = spec[alias];
2937
+ validateJsonPathKey(alias, BACKEND_ERR_LABEL);
2938
+ if (op === "count") {
2939
+ if (field !== void 0) {
2940
+ throw new FiregraphError(
2941
+ `Aggregate '${alias}' op 'count' must not specify a field \u2014 count operates on rows, not a column expression.`,
2942
+ "INVALID_QUERY"
2943
+ );
2944
+ }
2945
+ projections.push(`COUNT(*) AS ${quoteIdent2(alias)}`);
2946
+ continue;
2947
+ }
2948
+ if (!field) {
2949
+ throw new FiregraphError(
2950
+ `Aggregate '${alias}' op '${op}' requires a field.`,
2951
+ "INVALID_QUERY"
2952
+ );
2953
+ }
2954
+ const { expr } = compileFieldRef(field);
2955
+ const numeric = `CAST(${expr} AS REAL)`;
2956
+ if (op === "sum") projections.push(`SUM(${numeric}) AS ${quoteIdent2(alias)}`);
2957
+ else if (op === "avg") projections.push(`AVG(${numeric}) AS ${quoteIdent2(alias)}`);
2958
+ else if (op === "min") projections.push(`MIN(${numeric}) AS ${quoteIdent2(alias)}`);
2959
+ else if (op === "max") projections.push(`MAX(${numeric}) AS ${quoteIdent2(alias)}`);
2960
+ else
2961
+ throw new FiregraphError(
2962
+ `SQLite backend does not support aggregate op: ${String(op)}`,
2963
+ "INVALID_QUERY"
2964
+ );
2858
2965
  }
2859
- return { sql, params: sqlParams };
2860
- }
2861
- function compileExpandHydrate(table, scope, targetUids) {
2862
- if (targetUids.length === 0) {
2863
- throw new FiregraphError(
2864
- "compileExpandHydrate requires a non-empty target list \u2014 empty IN () is invalid SQL.",
2865
- "INVALID_QUERY"
2866
- );
2966
+ const params = [];
2967
+ const conditions = [];
2968
+ for (const f of filters) {
2969
+ conditions.push(compileFilter(f, params));
2867
2970
  }
2868
- const placeholders = targetUids.map(() => "?").join(", ");
2869
- const sqlParams = [scope, NODE_RELATION];
2870
- for (const uid of targetUids) sqlParams.push(uid);
2871
- const aUidCol = compileFieldRef("aUid").expr;
2872
- const bUidCol = compileFieldRef("bUid").expr;
2873
- const axbTypeCol = compileFieldRef("axbType").expr;
2874
- return {
2875
- sql: `SELECT * FROM ${quoteIdent(table)} WHERE "scope" = ? AND ${axbTypeCol} = ? AND ${aUidCol} = ${bUidCol} AND ${bUidCol} IN (${placeholders})`,
2876
- params: sqlParams
2877
- };
2971
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2972
+ const sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent2(table)}${where}`;
2973
+ return { stmt: { sql, params }, aliases };
2878
2974
  }
2879
- function compileSet(table, scope, docId, record, nowMillis, mode) {
2880
- assertJsonSafePayload(record.data, SQLITE_BACKEND_LABEL);
2975
+ function compileSet(table, docId, record, nowMillis, mode) {
2976
+ assertJsonSafePayload(record.data, BACKEND_LABEL);
2881
2977
  if (mode === "replace") {
2882
- const sql2 = `INSERT OR REPLACE INTO ${quoteIdent(table)} (
2883
- doc_id, scope, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
2884
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
2978
+ const sql2 = `INSERT OR REPLACE INTO ${quoteIdent2(table)} (
2979
+ doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
2980
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
2885
2981
  const params = [
2886
2982
  docId,
2887
- scope,
2888
2983
  record.aType,
2889
2984
  record.aUid,
2890
2985
  record.axbType,
@@ -2899,7 +2994,6 @@ function compileSet(table, scope, docId, record, nowMillis, mode) {
2899
2994
  }
2900
2995
  const insertParams = [
2901
2996
  docId,
2902
- scope,
2903
2997
  record.aType,
2904
2998
  record.aUid,
2905
2999
  record.axbType,
@@ -2912,11 +3006,11 @@ function compileSet(table, scope, docId, record, nowMillis, mode) {
2912
3006
  ];
2913
3007
  const ops = flattenPatch(record.data ?? {});
2914
3008
  const updateParams = [];
2915
- const dataExpr = compileDataOpsExpr(ops, `COALESCE("data", '{}')`, updateParams, SQLITE_BACKEND_ERR_LABEL) ?? `COALESCE("data", '{}')`;
2916
- const sql = `INSERT INTO ${quoteIdent(table)} (
2917
- doc_id, scope, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
2918
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2919
- ON CONFLICT(scope, doc_id) DO UPDATE SET
3009
+ const dataExpr = compileDataOpsExpr(ops, `COALESCE("data", '{}')`, updateParams, BACKEND_ERR_LABEL) ?? `COALESCE("data", '{}')`;
3010
+ const sql = `INSERT INTO ${quoteIdent2(table)} (
3011
+ doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
3012
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3013
+ ON CONFLICT(doc_id) DO UPDATE SET
2920
3014
  "a_type" = excluded."a_type",
2921
3015
  "a_uid" = excluded."a_uid",
2922
3016
  "axb_type" = excluded."axb_type",
@@ -2928,23 +3022,23 @@ function compileSet(table, scope, docId, record, nowMillis, mode) {
2928
3022
  "updated_at" = excluded."updated_at"`;
2929
3023
  return { sql, params: [...insertParams, ...updateParams] };
2930
3024
  }
2931
- function compileUpdate(table, scope, docId, update, nowMillis) {
3025
+ function compileUpdate(table, docId, update, nowMillis) {
2932
3026
  assertUpdatePayloadExclusive(update);
2933
3027
  const setClauses = [];
2934
3028
  const params = [];
2935
3029
  if (update.replaceData) {
2936
- assertJsonSafePayload(update.replaceData, SQLITE_BACKEND_LABEL);
3030
+ assertJsonSafePayload(update.replaceData, BACKEND_LABEL);
2937
3031
  setClauses.push(`"data" = ?`);
2938
3032
  params.push(JSON.stringify(update.replaceData));
2939
3033
  } else if (update.dataOps && update.dataOps.length > 0) {
2940
3034
  for (const op of update.dataOps) {
2941
- if (!op.delete) assertJsonSafePayload(op.value, SQLITE_BACKEND_LABEL);
3035
+ if (!op.delete) assertJsonSafePayload(op.value, BACKEND_LABEL);
2942
3036
  }
2943
3037
  const expr = compileDataOpsExpr(
2944
3038
  update.dataOps,
2945
3039
  `COALESCE("data", '{}')`,
2946
3040
  params,
2947
- SQLITE_BACKEND_ERR_LABEL
3041
+ BACKEND_ERR_LABEL
2948
3042
  );
2949
3043
  if (expr !== null) {
2950
3044
  setClauses.push(`"data" = ${expr}`);
@@ -2956,30 +3050,31 @@ function compileUpdate(table, scope, docId, update, nowMillis) {
2956
3050
  }
2957
3051
  setClauses.push(`"updated_at" = ?`);
2958
3052
  params.push(nowMillis);
2959
- params.push(scope, docId);
3053
+ params.push(docId);
2960
3054
  return {
2961
- sql: `UPDATE ${quoteIdent(table)} SET ${setClauses.join(", ")} WHERE "scope" = ? AND "doc_id" = ?`,
3055
+ sql: `UPDATE ${quoteIdent2(table)} SET ${setClauses.join(", ")} WHERE "doc_id" = ?`,
2962
3056
  params
2963
3057
  };
2964
3058
  }
2965
- function compileDelete(table, scope, docId) {
3059
+ function compileDelete(table, docId) {
2966
3060
  return {
2967
- sql: `DELETE FROM ${quoteIdent(table)} WHERE "scope" = ? AND "doc_id" = ?`,
2968
- params: [scope, docId]
3061
+ sql: `DELETE FROM ${quoteIdent2(table)} WHERE "doc_id" = ?`,
3062
+ params: [docId]
2969
3063
  };
2970
3064
  }
2971
- function compileBulkDelete(table, scope, filters) {
2972
- const params = [scope];
2973
- const conditions = ['"scope" = ?'];
3065
+ function compileBulkDelete(table, filters) {
3066
+ const params = [];
3067
+ const conditions = [];
2974
3068
  for (const f of filters) {
2975
3069
  conditions.push(compileFilter(f, params));
2976
3070
  }
3071
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2977
3072
  return {
2978
- sql: `DELETE FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`,
3073
+ sql: `DELETE FROM ${quoteIdent2(table)}${where}`,
2979
3074
  params
2980
3075
  };
2981
3076
  }
2982
- function compileBulkUpdate(table, scope, filters, patchData, nowMillis) {
3077
+ function compileBulkUpdate(table, filters, patchData, nowMillis) {
2983
3078
  const dataOps = flattenPatch(patchData);
2984
3079
  if (dataOps.length === 0) {
2985
3080
  throw new FiregraphError(
@@ -2988,15 +3083,10 @@ function compileBulkUpdate(table, scope, filters, patchData, nowMillis) {
2988
3083
  );
2989
3084
  }
2990
3085
  for (const op of dataOps) {
2991
- if (!op.delete) assertJsonSafePayload(op.value, SQLITE_BACKEND_LABEL);
3086
+ if (!op.delete) assertJsonSafePayload(op.value, BACKEND_LABEL);
2992
3087
  }
2993
3088
  const setParams = [];
2994
- const expr = compileDataOpsExpr(
2995
- dataOps,
2996
- `COALESCE("data", '{}')`,
2997
- setParams,
2998
- SQLITE_BACKEND_ERR_LABEL
2999
- );
3089
+ const expr = compileDataOpsExpr(dataOps, `COALESCE("data", '{}')`, setParams, BACKEND_ERR_LABEL);
3000
3090
  if (expr === null) {
3001
3091
  throw new FiregraphError(
3002
3092
  "bulkUpdate() patch produced no SQL operations \u2014 internal invariant violated.",
@@ -3005,38 +3095,22 @@ function compileBulkUpdate(table, scope, filters, patchData, nowMillis) {
3005
3095
  }
3006
3096
  const setClauses = [`"data" = ${expr}`, `"updated_at" = ?`];
3007
3097
  setParams.push(nowMillis);
3008
- const whereParams = [scope];
3009
- const conditions = ['"scope" = ?'];
3098
+ const whereParams = [];
3099
+ const conditions = [];
3010
3100
  for (const f of filters) {
3011
3101
  conditions.push(compileFilter(f, whereParams));
3012
3102
  }
3103
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
3013
3104
  return {
3014
- sql: `UPDATE ${quoteIdent(table)} SET ${setClauses.join(", ")} WHERE ${conditions.join(" AND ")}`,
3105
+ sql: `UPDATE ${quoteIdent2(table)} SET ${setClauses.join(", ")}${where}`,
3015
3106
  params: [...setParams, ...whereParams]
3016
3107
  };
3017
3108
  }
3018
- function compileDeleteScopePrefix(table, scopePrefix) {
3019
- const escaped = escapeLike(scopePrefix);
3020
- return {
3021
- sql: `DELETE FROM ${quoteIdent(table)} WHERE "scope" LIKE ? ESCAPE '\\'`,
3022
- params: [`${escaped}/%`]
3023
- };
3024
- }
3025
- function compileCountScopePrefix(table, scopePrefix) {
3026
- const escaped = escapeLike(scopePrefix);
3027
- return {
3028
- sql: `SELECT COUNT(*) AS n FROM ${quoteIdent(table)} WHERE "scope" LIKE ? ESCAPE '\\'`,
3029
- params: [`${escaped}/%`]
3030
- };
3031
- }
3032
- function escapeLike(value) {
3033
- return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
3034
- }
3035
3109
  function rowToRecord(row) {
3036
3110
  const dataString = row.data;
3037
3111
  const data = dataString ? JSON.parse(dataString) : {};
3038
- const createdMs = toMillis(row.created_at);
3039
- const updatedMs = toMillis(row.updated_at);
3112
+ const createdMs = rowTimestampToMillis(row.created_at);
3113
+ const updatedMs = rowTimestampToMillis(row.updated_at);
3040
3114
  const record = {
3041
3115
  aType: row.a_type,
3042
3116
  aUid: row.a_uid,
@@ -3052,11 +3126,71 @@ function rowToRecord(row) {
3052
3126
  }
3053
3127
  return record;
3054
3128
  }
3055
- function toMillis(value) {
3129
+ function rowTimestampToMillis(value) {
3056
3130
  if (typeof value === "number") return value;
3057
3131
  if (typeof value === "bigint") return Number(value);
3058
- if (typeof value === "string") return Number(value);
3059
- return 0;
3132
+ if (typeof value === "string") {
3133
+ const n = Number(value);
3134
+ if (Number.isFinite(n)) return n;
3135
+ }
3136
+ throw new FiregraphError(
3137
+ `SQLite row has non-numeric timestamp column: ${typeof value} (${String(value)})`,
3138
+ "INVALID_QUERY"
3139
+ );
3140
+ }
3141
+
3142
+ // src/sqlite/catalog.ts
3143
+ function catalogTableName(rootTable) {
3144
+ validateTableName(rootTable);
3145
+ return `${rootTable}_graphs`;
3146
+ }
3147
+ function mangleStorageScope(scope) {
3148
+ let out = "";
3149
+ for (const ch of scope) {
3150
+ if (/[A-Za-z0-9]/.test(ch)) out += ch;
3151
+ else if (ch === "_") out += "__";
3152
+ else if (ch === "-") out += "_h";
3153
+ else if (ch === "/") out += "_s";
3154
+ else out += `_u${ch.codePointAt(0).toString(16)}_`;
3155
+ }
3156
+ return out;
3157
+ }
3158
+ function tableForScope(rootTable, storageScope) {
3159
+ validateTableName(rootTable);
3160
+ if (storageScope === "") return rootTable;
3161
+ return `${rootTable}_g_${mangleStorageScope(storageScope)}`;
3162
+ }
3163
+ function escapeLikePrefix(prefix) {
3164
+ return prefix.replace(/[\\%_]/g, (c) => `\\${c}`);
3165
+ }
3166
+ function buildCatalogDDL(rootTable) {
3167
+ const t = quoteIdent2(catalogTableName(rootTable));
3168
+ return `CREATE TABLE IF NOT EXISTS ${t} (
3169
+ storage_scope TEXT NOT NULL PRIMARY KEY,
3170
+ table_name TEXT NOT NULL UNIQUE,
3171
+ scope_path TEXT NOT NULL
3172
+ )`;
3173
+ }
3174
+ function compileCatalogRegister(rootTable, storageScope, tableName, scopePath) {
3175
+ const t = quoteIdent2(catalogTableName(rootTable));
3176
+ return {
3177
+ sql: `INSERT OR IGNORE INTO ${t} (storage_scope, table_name, scope_path) VALUES (?, ?, ?)`,
3178
+ params: [storageScope, tableName, scopePath]
3179
+ };
3180
+ }
3181
+ function compileCatalogDescendants(rootTable, scopePrefix) {
3182
+ const t = quoteIdent2(catalogTableName(rootTable));
3183
+ return {
3184
+ sql: `SELECT storage_scope, table_name FROM ${t} WHERE storage_scope LIKE ? ESCAPE '\\' ORDER BY storage_scope`,
3185
+ params: [`${escapeLikePrefix(scopePrefix)}/%`]
3186
+ };
3187
+ }
3188
+ function compileCatalogDelete(rootTable, storageScope) {
3189
+ const t = quoteIdent2(catalogTableName(rootTable));
3190
+ return {
3191
+ sql: `DELETE FROM ${t} WHERE storage_scope = ?`,
3192
+ params: [storageScope]
3193
+ };
3060
3194
  }
3061
3195
 
3062
3196
  // src/sqlite/backend.ts
@@ -3096,63 +3230,59 @@ function chunkStatements(statements, maxStatements, maxParams) {
3096
3230
  return chunks;
3097
3231
  }
3098
3232
  var SqliteTransactionBackendImpl = class {
3099
- constructor(tx, tableName, storageScope) {
3233
+ constructor(tx, tableName) {
3100
3234
  this.tx = tx;
3101
3235
  this.tableName = tableName;
3102
- this.storageScope = storageScope;
3103
3236
  }
3104
3237
  async getDoc(docId) {
3105
- const stmt = compileSelectByDocId(this.tableName, this.storageScope, docId);
3238
+ const stmt = compileSelectByDocId(this.tableName, docId);
3106
3239
  const rows = await this.tx.all(stmt.sql, stmt.params);
3107
3240
  return rows.length === 0 ? null : rowToRecord(rows[0]);
3108
3241
  }
3109
3242
  async query(filters, options) {
3110
- const stmt = compileSelect(this.tableName, this.storageScope, filters, options);
3243
+ const stmt = compileSelect(this.tableName, filters, options);
3111
3244
  const rows = await this.tx.all(stmt.sql, stmt.params);
3112
3245
  return rows.map(rowToRecord);
3113
3246
  }
3114
3247
  async setDoc(docId, record, mode) {
3115
- const stmt = compileSet(this.tableName, this.storageScope, docId, record, Date.now(), mode);
3248
+ const stmt = compileSet(this.tableName, docId, record, Date.now(), mode);
3116
3249
  await this.tx.run(stmt.sql, stmt.params);
3117
3250
  }
3118
3251
  async updateDoc(docId, update) {
3119
- const stmt = compileUpdate(this.tableName, this.storageScope, docId, update, Date.now());
3252
+ const stmt = compileUpdate(this.tableName, docId, update, Date.now());
3120
3253
  const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
3121
3254
  const rows = await this.tx.all(sqlWithReturning, stmt.params);
3122
3255
  if (rows.length === 0) {
3123
3256
  throw new FiregraphError(
3124
- `updateDoc: no document found for doc_id=${docId} (scope=${this.storageScope})`,
3257
+ `updateDoc: no document found for doc_id=${docId} (table=${this.tableName})`,
3125
3258
  "NOT_FOUND"
3126
3259
  );
3127
3260
  }
3128
3261
  }
3129
3262
  async deleteDoc(docId) {
3130
- const stmt = compileDelete(this.tableName, this.storageScope, docId);
3263
+ const stmt = compileDelete(this.tableName, docId);
3131
3264
  await this.tx.run(stmt.sql, stmt.params);
3132
3265
  }
3133
3266
  };
3134
3267
  var SqliteBatchBackendImpl = class {
3135
- constructor(executor, tableName, storageScope) {
3268
+ constructor(executor, tableName, ensureSchema) {
3136
3269
  this.executor = executor;
3137
3270
  this.tableName = tableName;
3138
- this.storageScope = storageScope;
3271
+ this.ensureSchema = ensureSchema;
3139
3272
  }
3140
3273
  statements = [];
3141
3274
  setDoc(docId, record, mode) {
3142
- this.statements.push(
3143
- compileSet(this.tableName, this.storageScope, docId, record, Date.now(), mode)
3144
- );
3275
+ this.statements.push(compileSet(this.tableName, docId, record, Date.now(), mode));
3145
3276
  }
3146
3277
  updateDoc(docId, update) {
3147
- this.statements.push(
3148
- compileUpdate(this.tableName, this.storageScope, docId, update, Date.now())
3149
- );
3278
+ this.statements.push(compileUpdate(this.tableName, docId, update, Date.now()));
3150
3279
  }
3151
3280
  deleteDoc(docId) {
3152
- this.statements.push(compileDelete(this.tableName, this.storageScope, docId));
3281
+ this.statements.push(compileDelete(this.tableName, docId));
3153
3282
  }
3154
3283
  async commit() {
3155
3284
  if (this.statements.length === 0) return;
3285
+ await this.ensureSchema();
3156
3286
  await this.executor.batch(this.statements);
3157
3287
  this.statements.length = 0;
3158
3288
  }
@@ -3169,11 +3299,15 @@ var SQLITE_CORE_CAPS = [
3169
3299
  "raw.sql"
3170
3300
  ];
3171
3301
  var SqliteBackendImpl = class _SqliteBackendImpl {
3172
- constructor(executor, tableName, storageScope, scopePath) {
3302
+ constructor(executor, rootTable, storageScope, scopePath, registry, coreIndexes) {
3173
3303
  this.executor = executor;
3174
- this.collectionPath = tableName;
3304
+ validateTableName(rootTable);
3305
+ this.rootTable = rootTable;
3306
+ this.collectionPath = tableForScope(rootTable, storageScope);
3175
3307
  this.storageScope = storageScope;
3176
3308
  this.scopePath = scopePath;
3309
+ this.registry = registry;
3310
+ this.coreIndexes = coreIndexes;
3177
3311
  const caps = new Set(SQLITE_CORE_CAPS);
3178
3312
  if (typeof executor.transaction === "function") {
3179
3313
  caps.add("core.transactions");
@@ -3181,48 +3315,123 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3181
3315
  this.capabilities = createCapabilities(caps);
3182
3316
  }
3183
3317
  capabilities;
3184
- /** Logical table name (returned through `collectionPath` for parity with Firestore). */
3318
+ /** Physical table holding this graph's triples. */
3185
3319
  collectionPath;
3186
3320
  scopePath;
3187
- /** Materialized storage scope (interleaved parent UIDs + subgraph names). */
3321
+ /** Storage scope (interleaved parent UIDs + subgraph names) — `''` at root. */
3188
3322
  storageScope;
3323
+ /** Root graph's table name — prefix for subgraph tables and the catalog. */
3324
+ rootTable;
3325
+ registry;
3326
+ coreIndexes;
3327
+ ensured = null;
3328
+ /**
3329
+ * Lazily create this graph's table + indexes + the catalog, and register
3330
+ * the graph in the catalog. Runs once per backend instance; the DDL is
3331
+ * all `IF NOT EXISTS` / `INSERT OR IGNORE`, so concurrent instances over
3332
+ * the same database converge safely.
3333
+ */
3334
+ ensureSchema() {
3335
+ if (!this.ensured) {
3336
+ this.ensured = this.doEnsureSchema().catch((err) => {
3337
+ this.ensured = null;
3338
+ throw err;
3339
+ });
3340
+ }
3341
+ return this.ensured;
3342
+ }
3343
+ async doEnsureSchema() {
3344
+ const ddl = [
3345
+ ...buildSchemaStatements(this.collectionPath, {
3346
+ coreIndexes: this.coreIndexes,
3347
+ registry: this.registry
3348
+ }),
3349
+ buildCatalogDDL(this.rootTable)
3350
+ ];
3351
+ const statements = ddl.map((sql) => ({ sql, params: [] }));
3352
+ statements.push(
3353
+ compileCatalogRegister(
3354
+ this.rootTable,
3355
+ this.storageScope,
3356
+ this.collectionPath,
3357
+ this.scopePath
3358
+ )
3359
+ );
3360
+ const chunks = chunkStatements(
3361
+ statements,
3362
+ this.executor.maxBatchSize,
3363
+ this.executor.maxBatchParams
3364
+ );
3365
+ for (const chunk of chunks) {
3366
+ await this.executor.batch(chunk);
3367
+ }
3368
+ }
3369
+ /**
3370
+ * Run `op` with the schema bootstrap applied, self-healing when this
3371
+ * graph's table was dropped out from under the instance — a parent's
3372
+ * cascade delete DROPs descendant tables, but subgraph handles created
3373
+ * before the cascade still point at this (now missing) table with a
3374
+ * resolved bootstrap cache. On a "no such table: <own table>" error the
3375
+ * cache resets, the empty graph is recreated, and the op retries once.
3376
+ * This matches Firestore semantics, where a deleted subcollection reads
3377
+ * as empty and writes recreate it.
3378
+ */
3379
+ async withSchema(op) {
3380
+ await this.ensureSchema();
3381
+ try {
3382
+ return await op();
3383
+ } catch (err) {
3384
+ if (!this.isMissingOwnTable(err)) throw err;
3385
+ this.ensured = null;
3386
+ await this.ensureSchema();
3387
+ return op();
3388
+ }
3389
+ }
3390
+ /** True when `err` is SQLite's missing-table error naming OUR table. */
3391
+ isMissingOwnTable(err) {
3392
+ const message = err instanceof Error ? err.message : String(err);
3393
+ return message.includes(`no such table: ${this.collectionPath}`);
3394
+ }
3189
3395
  // --- Reads ---
3190
3396
  async getDoc(docId) {
3191
- const stmt = compileSelectByDocId(this.collectionPath, this.storageScope, docId);
3192
- const rows = await this.executor.all(stmt.sql, stmt.params);
3193
- return rows.length === 0 ? null : rowToRecord(rows[0]);
3397
+ return this.withSchema(async () => {
3398
+ const stmt = compileSelectByDocId(this.collectionPath, docId);
3399
+ const rows = await this.executor.all(stmt.sql, stmt.params);
3400
+ return rows.length === 0 ? null : rowToRecord(rows[0]);
3401
+ });
3194
3402
  }
3195
3403
  async query(filters, options) {
3196
- const stmt = compileSelect(this.collectionPath, this.storageScope, filters, options);
3197
- const rows = await this.executor.all(stmt.sql, stmt.params);
3198
- return rows.map(rowToRecord);
3404
+ return this.withSchema(async () => {
3405
+ const stmt = compileSelect(this.collectionPath, filters, options);
3406
+ const rows = await this.executor.all(stmt.sql, stmt.params);
3407
+ return rows.map(rowToRecord);
3408
+ });
3199
3409
  }
3200
3410
  // --- Writes ---
3201
3411
  async setDoc(docId, record, mode) {
3202
- const stmt = compileSet(
3203
- this.collectionPath,
3204
- this.storageScope,
3205
- docId,
3206
- record,
3207
- Date.now(),
3208
- mode
3209
- );
3210
- await this.executor.run(stmt.sql, stmt.params);
3412
+ return this.withSchema(async () => {
3413
+ const stmt = compileSet(this.collectionPath, docId, record, Date.now(), mode);
3414
+ await this.executor.run(stmt.sql, stmt.params);
3415
+ });
3211
3416
  }
3212
3417
  async updateDoc(docId, update) {
3213
- const stmt = compileUpdate(this.collectionPath, this.storageScope, docId, update, Date.now());
3214
- const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
3215
- const rows = await this.executor.all(sqlWithReturning, stmt.params);
3216
- if (rows.length === 0) {
3217
- throw new FiregraphError(
3218
- `updateDoc: no document found for doc_id=${docId} (scope=${this.storageScope})`,
3219
- "NOT_FOUND"
3220
- );
3221
- }
3418
+ return this.withSchema(async () => {
3419
+ const stmt = compileUpdate(this.collectionPath, docId, update, Date.now());
3420
+ const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
3421
+ const rows = await this.executor.all(sqlWithReturning, stmt.params);
3422
+ if (rows.length === 0) {
3423
+ throw new FiregraphError(
3424
+ `updateDoc: no document found for doc_id=${docId} (table=${this.collectionPath})`,
3425
+ "NOT_FOUND"
3426
+ );
3427
+ }
3428
+ });
3222
3429
  }
3223
3430
  async deleteDoc(docId) {
3224
- const stmt = compileDelete(this.collectionPath, this.storageScope, docId);
3225
- await this.executor.run(stmt.sql, stmt.params);
3431
+ return this.withSchema(async () => {
3432
+ const stmt = compileDelete(this.collectionPath, docId);
3433
+ await this.executor.run(stmt.sql, stmt.params);
3434
+ });
3226
3435
  }
3227
3436
  // --- Transactions / Batches ---
3228
3437
  async runTransaction(fn) {
@@ -3232,17 +3441,18 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3232
3441
  "UNSUPPORTED_OPERATION"
3233
3442
  );
3234
3443
  }
3444
+ await this.ensureSchema();
3235
3445
  return this.executor.transaction(async (tx) => {
3236
- const txBackend = new SqliteTransactionBackendImpl(
3237
- tx,
3238
- this.collectionPath,
3239
- this.storageScope
3240
- );
3446
+ const txBackend = new SqliteTransactionBackendImpl(tx, this.collectionPath);
3241
3447
  return fn(txBackend);
3242
3448
  });
3243
3449
  }
3244
3450
  createBatch() {
3245
- return new SqliteBatchBackendImpl(this.executor, this.collectionPath, this.storageScope);
3451
+ return new SqliteBatchBackendImpl(
3452
+ this.executor,
3453
+ this.collectionPath,
3454
+ () => this.ensureSchema()
3455
+ );
3246
3456
  }
3247
3457
  // --- Subgraphs ---
3248
3458
  subgraph(parentNodeUid, name) {
@@ -3260,10 +3470,18 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3260
3470
  }
3261
3471
  const newStorageScope = this.storageScope ? `${this.storageScope}/${parentNodeUid}/${name}` : `${parentNodeUid}/${name}`;
3262
3472
  const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
3263
- return new _SqliteBackendImpl(this.executor, this.collectionPath, newStorageScope, newScope);
3473
+ return new _SqliteBackendImpl(
3474
+ this.executor,
3475
+ this.rootTable,
3476
+ newStorageScope,
3477
+ newScope,
3478
+ this.registry,
3479
+ this.coreIndexes
3480
+ );
3264
3481
  }
3265
3482
  // --- Cascade & bulk ---
3266
3483
  async removeNodeCascade(uid, reader, options) {
3484
+ await this.ensureSchema();
3267
3485
  const [outgoingRaw, incomingRaw] = await Promise.all([
3268
3486
  reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
3269
3487
  reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
@@ -3280,22 +3498,33 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3280
3498
  }
3281
3499
  const nodeDocId = computeNodeDocId(uid);
3282
3500
  const shouldDeleteSubgraphs = options?.deleteSubcollections !== false;
3501
+ const descendants = [];
3283
3502
  let subgraphRowCount = 0;
3284
3503
  if (shouldDeleteSubgraphs) {
3285
3504
  const prefix = this.storageScope ? `${this.storageScope}/${uid}` : uid;
3286
- const countStmt = compileCountScopePrefix(this.collectionPath, prefix);
3287
- const countRows = await this.executor.all(countStmt.sql, countStmt.params);
3288
- const first = countRows[0];
3289
- const n = first?.n;
3290
- subgraphRowCount = typeof n === "bigint" ? Number(n) : Number(n ?? 0);
3505
+ const descStmt = compileCatalogDescendants(this.rootTable, prefix);
3506
+ const rows = await this.executor.all(descStmt.sql, descStmt.params);
3507
+ for (const row of rows) {
3508
+ const tableName = String(row.table_name);
3509
+ validateTableName(tableName);
3510
+ descendants.push({ storageScope: String(row.storage_scope), tableName });
3511
+ }
3512
+ for (const d of descendants) {
3513
+ const countRows = await this.executor.all(
3514
+ `SELECT COUNT(*) AS n FROM ${quoteIdent2(d.tableName)}`,
3515
+ []
3516
+ );
3517
+ const n = countRows[0]?.n;
3518
+ subgraphRowCount += typeof n === "bigint" ? Number(n) : Number(n ?? 0);
3519
+ }
3291
3520
  }
3292
3521
  const writeStatements = edgeDocIds.map(
3293
- (id) => compileDelete(this.collectionPath, this.storageScope, id)
3522
+ (id) => compileDelete(this.collectionPath, id)
3294
3523
  );
3295
- writeStatements.push(compileDelete(this.collectionPath, this.storageScope, nodeDocId));
3296
- if (shouldDeleteSubgraphs) {
3297
- const prefix = this.storageScope ? `${this.storageScope}/${uid}` : uid;
3298
- writeStatements.push(compileDeleteScopePrefix(this.collectionPath, prefix));
3524
+ writeStatements.push(compileDelete(this.collectionPath, nodeDocId));
3525
+ for (const d of descendants) {
3526
+ writeStatements.push({ sql: `DROP TABLE IF EXISTS ${quoteIdent2(d.tableName)}`, params: [] });
3527
+ writeStatements.push(compileCatalogDelete(this.rootTable, d.storageScope));
3299
3528
  }
3300
3529
  const {
3301
3530
  deleted: stmtDeleted,
@@ -3305,20 +3534,19 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3305
3534
  const allOk = errors.length === 0;
3306
3535
  const edgesDeleted = allOk ? edgeDocIds.length : 0;
3307
3536
  const nodeDeleted = allOk;
3308
- const prefixStatementContribution = shouldDeleteSubgraphs && allOk ? 1 : 0;
3309
- const deleted = stmtDeleted - prefixStatementContribution + (allOk ? subgraphRowCount : 0);
3537
+ const bookkeepingContribution = allOk ? descendants.length * 2 : 0;
3538
+ const deleted = stmtDeleted - bookkeepingContribution + (allOk ? subgraphRowCount : 0);
3310
3539
  return { deleted, batches, errors, edgesDeleted, nodeDeleted };
3311
3540
  }
3312
3541
  async bulkRemoveEdges(params, reader, options) {
3542
+ await this.ensureSchema();
3313
3543
  const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
3314
3544
  const edges = await reader.findEdges(effectiveParams);
3315
3545
  const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
3316
3546
  if (docIds.length === 0) {
3317
3547
  return { deleted: 0, batches: 0, errors: [] };
3318
3548
  }
3319
- const statements = docIds.map(
3320
- (id) => compileDelete(this.collectionPath, this.storageScope, id)
3321
- );
3549
+ const statements = docIds.map((id) => compileDelete(this.collectionPath, id));
3322
3550
  return this.executeChunkedBatches(statements, options);
3323
3551
  }
3324
3552
  /**
@@ -3334,9 +3562,9 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3334
3562
  * `result.errors.length` after the call.
3335
3563
  *
3336
3564
  * Returns `BulkResult`-shaped fields. `deleted` reflects only the
3337
- * statement count of *successfully committed* batches — a prefix-delete
3338
- * statement contributes 1 to that total even though it may match many
3339
- * rows; `removeNodeCascade` patches that up with a pre-counted row total.
3565
+ * statement count of *successfully committed* batches — a DROP TABLE
3566
+ * statement contributes 1 to that total even though it may remove many
3567
+ * rows; `removeNodeCascade` patches that up with pre-counted row totals.
3340
3568
  *
3341
3569
  * **Atomicity caveat (D1):** when chunking kicks in, atomicity is lost
3342
3570
  * across chunk boundaries — one chunk may commit while a later one fails.
@@ -3398,29 +3626,12 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3398
3626
  }
3399
3627
  return { deleted, batches, errors };
3400
3628
  }
3401
- // --- Cross-scope (collection group) ---
3402
- async findEdgesGlobal(params, collectionName) {
3403
- const plan = buildEdgeQueryPlan(params);
3404
- if (plan.strategy === "get") {
3405
- throw new FiregraphError(
3406
- "findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
3407
- "INVALID_QUERY"
3408
- );
3409
- }
3410
- const name = collectionName ?? this.collectionPath;
3411
- const scopeNameFilter = {
3412
- name,
3413
- isRoot: name === this.collectionPath
3414
- };
3415
- const stmt = compileSelectGlobal(
3416
- this.collectionPath,
3417
- plan.filters,
3418
- plan.options,
3419
- scopeNameFilter
3420
- );
3421
- const rows = await this.executor.all(stmt.sql, stmt.params);
3422
- return rows.map(rowToRecord);
3423
- }
3629
+ // `findEdgesGlobal` is deliberately NOT defined on this class. Each graph
3630
+ // is its own table, so a "collection group" query would mean scanning every
3631
+ // table listed in the catalog — an unbounded fan-out the cross-backend
3632
+ // contract treats as unsupported (the Cloudflare DO edition makes the same
3633
+ // call: no cross-DO index, no `findEdgesGlobal`). The client surfaces
3634
+ // `UNSUPPORTED_OPERATION` when the method is absent.
3424
3635
  // --- Aggregate ---
3425
3636
  /**
3426
3637
  * Run an aggregate query in a single SQL statement. Supports the full
@@ -3432,13 +3643,8 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3432
3643
  * convention and the Firestore Standard helper.
3433
3644
  */
3434
3645
  async aggregate(spec, filters) {
3435
- const { stmt, aliases } = compileAggregate(
3436
- this.collectionPath,
3437
- this.storageScope,
3438
- spec,
3439
- filters
3440
- );
3441
- const rows = await this.executor.all(stmt.sql, stmt.params);
3646
+ const { stmt, aliases } = compileAggregate(this.collectionPath, spec, filters);
3647
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
3442
3648
  const row = rows[0] ?? {};
3443
3649
  const out = {};
3444
3650
  for (const alias of aliases) {
@@ -3472,12 +3678,12 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3472
3678
  * congestion); a permanent failure is surfaced via the `errors` array
3473
3679
  * with `batchIndex: 0` so callers see the same shape as `bulkRemoveEdges`.
3474
3680
  *
3475
- * Subgraph scoping is enforced inside `compileBulkDelete` (the leading
3476
- * `"scope" = ?` predicate) so this method, like every other backend
3477
- * surface, naturally honours subgraph isolation.
3681
+ * Subgraph isolation is physical the statement only ever touches this
3682
+ * graph's table, so no scoping predicate is needed.
3478
3683
  */
3479
3684
  async bulkDelete(filters, options) {
3480
- const stmt = compileBulkDelete(this.collectionPath, this.storageScope, filters);
3685
+ await this.ensureSchema();
3686
+ const stmt = compileBulkDelete(this.collectionPath, filters);
3481
3687
  return this.executeDmlWithReturning(stmt, options);
3482
3688
  }
3483
3689
  /**
@@ -3491,13 +3697,8 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3491
3697
  * transient driver errors.
3492
3698
  */
3493
3699
  async bulkUpdate(filters, patch, options) {
3494
- const stmt = compileBulkUpdate(
3495
- this.collectionPath,
3496
- this.storageScope,
3497
- filters,
3498
- patch.data,
3499
- Date.now()
3500
- );
3700
+ await this.ensureSchema();
3701
+ const stmt = compileBulkUpdate(this.collectionPath, filters, patch.data, Date.now());
3501
3702
  return this.executeDmlWithReturning(stmt, options);
3502
3703
  }
3503
3704
  /**
@@ -3522,8 +3723,8 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3522
3723
  if (params.sources.length === 0) {
3523
3724
  return params.hydrate ? { edges: [], targets: [] } : { edges: [] };
3524
3725
  }
3525
- const stmt = compileExpand(this.collectionPath, this.storageScope, params);
3526
- const rows = await this.executor.all(stmt.sql, stmt.params);
3726
+ const stmt = compileExpand(this.collectionPath, params);
3727
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
3527
3728
  const edges = rows.map(rowToRecord);
3528
3729
  if (!params.hydrate) {
3529
3730
  return { edges };
@@ -3534,7 +3735,7 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3534
3735
  if (uniqueTargets.length === 0) {
3535
3736
  return { edges, targets: [] };
3536
3737
  }
3537
- const hydrateStmt = compileExpandHydrate(this.collectionPath, this.storageScope, uniqueTargets);
3738
+ const hydrateStmt = compileExpandHydrate(this.collectionPath, uniqueTargets);
3538
3739
  const hydrateRows = await this.executor.all(hydrateStmt.sql, hydrateStmt.params);
3539
3740
  const byUid = /* @__PURE__ */ new Map();
3540
3741
  for (const row of hydrateRows) {
@@ -3563,12 +3764,11 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3563
3764
  async findEdgesProjected(select, filters, options) {
3564
3765
  const { stmt, columns } = compileFindEdgesProjected(
3565
3766
  this.collectionPath,
3566
- this.storageScope,
3567
3767
  select,
3568
3768
  filters,
3569
3769
  options
3570
3770
  );
3571
- const rows = await this.executor.all(stmt.sql, stmt.params);
3771
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
3572
3772
  return rows.map((row) => decodeProjectedRow(row, columns));
3573
3773
  }
3574
3774
  /**
@@ -3616,7 +3816,14 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3616
3816
  function createSqliteBackend(executor, tableName, options = {}) {
3617
3817
  const storageScope = options.storageScope ?? "";
3618
3818
  const scopePath = options.scopePath ?? "";
3619
- return new SqliteBackendImpl(executor, tableName, storageScope, scopePath);
3819
+ return new SqliteBackendImpl(
3820
+ executor,
3821
+ tableName,
3822
+ storageScope,
3823
+ scopePath,
3824
+ options.registry,
3825
+ options.coreIndexes
3826
+ );
3620
3827
  }
3621
3828
  // Annotate the CommonJS export names for ESM import in node:
3622
3829
  0 && (module.exports = {