@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
@@ -2048,7 +2048,7 @@ var GraphClientImpl = class _GraphClientImpl {
2048
2048
  async findNearest(params) {
2049
2049
  if (!this.backend.findNearest) {
2050
2050
  throw new FiregraphError(
2051
- "findNearest() is not supported by the current storage backend. Vector search requires a backend that declares `search.vector` (currently Firestore Standard and Enterprise). There is no client-side fallback because emulating ANN on top of the generic backend surface does not scale beyond toy datasets.",
2051
+ "findNearest() is not supported by the current storage backend. Vector search requires a backend that declares `search.vector` (currently Firestore Standard, Firestore Enterprise, and the local better-sqlite3 backend). There is no client-side fallback because emulating ANN on top of the generic backend surface does not scale beyond toy datasets.",
2052
2052
  "UNSUPPORTED_OPERATION"
2053
2053
  );
2054
2054
  }
@@ -2064,13 +2064,15 @@ var GraphClientImpl = class _GraphClientImpl {
2064
2064
  * Native full-text search (capability `search.fullText`).
2065
2065
  *
2066
2066
  * Returns the top-N records by relevance, ordered by the search
2067
- * index's score. Only Firestore Enterprise declares this capability
2068
- * today — the underlying Pipelines `search({ query: documentMatches(...) })`
2069
- * stage requires Enterprise's FTS index. Standard does not declare
2070
- * the cap (FTS is an Enterprise-only product feature, not a
2071
- * typed-API gap), and the SQLite-shaped backends have no native
2072
- * FTS index. Backends without `search.fullText` throw
2073
- * `UNSUPPORTED_OPERATION` from this wrapper.
2067
+ * index's score. Firestore Enterprise declares this capability (via
2068
+ * the Pipelines `search({ query: documentMatches(...) })` stage over
2069
+ * Enterprise's FTS index), as does the local better-sqlite3 backend
2070
+ * (`firegraph/sqlite-local`, via a trigger-synced FTS5 index ranked
2071
+ * by `bm25()`). Standard does not declare the cap (FTS is an
2072
+ * Enterprise-only product feature, not a typed-API gap); D1 and the
2073
+ * Cloudflare DO edition have no FTS trigger infrastructure. Backends
2074
+ * without `search.fullText` throw `UNSUPPORTED_OPERATION` from this
2075
+ * wrapper.
2074
2076
  *
2075
2077
  * Scan-protection mirrors `findNearest`: a search with no
2076
2078
  * identifying filters (`aType` / `axbType` / `bType`) walks every
@@ -2086,7 +2088,7 @@ var GraphClientImpl = class _GraphClientImpl {
2086
2088
  async fullTextSearch(params) {
2087
2089
  if (!this.backend.fullTextSearch) {
2088
2090
  throw new FiregraphError(
2089
- "fullTextSearch() is not supported by the current storage backend. Full-text search requires a backend that declares `search.fullText` (currently Firestore Enterprise only \u2014 FTS is an Enterprise product feature). There is no client-side fallback because emulating FTS over the generic backend surface would not scale beyond toy datasets.",
2091
+ "fullTextSearch() is not supported by the current storage backend. Full-text search requires a backend that declares `search.fullText` (currently Firestore Enterprise and the local better-sqlite3 backend). There is no client-side fallback because emulating FTS over the generic backend surface would not scale beyond toy datasets.",
2090
2092
  "UNSUPPORTED_OPERATION"
2091
2093
  );
2092
2094
  }
@@ -2308,6 +2310,186 @@ function createCapabilities(caps) {
2308
2310
  };
2309
2311
  }
2310
2312
 
2313
+ // src/default-indexes.ts
2314
+ var DEFAULT_CORE_INDEXES = Object.freeze([
2315
+ { fields: ["aUid"] },
2316
+ { fields: ["bUid"] },
2317
+ { fields: ["aType"] },
2318
+ { fields: ["bType"] },
2319
+ { fields: ["aUid", "axbType"] },
2320
+ { fields: ["axbType", "bUid"] },
2321
+ { fields: ["aType", "axbType"] },
2322
+ { fields: ["axbType", "bType"] }
2323
+ ]);
2324
+
2325
+ // src/internal/sqlite-index-ddl.ts
2326
+ var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2327
+ var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
2328
+ function quoteIdent(name) {
2329
+ if (!IDENT_RE.test(name)) {
2330
+ throw new FiregraphError(
2331
+ `Invalid SQL identifier in index DDL: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
2332
+ "INVALID_INDEX"
2333
+ );
2334
+ }
2335
+ return `"${name}"`;
2336
+ }
2337
+ function fnv1a32(str) {
2338
+ let h = 2166136261;
2339
+ for (let i = 0; i < str.length; i++) {
2340
+ h ^= str.charCodeAt(i);
2341
+ h = Math.imul(h, 16777619);
2342
+ }
2343
+ return (h >>> 0).toString(16).padStart(8, "0");
2344
+ }
2345
+ function normalizeFields(fields) {
2346
+ return fields.map((f) => {
2347
+ if (typeof f === "string") return { path: f, desc: false };
2348
+ if (!f.path || typeof f.path !== "string") {
2349
+ throw new FiregraphError(
2350
+ `IndexSpec field must be a string or { path: string, desc?: boolean }; got ${JSON.stringify(f)}`,
2351
+ "INVALID_INDEX"
2352
+ );
2353
+ }
2354
+ return { path: f.path, desc: !!f.desc };
2355
+ });
2356
+ }
2357
+ function specFingerprint(spec) {
2358
+ const normalized = {
2359
+ lead: [],
2360
+ fields: normalizeFields(spec.fields),
2361
+ where: spec.where ?? ""
2362
+ };
2363
+ return fnv1a32(JSON.stringify(normalized));
2364
+ }
2365
+ function compileFieldExpr(path, fieldToColumn) {
2366
+ const col = fieldToColumn[path];
2367
+ if (col) return quoteIdent(col);
2368
+ if (path === "data") {
2369
+ return `json_extract("data", '$')`;
2370
+ }
2371
+ if (path.startsWith("data.")) {
2372
+ const suffix = path.slice(5);
2373
+ const parts = suffix.split(".");
2374
+ for (const part of parts) {
2375
+ if (!JSON_PATH_KEY_RE.test(part)) {
2376
+ throw new FiregraphError(
2377
+ `IndexSpec data path "${path}" has invalid component "${part}". Each component must match /^[A-Za-z_][A-Za-z0-9_-]*$/.`,
2378
+ "INVALID_INDEX"
2379
+ );
2380
+ }
2381
+ }
2382
+ return `json_extract("data", '$.${suffix}')`;
2383
+ }
2384
+ throw new FiregraphError(
2385
+ `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'.`,
2386
+ "INVALID_INDEX"
2387
+ );
2388
+ }
2389
+ function buildIndexDDL(spec, options) {
2390
+ const { table, fieldToColumn } = options;
2391
+ if (!spec.fields || spec.fields.length === 0) {
2392
+ throw new FiregraphError("IndexSpec.fields must be a non-empty array", "INVALID_INDEX");
2393
+ }
2394
+ const normalized = normalizeFields(spec.fields);
2395
+ const hash = specFingerprint(spec);
2396
+ const indexName = `${table}_idx_${hash}`;
2397
+ const cols = [];
2398
+ for (const f of normalized) {
2399
+ const expr = compileFieldExpr(f.path, fieldToColumn);
2400
+ cols.push(f.desc ? `${expr} DESC` : expr);
2401
+ }
2402
+ let ddl = `CREATE INDEX IF NOT EXISTS ${quoteIdent(indexName)} ON ${quoteIdent(table)}(${cols.join(", ")})`;
2403
+ if (spec.where) {
2404
+ ddl += ` WHERE ${spec.where}`;
2405
+ }
2406
+ return ddl;
2407
+ }
2408
+ function dedupeIndexSpecs(specs) {
2409
+ const seen = /* @__PURE__ */ new Set();
2410
+ const out = [];
2411
+ for (const spec of specs) {
2412
+ const fp = specFingerprint(spec);
2413
+ if (seen.has(fp)) continue;
2414
+ seen.add(fp);
2415
+ out.push(spec);
2416
+ }
2417
+ return out;
2418
+ }
2419
+
2420
+ // src/internal/sqlite-schema.ts
2421
+ var FIELD_TO_COLUMN = {
2422
+ aType: "a_type",
2423
+ aUid: "a_uid",
2424
+ axbType: "axb_type",
2425
+ bType: "b_type",
2426
+ bUid: "b_uid",
2427
+ v: "v",
2428
+ createdAt: "created_at",
2429
+ updatedAt: "updated_at"
2430
+ };
2431
+ function buildSchemaStatements(table, options = {}) {
2432
+ const t = quoteIdent2(table);
2433
+ const statements = [
2434
+ `CREATE TABLE IF NOT EXISTS ${t} (
2435
+ doc_id TEXT NOT NULL PRIMARY KEY,
2436
+ a_type TEXT NOT NULL,
2437
+ a_uid TEXT NOT NULL,
2438
+ axb_type TEXT NOT NULL,
2439
+ b_type TEXT NOT NULL,
2440
+ b_uid TEXT NOT NULL,
2441
+ data TEXT NOT NULL,
2442
+ v INTEGER,
2443
+ created_at INTEGER NOT NULL,
2444
+ updated_at INTEGER NOT NULL
2445
+ )`
2446
+ ];
2447
+ const core = options.coreIndexes ?? [...DEFAULT_CORE_INDEXES];
2448
+ const fromRegistry = options.registry?.entries().flatMap((e) => e.indexes ?? []) ?? [];
2449
+ const deduped = dedupeIndexSpecs([...core, ...fromRegistry]);
2450
+ for (const spec of deduped) {
2451
+ statements.push(buildIndexDDL(spec, { table, fieldToColumn: FIELD_TO_COLUMN }));
2452
+ }
2453
+ return statements;
2454
+ }
2455
+ function quoteIdent2(name) {
2456
+ validateTableName(name);
2457
+ return `"${name}"`;
2458
+ }
2459
+ function validateTableName(name) {
2460
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2461
+ throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
2462
+ }
2463
+ }
2464
+ function quoteColumnAlias(label) {
2465
+ return `"${label.replace(/"/g, '""')}"`;
2466
+ }
2467
+
2468
+ // src/timestamp.ts
2469
+ var GraphTimestampImpl = class _GraphTimestampImpl {
2470
+ constructor(seconds, nanoseconds) {
2471
+ this.seconds = seconds;
2472
+ this.nanoseconds = nanoseconds;
2473
+ }
2474
+ toDate() {
2475
+ return new Date(this.toMillis());
2476
+ }
2477
+ toMillis() {
2478
+ return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
2479
+ }
2480
+ toJSON() {
2481
+ return { seconds: this.seconds, nanoseconds: this.nanoseconds };
2482
+ }
2483
+ static fromMillis(ms) {
2484
+ const seconds = Math.floor(ms / 1e3);
2485
+ const nanoseconds = (ms - seconds * 1e3) * 1e6;
2486
+ return new _GraphTimestampImpl(seconds, nanoseconds);
2487
+ }
2488
+ static now() {
2489
+ return _GraphTimestampImpl.fromMillis(Date.now());
2490
+ }
2491
+ };
2492
+
2311
2493
  // src/internal/sqlite-data-ops.ts
2312
2494
  var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
2313
2495
  "Timestamp",
@@ -2321,7 +2503,7 @@ function isFirestoreSpecialType(value) {
2321
2503
  if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
2322
2504
  return null;
2323
2505
  }
2324
- var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
2506
+ var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
2325
2507
  function validateJsonPathKey(key, backendLabel) {
2326
2508
  if (key.length === 0) {
2327
2509
  throw new FiregraphError(
@@ -2329,7 +2511,7 @@ function validateJsonPathKey(key, backendLabel) {
2329
2511
  "INVALID_QUERY"
2330
2512
  );
2331
2513
  }
2332
- if (!JSON_PATH_KEY_RE.test(key)) {
2514
+ if (!JSON_PATH_KEY_RE2.test(key)) {
2333
2515
  throw new FiregraphError(
2334
2516
  `${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.).`,
2335
2517
  "INVALID_QUERY"
@@ -2457,79 +2639,18 @@ function formatTagValue(value) {
2457
2639
  return typeof value;
2458
2640
  }
2459
2641
 
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";
2642
+ // src/internal/sqlite-sql.ts
2643
+ var BACKEND_LABEL = "SQLite";
2644
+ var BACKEND_ERR_LABEL = "SQLite backend";
2524
2645
  function compileFieldRef(field) {
2525
2646
  const column = FIELD_TO_COLUMN[field];
2526
2647
  if (column) {
2527
- return { expr: quoteIdent(column) };
2648
+ return { expr: quoteIdent2(column) };
2528
2649
  }
2529
2650
  if (field.startsWith("data.")) {
2530
2651
  const suffix = field.slice(5);
2531
2652
  for (const part of suffix.split(".")) {
2532
- validateJsonPathKey(part, SQLITE_BACKEND_ERR_LABEL);
2653
+ validateJsonPathKey(part, BACKEND_ERR_LABEL);
2533
2654
  }
2534
2655
  return { expr: `json_extract("data", '$.${suffix}')` };
2535
2656
  }
@@ -2624,95 +2745,81 @@ function compileLimit(options, params) {
2624
2745
  params.push(options.limit);
2625
2746
  return ` LIMIT ?`;
2626
2747
  }
2627
- function compileSelect(table, scope, filters, options) {
2748
+ function compileSelect(table, filters, options) {
2628
2749
  const params = [];
2629
- const conditions = ['"scope" = ?'];
2630
- params.push(scope);
2750
+ const conditions = [];
2631
2751
  for (const f of filters) {
2632
2752
  conditions.push(compileFilter(f, params));
2633
2753
  }
2634
- let sql = `SELECT * FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`;
2635
- const orderClause = compileOrderBy(options, params);
2636
- sql += orderClause;
2754
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2755
+ let sql = `SELECT * FROM ${quoteIdent2(table)}${where}`;
2756
+ sql += compileOrderBy(options, params);
2637
2757
  sql += compileLimit(options, params);
2638
2758
  return { sql, params };
2639
2759
  }
2640
- function compileAggregate(table, scope, spec, filters) {
2641
- const aliases = Object.keys(spec);
2642
- if (aliases.length === 0) {
2760
+ function compileExpand(table, params) {
2761
+ if (params.sources.length === 0) {
2643
2762
  throw new FiregraphError(
2644
- "aggregate() requires at least one aggregation in the `aggregates` map.",
2763
+ "compileExpand requires a non-empty sources list \u2014 empty IN () is invalid SQL.",
2645
2764
  "INVALID_QUERY"
2646
2765
  );
2647
2766
  }
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
- );
2767
+ const direction = params.direction ?? "forward";
2768
+ const aUidCol = compileFieldRef("aUid").expr;
2769
+ const bUidCol = compileFieldRef("bUid").expr;
2770
+ const aTypeCol = compileFieldRef("aType").expr;
2771
+ const bTypeCol = compileFieldRef("bType").expr;
2772
+ const axbTypeCol = compileFieldRef("axbType").expr;
2773
+ const sourceColumn = direction === "forward" ? aUidCol : bUidCol;
2774
+ const sqlParams = [params.axbType];
2775
+ const conditions = [`${axbTypeCol} = ?`];
2776
+ const placeholders = params.sources.map(() => "?").join(", ");
2777
+ conditions.push(`${sourceColumn} IN (${placeholders})`);
2778
+ for (const uid of params.sources) sqlParams.push(uid);
2779
+ if (params.aType !== void 0) {
2780
+ conditions.push(`${aTypeCol} = ?`);
2781
+ sqlParams.push(params.aType);
2679
2782
  }
2680
- const params = [scope];
2681
- const conditions = ['"scope" = ?'];
2682
- for (const f of filters) {
2683
- conditions.push(compileFilter(f, params));
2783
+ if (params.bType !== void 0) {
2784
+ conditions.push(`${bTypeCol} = ?`);
2785
+ sqlParams.push(params.bType);
2684
2786
  }
2685
- const sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`;
2686
- return { stmt: { sql, params }, aliases };
2787
+ if (params.axbType === NODE_RELATION) {
2788
+ conditions.push(`${aUidCol} != ${bUidCol}`);
2789
+ }
2790
+ let sql = `SELECT * FROM ${quoteIdent2(table)} WHERE ${conditions.join(" AND ")}`;
2791
+ if (params.orderBy) {
2792
+ sql += compileOrderBy({ orderBy: params.orderBy }, sqlParams);
2793
+ }
2794
+ if (params.limitPerSource !== void 0) {
2795
+ const totalLimit = params.sources.length * params.limitPerSource;
2796
+ sql += ` LIMIT ?`;
2797
+ sqlParams.push(totalLimit);
2798
+ }
2799
+ return { sql, params: sqlParams };
2687
2800
  }
2688
- function compileSelectGlobal(table, filters, options, scopeNameFilter) {
2689
- if (filters.length === 0) {
2801
+ function compileExpandHydrate(table, targetUids) {
2802
+ if (targetUids.length === 0) {
2690
2803
  throw new FiregraphError(
2691
- "compileSelectGlobal requires at least one filter \u2014 refusing to issue an unbounded SELECT.",
2804
+ "compileExpandHydrate requires a non-empty target list \u2014 empty IN () is invalid SQL.",
2692
2805
  "INVALID_QUERY"
2693
2806
  );
2694
2807
  }
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 };
2808
+ const placeholders = targetUids.map(() => "?").join(", ");
2809
+ const sqlParams = [NODE_RELATION];
2810
+ for (const uid of targetUids) sqlParams.push(uid);
2811
+ const aUidCol = compileFieldRef("aUid").expr;
2812
+ const bUidCol = compileFieldRef("bUid").expr;
2813
+ const axbTypeCol = compileFieldRef("axbType").expr;
2814
+ return {
2815
+ sql: `SELECT * FROM ${quoteIdent2(table)} WHERE ${axbTypeCol} = ? AND ${aUidCol} = ${bUidCol} AND ${bUidCol} IN (${placeholders})`,
2816
+ params: sqlParams
2817
+ };
2711
2818
  }
2712
- function compileSelectByDocId(table, scope, docId) {
2819
+ function compileSelectByDocId(table, docId) {
2713
2820
  return {
2714
- sql: `SELECT * FROM ${quoteIdent(table)} WHERE "scope" = ? AND "doc_id" = ? LIMIT 1`,
2715
- params: [scope, docId]
2821
+ sql: `SELECT * FROM ${quoteIdent2(table)} WHERE "doc_id" = ? LIMIT 1`,
2822
+ params: [docId]
2716
2823
  };
2717
2824
  }
2718
2825
  function normalizeProjectionField(field) {
@@ -2720,7 +2827,7 @@ function normalizeProjectionField(field) {
2720
2827
  if (field === "data" || field.startsWith("data.")) return field;
2721
2828
  return `data.${field}`;
2722
2829
  }
2723
- function compileFindEdgesProjected(table, scope, select, filters, options) {
2830
+ function compileFindEdgesProjected(table, select, filters, options) {
2724
2831
  if (select.length === 0) {
2725
2832
  throw new FiregraphError(
2726
2833
  "compileFindEdgesProjected requires a non-empty select list \u2014 an empty projection has no SQL representation distinct from `findEdges`.",
@@ -2759,12 +2866,13 @@ function compileFindEdgesProjected(table, scope, select, filters, options) {
2759
2866
  }
2760
2867
  columns.push({ field, kind, typeAlias: typeAliasName });
2761
2868
  }
2762
- const params = [scope];
2763
- const conditions = ['"scope" = ?'];
2869
+ const params = [];
2870
+ const conditions = [];
2764
2871
  for (const f of filters) {
2765
2872
  conditions.push(compileFilter(f, params));
2766
2873
  }
2767
- let sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`;
2874
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2875
+ let sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent2(table)}${where}`;
2768
2876
  sql += compileOrderBy(options, params);
2769
2877
  sql += compileLimit(options, params);
2770
2878
  return { stmt: { sql, params }, columns };
@@ -2789,7 +2897,7 @@ function decodeProjectedRow(row, columns) {
2789
2897
  }
2790
2898
  break;
2791
2899
  case "builtin-timestamp": {
2792
- const ms = toMillis(raw);
2900
+ const ms = rowTimestampToMillis(raw);
2793
2901
  out[c.field] = GraphTimestampImpl.fromMillis(ms);
2794
2902
  break;
2795
2903
  }
@@ -2817,74 +2925,63 @@ function decodeProjectedRow(row, columns) {
2817
2925
  }
2818
2926
  return out;
2819
2927
  }
2820
- function compileExpand(table, scope, params) {
2821
- if (params.sources.length === 0) {
2928
+ function compileAggregate(table, spec, filters) {
2929
+ const aliases = Object.keys(spec);
2930
+ if (aliases.length === 0) {
2822
2931
  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.",
2932
+ "aggregate() requires at least one aggregation in the `aggregates` map.",
2824
2933
  "INVALID_QUERY"
2825
2934
  );
2826
2935
  }
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);
2936
+ const projections = [];
2937
+ for (const alias of aliases) {
2938
+ const { op, field } = spec[alias];
2939
+ validateJsonPathKey(alias, BACKEND_ERR_LABEL);
2940
+ if (op === "count") {
2941
+ if (field !== void 0) {
2942
+ throw new FiregraphError(
2943
+ `Aggregate '${alias}' op 'count' must not specify a field \u2014 count operates on rows, not a column expression.`,
2944
+ "INVALID_QUERY"
2945
+ );
2946
+ }
2947
+ projections.push(`COUNT(*) AS ${quoteIdent2(alias)}`);
2948
+ continue;
2949
+ }
2950
+ if (!field) {
2951
+ throw new FiregraphError(
2952
+ `Aggregate '${alias}' op '${op}' requires a field.`,
2953
+ "INVALID_QUERY"
2954
+ );
2955
+ }
2956
+ const { expr } = compileFieldRef(field);
2957
+ const numeric = `CAST(${expr} AS REAL)`;
2958
+ if (op === "sum") projections.push(`SUM(${numeric}) AS ${quoteIdent2(alias)}`);
2959
+ else if (op === "avg") projections.push(`AVG(${numeric}) AS ${quoteIdent2(alias)}`);
2960
+ else if (op === "min") projections.push(`MIN(${numeric}) AS ${quoteIdent2(alias)}`);
2961
+ else if (op === "max") projections.push(`MAX(${numeric}) AS ${quoteIdent2(alias)}`);
2962
+ else
2963
+ throw new FiregraphError(
2964
+ `SQLite backend does not support aggregate op: ${String(op)}`,
2965
+ "INVALID_QUERY"
2966
+ );
2858
2967
  }
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
- );
2968
+ const params = [];
2969
+ const conditions = [];
2970
+ for (const f of filters) {
2971
+ conditions.push(compileFilter(f, params));
2867
2972
  }
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
- };
2973
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2974
+ const sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent2(table)}${where}`;
2975
+ return { stmt: { sql, params }, aliases };
2878
2976
  }
2879
- function compileSet(table, scope, docId, record, nowMillis, mode) {
2880
- assertJsonSafePayload(record.data, SQLITE_BACKEND_LABEL);
2977
+ function compileSet(table, docId, record, nowMillis, mode) {
2978
+ assertJsonSafePayload(record.data, BACKEND_LABEL);
2881
2979
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
2980
+ const sql2 = `INSERT OR REPLACE INTO ${quoteIdent2(table)} (
2981
+ doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
2982
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
2885
2983
  const params = [
2886
2984
  docId,
2887
- scope,
2888
2985
  record.aType,
2889
2986
  record.aUid,
2890
2987
  record.axbType,
@@ -2899,7 +2996,6 @@ function compileSet(table, scope, docId, record, nowMillis, mode) {
2899
2996
  }
2900
2997
  const insertParams = [
2901
2998
  docId,
2902
- scope,
2903
2999
  record.aType,
2904
3000
  record.aUid,
2905
3001
  record.axbType,
@@ -2912,11 +3008,11 @@ function compileSet(table, scope, docId, record, nowMillis, mode) {
2912
3008
  ];
2913
3009
  const ops = flattenPatch(record.data ?? {});
2914
3010
  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
3011
+ const dataExpr = compileDataOpsExpr(ops, `COALESCE("data", '{}')`, updateParams, BACKEND_ERR_LABEL) ?? `COALESCE("data", '{}')`;
3012
+ const sql = `INSERT INTO ${quoteIdent2(table)} (
3013
+ doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
3014
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3015
+ ON CONFLICT(doc_id) DO UPDATE SET
2920
3016
  "a_type" = excluded."a_type",
2921
3017
  "a_uid" = excluded."a_uid",
2922
3018
  "axb_type" = excluded."axb_type",
@@ -2928,23 +3024,23 @@ function compileSet(table, scope, docId, record, nowMillis, mode) {
2928
3024
  "updated_at" = excluded."updated_at"`;
2929
3025
  return { sql, params: [...insertParams, ...updateParams] };
2930
3026
  }
2931
- function compileUpdate(table, scope, docId, update, nowMillis) {
3027
+ function compileUpdate(table, docId, update, nowMillis) {
2932
3028
  assertUpdatePayloadExclusive(update);
2933
3029
  const setClauses = [];
2934
3030
  const params = [];
2935
3031
  if (update.replaceData) {
2936
- assertJsonSafePayload(update.replaceData, SQLITE_BACKEND_LABEL);
3032
+ assertJsonSafePayload(update.replaceData, BACKEND_LABEL);
2937
3033
  setClauses.push(`"data" = ?`);
2938
3034
  params.push(JSON.stringify(update.replaceData));
2939
3035
  } else if (update.dataOps && update.dataOps.length > 0) {
2940
3036
  for (const op of update.dataOps) {
2941
- if (!op.delete) assertJsonSafePayload(op.value, SQLITE_BACKEND_LABEL);
3037
+ if (!op.delete) assertJsonSafePayload(op.value, BACKEND_LABEL);
2942
3038
  }
2943
3039
  const expr = compileDataOpsExpr(
2944
3040
  update.dataOps,
2945
3041
  `COALESCE("data", '{}')`,
2946
3042
  params,
2947
- SQLITE_BACKEND_ERR_LABEL
3043
+ BACKEND_ERR_LABEL
2948
3044
  );
2949
3045
  if (expr !== null) {
2950
3046
  setClauses.push(`"data" = ${expr}`);
@@ -2956,30 +3052,31 @@ function compileUpdate(table, scope, docId, update, nowMillis) {
2956
3052
  }
2957
3053
  setClauses.push(`"updated_at" = ?`);
2958
3054
  params.push(nowMillis);
2959
- params.push(scope, docId);
3055
+ params.push(docId);
2960
3056
  return {
2961
- sql: `UPDATE ${quoteIdent(table)} SET ${setClauses.join(", ")} WHERE "scope" = ? AND "doc_id" = ?`,
3057
+ sql: `UPDATE ${quoteIdent2(table)} SET ${setClauses.join(", ")} WHERE "doc_id" = ?`,
2962
3058
  params
2963
3059
  };
2964
3060
  }
2965
- function compileDelete(table, scope, docId) {
3061
+ function compileDelete(table, docId) {
2966
3062
  return {
2967
- sql: `DELETE FROM ${quoteIdent(table)} WHERE "scope" = ? AND "doc_id" = ?`,
2968
- params: [scope, docId]
3063
+ sql: `DELETE FROM ${quoteIdent2(table)} WHERE "doc_id" = ?`,
3064
+ params: [docId]
2969
3065
  };
2970
3066
  }
2971
- function compileBulkDelete(table, scope, filters) {
2972
- const params = [scope];
2973
- const conditions = ['"scope" = ?'];
3067
+ function compileBulkDelete(table, filters) {
3068
+ const params = [];
3069
+ const conditions = [];
2974
3070
  for (const f of filters) {
2975
3071
  conditions.push(compileFilter(f, params));
2976
3072
  }
3073
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2977
3074
  return {
2978
- sql: `DELETE FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`,
3075
+ sql: `DELETE FROM ${quoteIdent2(table)}${where}`,
2979
3076
  params
2980
3077
  };
2981
3078
  }
2982
- function compileBulkUpdate(table, scope, filters, patchData, nowMillis) {
3079
+ function compileBulkUpdate(table, filters, patchData, nowMillis) {
2983
3080
  const dataOps = flattenPatch(patchData);
2984
3081
  if (dataOps.length === 0) {
2985
3082
  throw new FiregraphError(
@@ -2988,15 +3085,10 @@ function compileBulkUpdate(table, scope, filters, patchData, nowMillis) {
2988
3085
  );
2989
3086
  }
2990
3087
  for (const op of dataOps) {
2991
- if (!op.delete) assertJsonSafePayload(op.value, SQLITE_BACKEND_LABEL);
3088
+ if (!op.delete) assertJsonSafePayload(op.value, BACKEND_LABEL);
2992
3089
  }
2993
3090
  const setParams = [];
2994
- const expr = compileDataOpsExpr(
2995
- dataOps,
2996
- `COALESCE("data", '{}')`,
2997
- setParams,
2998
- SQLITE_BACKEND_ERR_LABEL
2999
- );
3091
+ const expr = compileDataOpsExpr(dataOps, `COALESCE("data", '{}')`, setParams, BACKEND_ERR_LABEL);
3000
3092
  if (expr === null) {
3001
3093
  throw new FiregraphError(
3002
3094
  "bulkUpdate() patch produced no SQL operations \u2014 internal invariant violated.",
@@ -3005,38 +3097,22 @@ function compileBulkUpdate(table, scope, filters, patchData, nowMillis) {
3005
3097
  }
3006
3098
  const setClauses = [`"data" = ${expr}`, `"updated_at" = ?`];
3007
3099
  setParams.push(nowMillis);
3008
- const whereParams = [scope];
3009
- const conditions = ['"scope" = ?'];
3100
+ const whereParams = [];
3101
+ const conditions = [];
3010
3102
  for (const f of filters) {
3011
3103
  conditions.push(compileFilter(f, whereParams));
3012
3104
  }
3105
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
3013
3106
  return {
3014
- sql: `UPDATE ${quoteIdent(table)} SET ${setClauses.join(", ")} WHERE ${conditions.join(" AND ")}`,
3107
+ sql: `UPDATE ${quoteIdent2(table)} SET ${setClauses.join(", ")}${where}`,
3015
3108
  params: [...setParams, ...whereParams]
3016
3109
  };
3017
3110
  }
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
3111
  function rowToRecord(row) {
3036
3112
  const dataString = row.data;
3037
3113
  const data = dataString ? JSON.parse(dataString) : {};
3038
- const createdMs = toMillis(row.created_at);
3039
- const updatedMs = toMillis(row.updated_at);
3114
+ const createdMs = rowTimestampToMillis(row.created_at);
3115
+ const updatedMs = rowTimestampToMillis(row.updated_at);
3040
3116
  const record = {
3041
3117
  aType: row.a_type,
3042
3118
  aUid: row.a_uid,
@@ -3052,11 +3128,71 @@ function rowToRecord(row) {
3052
3128
  }
3053
3129
  return record;
3054
3130
  }
3055
- function toMillis(value) {
3131
+ function rowTimestampToMillis(value) {
3056
3132
  if (typeof value === "number") return value;
3057
3133
  if (typeof value === "bigint") return Number(value);
3058
- if (typeof value === "string") return Number(value);
3059
- return 0;
3134
+ if (typeof value === "string") {
3135
+ const n = Number(value);
3136
+ if (Number.isFinite(n)) return n;
3137
+ }
3138
+ throw new FiregraphError(
3139
+ `SQLite row has non-numeric timestamp column: ${typeof value} (${String(value)})`,
3140
+ "INVALID_QUERY"
3141
+ );
3142
+ }
3143
+
3144
+ // src/sqlite/catalog.ts
3145
+ function catalogTableName(rootTable) {
3146
+ validateTableName(rootTable);
3147
+ return `${rootTable}_graphs`;
3148
+ }
3149
+ function mangleStorageScope(scope) {
3150
+ let out = "";
3151
+ for (const ch of scope) {
3152
+ if (/[A-Za-z0-9]/.test(ch)) out += ch;
3153
+ else if (ch === "_") out += "__";
3154
+ else if (ch === "-") out += "_h";
3155
+ else if (ch === "/") out += "_s";
3156
+ else out += `_u${ch.codePointAt(0).toString(16)}_`;
3157
+ }
3158
+ return out;
3159
+ }
3160
+ function tableForScope(rootTable, storageScope) {
3161
+ validateTableName(rootTable);
3162
+ if (storageScope === "") return rootTable;
3163
+ return `${rootTable}_g_${mangleStorageScope(storageScope)}`;
3164
+ }
3165
+ function escapeLikePrefix(prefix) {
3166
+ return prefix.replace(/[\\%_]/g, (c) => `\\${c}`);
3167
+ }
3168
+ function buildCatalogDDL(rootTable) {
3169
+ const t = quoteIdent2(catalogTableName(rootTable));
3170
+ return `CREATE TABLE IF NOT EXISTS ${t} (
3171
+ storage_scope TEXT NOT NULL PRIMARY KEY,
3172
+ table_name TEXT NOT NULL UNIQUE,
3173
+ scope_path TEXT NOT NULL
3174
+ )`;
3175
+ }
3176
+ function compileCatalogRegister(rootTable, storageScope, tableName, scopePath) {
3177
+ const t = quoteIdent2(catalogTableName(rootTable));
3178
+ return {
3179
+ sql: `INSERT OR IGNORE INTO ${t} (storage_scope, table_name, scope_path) VALUES (?, ?, ?)`,
3180
+ params: [storageScope, tableName, scopePath]
3181
+ };
3182
+ }
3183
+ function compileCatalogDescendants(rootTable, scopePrefix) {
3184
+ const t = quoteIdent2(catalogTableName(rootTable));
3185
+ return {
3186
+ sql: `SELECT storage_scope, table_name FROM ${t} WHERE storage_scope LIKE ? ESCAPE '\\' ORDER BY storage_scope`,
3187
+ params: [`${escapeLikePrefix(scopePrefix)}/%`]
3188
+ };
3189
+ }
3190
+ function compileCatalogDelete(rootTable, storageScope) {
3191
+ const t = quoteIdent2(catalogTableName(rootTable));
3192
+ return {
3193
+ sql: `DELETE FROM ${t} WHERE storage_scope = ?`,
3194
+ params: [storageScope]
3195
+ };
3060
3196
  }
3061
3197
 
3062
3198
  // src/sqlite/backend.ts
@@ -3096,63 +3232,59 @@ function chunkStatements(statements, maxStatements, maxParams) {
3096
3232
  return chunks;
3097
3233
  }
3098
3234
  var SqliteTransactionBackendImpl = class {
3099
- constructor(tx, tableName, storageScope) {
3235
+ constructor(tx, tableName) {
3100
3236
  this.tx = tx;
3101
3237
  this.tableName = tableName;
3102
- this.storageScope = storageScope;
3103
3238
  }
3104
3239
  async getDoc(docId) {
3105
- const stmt = compileSelectByDocId(this.tableName, this.storageScope, docId);
3240
+ const stmt = compileSelectByDocId(this.tableName, docId);
3106
3241
  const rows = await this.tx.all(stmt.sql, stmt.params);
3107
3242
  return rows.length === 0 ? null : rowToRecord(rows[0]);
3108
3243
  }
3109
3244
  async query(filters, options) {
3110
- const stmt = compileSelect(this.tableName, this.storageScope, filters, options);
3245
+ const stmt = compileSelect(this.tableName, filters, options);
3111
3246
  const rows = await this.tx.all(stmt.sql, stmt.params);
3112
3247
  return rows.map(rowToRecord);
3113
3248
  }
3114
3249
  async setDoc(docId, record, mode) {
3115
- const stmt = compileSet(this.tableName, this.storageScope, docId, record, Date.now(), mode);
3250
+ const stmt = compileSet(this.tableName, docId, record, Date.now(), mode);
3116
3251
  await this.tx.run(stmt.sql, stmt.params);
3117
3252
  }
3118
3253
  async updateDoc(docId, update) {
3119
- const stmt = compileUpdate(this.tableName, this.storageScope, docId, update, Date.now());
3254
+ const stmt = compileUpdate(this.tableName, docId, update, Date.now());
3120
3255
  const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
3121
3256
  const rows = await this.tx.all(sqlWithReturning, stmt.params);
3122
3257
  if (rows.length === 0) {
3123
3258
  throw new FiregraphError(
3124
- `updateDoc: no document found for doc_id=${docId} (scope=${this.storageScope})`,
3259
+ `updateDoc: no document found for doc_id=${docId} (table=${this.tableName})`,
3125
3260
  "NOT_FOUND"
3126
3261
  );
3127
3262
  }
3128
3263
  }
3129
3264
  async deleteDoc(docId) {
3130
- const stmt = compileDelete(this.tableName, this.storageScope, docId);
3265
+ const stmt = compileDelete(this.tableName, docId);
3131
3266
  await this.tx.run(stmt.sql, stmt.params);
3132
3267
  }
3133
3268
  };
3134
3269
  var SqliteBatchBackendImpl = class {
3135
- constructor(executor, tableName, storageScope) {
3270
+ constructor(executor, tableName, ensureSchema) {
3136
3271
  this.executor = executor;
3137
3272
  this.tableName = tableName;
3138
- this.storageScope = storageScope;
3273
+ this.ensureSchema = ensureSchema;
3139
3274
  }
3140
3275
  statements = [];
3141
3276
  setDoc(docId, record, mode) {
3142
- this.statements.push(
3143
- compileSet(this.tableName, this.storageScope, docId, record, Date.now(), mode)
3144
- );
3277
+ this.statements.push(compileSet(this.tableName, docId, record, Date.now(), mode));
3145
3278
  }
3146
3279
  updateDoc(docId, update) {
3147
- this.statements.push(
3148
- compileUpdate(this.tableName, this.storageScope, docId, update, Date.now())
3149
- );
3280
+ this.statements.push(compileUpdate(this.tableName, docId, update, Date.now()));
3150
3281
  }
3151
3282
  deleteDoc(docId) {
3152
- this.statements.push(compileDelete(this.tableName, this.storageScope, docId));
3283
+ this.statements.push(compileDelete(this.tableName, docId));
3153
3284
  }
3154
3285
  async commit() {
3155
3286
  if (this.statements.length === 0) return;
3287
+ await this.ensureSchema();
3156
3288
  await this.executor.batch(this.statements);
3157
3289
  this.statements.length = 0;
3158
3290
  }
@@ -3169,11 +3301,16 @@ var SQLITE_CORE_CAPS = [
3169
3301
  "raw.sql"
3170
3302
  ];
3171
3303
  var SqliteBackendImpl = class _SqliteBackendImpl {
3172
- constructor(executor, tableName, storageScope, scopePath) {
3304
+ constructor(executor, rootTable, storageScope, scopePath, registry, coreIndexes, extraTableDDL) {
3173
3305
  this.executor = executor;
3174
- this.collectionPath = tableName;
3306
+ validateTableName(rootTable);
3307
+ this.rootTable = rootTable;
3308
+ this.collectionPath = tableForScope(rootTable, storageScope);
3175
3309
  this.storageScope = storageScope;
3176
3310
  this.scopePath = scopePath;
3311
+ this.registry = registry;
3312
+ this.coreIndexes = coreIndexes;
3313
+ this.extraTableDDL = extraTableDDL;
3177
3314
  const caps = new Set(SQLITE_CORE_CAPS);
3178
3315
  if (typeof executor.transaction === "function") {
3179
3316
  caps.add("core.transactions");
@@ -3181,48 +3318,130 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3181
3318
  this.capabilities = createCapabilities(caps);
3182
3319
  }
3183
3320
  capabilities;
3184
- /** Logical table name (returned through `collectionPath` for parity with Firestore). */
3321
+ /** Physical table holding this graph's triples. */
3185
3322
  collectionPath;
3186
3323
  scopePath;
3187
- /** Materialized storage scope (interleaved parent UIDs + subgraph names). */
3324
+ /** Storage scope (interleaved parent UIDs + subgraph names) — `''` at root. */
3188
3325
  storageScope;
3326
+ /** Root graph's table name — prefix for subgraph tables and the catalog. */
3327
+ rootTable;
3328
+ registry;
3329
+ coreIndexes;
3330
+ extraTableDDL;
3331
+ ensured = null;
3332
+ /**
3333
+ * Lazily create this graph's table + indexes + the catalog, and register
3334
+ * the graph in the catalog. Runs once per backend instance; the DDL is
3335
+ * all `IF NOT EXISTS` / `INSERT OR IGNORE`, so concurrent instances over
3336
+ * the same database converge safely.
3337
+ */
3338
+ ensureSchema() {
3339
+ if (!this.ensured) {
3340
+ this.ensured = this.doEnsureSchema().catch((err) => {
3341
+ this.ensured = null;
3342
+ throw err;
3343
+ });
3344
+ }
3345
+ return this.ensured;
3346
+ }
3347
+ /** @internal See `SqliteStorageBackend.ensureReady`. */
3348
+ async ensureReady(force = false) {
3349
+ if (force) this.ensured = null;
3350
+ await this.ensureSchema();
3351
+ }
3352
+ async doEnsureSchema() {
3353
+ const ddl = [
3354
+ ...buildSchemaStatements(this.collectionPath, {
3355
+ coreIndexes: this.coreIndexes,
3356
+ registry: this.registry
3357
+ }),
3358
+ ...this.extraTableDDL ? this.extraTableDDL(this.collectionPath) : [],
3359
+ buildCatalogDDL(this.rootTable)
3360
+ ];
3361
+ const statements = ddl.map((sql) => ({ sql, params: [] }));
3362
+ statements.push(
3363
+ compileCatalogRegister(
3364
+ this.rootTable,
3365
+ this.storageScope,
3366
+ this.collectionPath,
3367
+ this.scopePath
3368
+ )
3369
+ );
3370
+ const chunks = chunkStatements(
3371
+ statements,
3372
+ this.executor.maxBatchSize,
3373
+ this.executor.maxBatchParams
3374
+ );
3375
+ for (const chunk of chunks) {
3376
+ await this.executor.batch(chunk);
3377
+ }
3378
+ }
3379
+ /**
3380
+ * Run `op` with the schema bootstrap applied, self-healing when this
3381
+ * graph's table was dropped out from under the instance — a parent's
3382
+ * cascade delete DROPs descendant tables, but subgraph handles created
3383
+ * before the cascade still point at this (now missing) table with a
3384
+ * resolved bootstrap cache. On a "no such table: <own table>" error the
3385
+ * cache resets, the empty graph is recreated, and the op retries once.
3386
+ * This matches Firestore semantics, where a deleted subcollection reads
3387
+ * as empty and writes recreate it.
3388
+ */
3389
+ async withSchema(op) {
3390
+ await this.ensureSchema();
3391
+ try {
3392
+ return await op();
3393
+ } catch (err) {
3394
+ if (!this.isMissingOwnTable(err)) throw err;
3395
+ this.ensured = null;
3396
+ await this.ensureSchema();
3397
+ return op();
3398
+ }
3399
+ }
3400
+ /** True when `err` is SQLite's missing-table error naming OUR table. */
3401
+ isMissingOwnTable(err) {
3402
+ const message = err instanceof Error ? err.message : String(err);
3403
+ return message.includes(`no such table: ${this.collectionPath}`);
3404
+ }
3189
3405
  // --- Reads ---
3190
3406
  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]);
3407
+ return this.withSchema(async () => {
3408
+ const stmt = compileSelectByDocId(this.collectionPath, docId);
3409
+ const rows = await this.executor.all(stmt.sql, stmt.params);
3410
+ return rows.length === 0 ? null : rowToRecord(rows[0]);
3411
+ });
3194
3412
  }
3195
3413
  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);
3414
+ return this.withSchema(async () => {
3415
+ const stmt = compileSelect(this.collectionPath, filters, options);
3416
+ const rows = await this.executor.all(stmt.sql, stmt.params);
3417
+ return rows.map(rowToRecord);
3418
+ });
3199
3419
  }
3200
3420
  // --- Writes ---
3201
3421
  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);
3422
+ return this.withSchema(async () => {
3423
+ const stmt = compileSet(this.collectionPath, docId, record, Date.now(), mode);
3424
+ await this.executor.run(stmt.sql, stmt.params);
3425
+ });
3211
3426
  }
3212
3427
  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
- }
3428
+ return this.withSchema(async () => {
3429
+ const stmt = compileUpdate(this.collectionPath, docId, update, Date.now());
3430
+ const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
3431
+ const rows = await this.executor.all(sqlWithReturning, stmt.params);
3432
+ if (rows.length === 0) {
3433
+ throw new FiregraphError(
3434
+ `updateDoc: no document found for doc_id=${docId} (table=${this.collectionPath})`,
3435
+ "NOT_FOUND"
3436
+ );
3437
+ }
3438
+ });
3222
3439
  }
3223
3440
  async deleteDoc(docId) {
3224
- const stmt = compileDelete(this.collectionPath, this.storageScope, docId);
3225
- await this.executor.run(stmt.sql, stmt.params);
3441
+ return this.withSchema(async () => {
3442
+ const stmt = compileDelete(this.collectionPath, docId);
3443
+ await this.executor.run(stmt.sql, stmt.params);
3444
+ });
3226
3445
  }
3227
3446
  // --- Transactions / Batches ---
3228
3447
  async runTransaction(fn) {
@@ -3232,17 +3451,18 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3232
3451
  "UNSUPPORTED_OPERATION"
3233
3452
  );
3234
3453
  }
3454
+ await this.ensureSchema();
3235
3455
  return this.executor.transaction(async (tx) => {
3236
- const txBackend = new SqliteTransactionBackendImpl(
3237
- tx,
3238
- this.collectionPath,
3239
- this.storageScope
3240
- );
3456
+ const txBackend = new SqliteTransactionBackendImpl(tx, this.collectionPath);
3241
3457
  return fn(txBackend);
3242
3458
  });
3243
3459
  }
3244
3460
  createBatch() {
3245
- return new SqliteBatchBackendImpl(this.executor, this.collectionPath, this.storageScope);
3461
+ return new SqliteBatchBackendImpl(
3462
+ this.executor,
3463
+ this.collectionPath,
3464
+ () => this.ensureSchema()
3465
+ );
3246
3466
  }
3247
3467
  // --- Subgraphs ---
3248
3468
  subgraph(parentNodeUid, name) {
@@ -3260,10 +3480,19 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3260
3480
  }
3261
3481
  const newStorageScope = this.storageScope ? `${this.storageScope}/${parentNodeUid}/${name}` : `${parentNodeUid}/${name}`;
3262
3482
  const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
3263
- return new _SqliteBackendImpl(this.executor, this.collectionPath, newStorageScope, newScope);
3483
+ return new _SqliteBackendImpl(
3484
+ this.executor,
3485
+ this.rootTable,
3486
+ newStorageScope,
3487
+ newScope,
3488
+ this.registry,
3489
+ this.coreIndexes,
3490
+ this.extraTableDDL
3491
+ );
3264
3492
  }
3265
3493
  // --- Cascade & bulk ---
3266
3494
  async removeNodeCascade(uid, reader, options) {
3495
+ await this.ensureSchema();
3267
3496
  const [outgoingRaw, incomingRaw] = await Promise.all([
3268
3497
  reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
3269
3498
  reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
@@ -3280,22 +3509,33 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3280
3509
  }
3281
3510
  const nodeDocId = computeNodeDocId(uid);
3282
3511
  const shouldDeleteSubgraphs = options?.deleteSubcollections !== false;
3512
+ const descendants = [];
3283
3513
  let subgraphRowCount = 0;
3284
3514
  if (shouldDeleteSubgraphs) {
3285
3515
  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);
3516
+ const descStmt = compileCatalogDescendants(this.rootTable, prefix);
3517
+ const rows = await this.executor.all(descStmt.sql, descStmt.params);
3518
+ for (const row of rows) {
3519
+ const tableName = String(row.table_name);
3520
+ validateTableName(tableName);
3521
+ descendants.push({ storageScope: String(row.storage_scope), tableName });
3522
+ }
3523
+ for (const d of descendants) {
3524
+ const countRows = await this.executor.all(
3525
+ `SELECT COUNT(*) AS n FROM ${quoteIdent2(d.tableName)}`,
3526
+ []
3527
+ );
3528
+ const n = countRows[0]?.n;
3529
+ subgraphRowCount += typeof n === "bigint" ? Number(n) : Number(n ?? 0);
3530
+ }
3291
3531
  }
3292
3532
  const writeStatements = edgeDocIds.map(
3293
- (id) => compileDelete(this.collectionPath, this.storageScope, id)
3533
+ (id) => compileDelete(this.collectionPath, id)
3294
3534
  );
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));
3535
+ writeStatements.push(compileDelete(this.collectionPath, nodeDocId));
3536
+ for (const d of descendants) {
3537
+ writeStatements.push({ sql: `DROP TABLE IF EXISTS ${quoteIdent2(d.tableName)}`, params: [] });
3538
+ writeStatements.push(compileCatalogDelete(this.rootTable, d.storageScope));
3299
3539
  }
3300
3540
  const {
3301
3541
  deleted: stmtDeleted,
@@ -3305,20 +3545,19 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3305
3545
  const allOk = errors.length === 0;
3306
3546
  const edgesDeleted = allOk ? edgeDocIds.length : 0;
3307
3547
  const nodeDeleted = allOk;
3308
- const prefixStatementContribution = shouldDeleteSubgraphs && allOk ? 1 : 0;
3309
- const deleted = stmtDeleted - prefixStatementContribution + (allOk ? subgraphRowCount : 0);
3548
+ const bookkeepingContribution = allOk ? descendants.length * 2 : 0;
3549
+ const deleted = stmtDeleted - bookkeepingContribution + (allOk ? subgraphRowCount : 0);
3310
3550
  return { deleted, batches, errors, edgesDeleted, nodeDeleted };
3311
3551
  }
3312
3552
  async bulkRemoveEdges(params, reader, options) {
3553
+ await this.ensureSchema();
3313
3554
  const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
3314
3555
  const edges = await reader.findEdges(effectiveParams);
3315
3556
  const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
3316
3557
  if (docIds.length === 0) {
3317
3558
  return { deleted: 0, batches: 0, errors: [] };
3318
3559
  }
3319
- const statements = docIds.map(
3320
- (id) => compileDelete(this.collectionPath, this.storageScope, id)
3321
- );
3560
+ const statements = docIds.map((id) => compileDelete(this.collectionPath, id));
3322
3561
  return this.executeChunkedBatches(statements, options);
3323
3562
  }
3324
3563
  /**
@@ -3334,9 +3573,9 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3334
3573
  * `result.errors.length` after the call.
3335
3574
  *
3336
3575
  * 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.
3576
+ * statement count of *successfully committed* batches — a DROP TABLE
3577
+ * statement contributes 1 to that total even though it may remove many
3578
+ * rows; `removeNodeCascade` patches that up with pre-counted row totals.
3340
3579
  *
3341
3580
  * **Atomicity caveat (D1):** when chunking kicks in, atomicity is lost
3342
3581
  * across chunk boundaries — one chunk may commit while a later one fails.
@@ -3398,29 +3637,12 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3398
3637
  }
3399
3638
  return { deleted, batches, errors };
3400
3639
  }
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
- }
3640
+ // `findEdgesGlobal` is deliberately NOT defined on this class. Each graph
3641
+ // is its own table, so a "collection group" query would mean scanning every
3642
+ // table listed in the catalog — an unbounded fan-out the cross-backend
3643
+ // contract treats as unsupported (the Cloudflare DO edition makes the same
3644
+ // call: no cross-DO index, no `findEdgesGlobal`). The client surfaces
3645
+ // `UNSUPPORTED_OPERATION` when the method is absent.
3424
3646
  // --- Aggregate ---
3425
3647
  /**
3426
3648
  * Run an aggregate query in a single SQL statement. Supports the full
@@ -3432,13 +3654,8 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3432
3654
  * convention and the Firestore Standard helper.
3433
3655
  */
3434
3656
  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);
3657
+ const { stmt, aliases } = compileAggregate(this.collectionPath, spec, filters);
3658
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
3442
3659
  const row = rows[0] ?? {};
3443
3660
  const out = {};
3444
3661
  for (const alias of aliases) {
@@ -3472,12 +3689,12 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3472
3689
  * congestion); a permanent failure is surfaced via the `errors` array
3473
3690
  * with `batchIndex: 0` so callers see the same shape as `bulkRemoveEdges`.
3474
3691
  *
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.
3692
+ * Subgraph isolation is physical the statement only ever touches this
3693
+ * graph's table, so no scoping predicate is needed.
3478
3694
  */
3479
3695
  async bulkDelete(filters, options) {
3480
- const stmt = compileBulkDelete(this.collectionPath, this.storageScope, filters);
3696
+ await this.ensureSchema();
3697
+ const stmt = compileBulkDelete(this.collectionPath, filters);
3481
3698
  return this.executeDmlWithReturning(stmt, options);
3482
3699
  }
3483
3700
  /**
@@ -3491,13 +3708,8 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3491
3708
  * transient driver errors.
3492
3709
  */
3493
3710
  async bulkUpdate(filters, patch, options) {
3494
- const stmt = compileBulkUpdate(
3495
- this.collectionPath,
3496
- this.storageScope,
3497
- filters,
3498
- patch.data,
3499
- Date.now()
3500
- );
3711
+ await this.ensureSchema();
3712
+ const stmt = compileBulkUpdate(this.collectionPath, filters, patch.data, Date.now());
3501
3713
  return this.executeDmlWithReturning(stmt, options);
3502
3714
  }
3503
3715
  /**
@@ -3522,8 +3734,8 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3522
3734
  if (params.sources.length === 0) {
3523
3735
  return params.hydrate ? { edges: [], targets: [] } : { edges: [] };
3524
3736
  }
3525
- const stmt = compileExpand(this.collectionPath, this.storageScope, params);
3526
- const rows = await this.executor.all(stmt.sql, stmt.params);
3737
+ const stmt = compileExpand(this.collectionPath, params);
3738
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
3527
3739
  const edges = rows.map(rowToRecord);
3528
3740
  if (!params.hydrate) {
3529
3741
  return { edges };
@@ -3534,7 +3746,7 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3534
3746
  if (uniqueTargets.length === 0) {
3535
3747
  return { edges, targets: [] };
3536
3748
  }
3537
- const hydrateStmt = compileExpandHydrate(this.collectionPath, this.storageScope, uniqueTargets);
3749
+ const hydrateStmt = compileExpandHydrate(this.collectionPath, uniqueTargets);
3538
3750
  const hydrateRows = await this.executor.all(hydrateStmt.sql, hydrateStmt.params);
3539
3751
  const byUid = /* @__PURE__ */ new Map();
3540
3752
  for (const row of hydrateRows) {
@@ -3563,12 +3775,11 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3563
3775
  async findEdgesProjected(select, filters, options) {
3564
3776
  const { stmt, columns } = compileFindEdgesProjected(
3565
3777
  this.collectionPath,
3566
- this.storageScope,
3567
3778
  select,
3568
3779
  filters,
3569
3780
  options
3570
3781
  );
3571
- const rows = await this.executor.all(stmt.sql, stmt.params);
3782
+ const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
3572
3783
  return rows.map((row) => decodeProjectedRow(row, columns));
3573
3784
  }
3574
3785
  /**
@@ -3616,7 +3827,15 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
3616
3827
  function createSqliteBackend(executor, tableName, options = {}) {
3617
3828
  const storageScope = options.storageScope ?? "";
3618
3829
  const scopePath = options.scopePath ?? "";
3619
- return new SqliteBackendImpl(executor, tableName, storageScope, scopePath);
3830
+ return new SqliteBackendImpl(
3831
+ executor,
3832
+ tableName,
3833
+ storageScope,
3834
+ scopePath,
3835
+ options.registry,
3836
+ options.coreIndexes,
3837
+ options.extraTableDDL
3838
+ );
3620
3839
  }
3621
3840
  // Annotate the CommonJS export names for ESM import in node:
3622
3841
  0 && (module.exports = {