@typicalday/firegraph 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/{backend-DuvHGgK1.d.cts → backend-BpYLdwCW.d.cts} +1 -1
- package/dist/{backend-DuvHGgK1.d.ts → backend-BpYLdwCW.d.ts} +1 -1
- package/dist/backend-CvImIwTY.d.cts +137 -0
- package/dist/backend-YH5HtawN.d.ts +137 -0
- package/dist/backend.d.cts +2 -2
- package/dist/backend.d.ts +2 -2
- package/dist/{chunk-3AHHXMWX.js → chunk-5HIRYV2S.js} +12 -35
- package/dist/chunk-5HIRYV2S.js.map +1 -0
- package/dist/{chunk-DJI3VXXA.js → chunk-7IEZ6IYY.js} +2 -2
- package/dist/chunk-7IEZ6IYY.js.map +1 -0
- package/dist/chunk-FODIMIWY.js +721 -0
- package/dist/chunk-FODIMIWY.js.map +1 -0
- package/dist/chunk-NGAJCALM.js +34 -0
- package/dist/chunk-NGAJCALM.js.map +1 -0
- package/dist/chunk-ULRDQ6HZ.js +862 -0
- package/dist/chunk-ULRDQ6HZ.js.map +1 -0
- package/dist/{client-BKi3vk0Q.d.ts → client-B5o39X79.d.ts} +1 -1
- package/dist/{client-BrsaXtDV.d.cts → client-BGHwxwPg.d.cts} +1 -1
- package/dist/{client-Bk2Cm6xv.d.cts → client-DoyEdJ5w.d.cts} +1 -1
- package/dist/{client-Bk2Cm6xv.d.ts → client-DoyEdJ5w.d.ts} +1 -1
- package/dist/cloudflare/index.cjs +148 -158
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +73 -70
- package/dist/cloudflare/index.d.ts +73 -70
- package/dist/cloudflare/index.js +53 -588
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/firestore-enterprise/index.cjs.map +1 -1
- package/dist/firestore-enterprise/index.d.cts +3 -3
- package/dist/firestore-enterprise/index.d.ts +3 -3
- package/dist/firestore-enterprise/index.js +5 -3
- package/dist/firestore-enterprise/index.js.map +1 -1
- package/dist/firestore-standard/index.cjs.map +1 -1
- package/dist/firestore-standard/index.d.cts +3 -3
- package/dist/firestore-standard/index.d.ts +3 -3
- package/dist/firestore-standard/index.js +3 -2
- package/dist/firestore-standard/index.js.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/query-client/index.d.cts +2 -2
- package/dist/query-client/index.d.ts +2 -2
- package/dist/{registry-Bc7h6WTM.d.cts → registry-BGh7Jqpb.d.cts} +2 -2
- package/dist/{registry-C2KUPVZj.d.ts → registry-tKTb5Kx1.d.ts} +2 -2
- package/dist/sqlite/index.cjs +578 -371
- package/dist/sqlite/index.cjs.map +1 -1
- package/dist/sqlite/index.d.cts +4 -110
- package/dist/sqlite/index.d.ts +4 -110
- package/dist/sqlite/index.js +7 -1144
- package/dist/sqlite/index.js.map +1 -1
- package/dist/sqlite/local.cjs +1835 -0
- package/dist/sqlite/local.cjs.map +1 -0
- package/dist/sqlite/local.d.cts +83 -0
- package/dist/sqlite/local.d.ts +83 -0
- package/dist/sqlite/local.js +121 -0
- package/dist/sqlite/local.js.map +1 -0
- package/package.json +15 -1
- package/dist/chunk-3AHHXMWX.js.map +0 -1
- package/dist/chunk-DJI3VXXA.js.map +0 -1
- package/dist/chunk-NNBSUOOF.js +0 -289
- package/dist/chunk-NNBSUOOF.js.map +0 -1
package/dist/sqlite/index.cjs
CHANGED
|
@@ -2308,6 +2308,186 @@ function createCapabilities(caps) {
|
|
|
2308
2308
|
};
|
|
2309
2309
|
}
|
|
2310
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
|
+
|
|
2311
2491
|
// src/internal/sqlite-data-ops.ts
|
|
2312
2492
|
var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
2313
2493
|
"Timestamp",
|
|
@@ -2321,7 +2501,7 @@ function isFirestoreSpecialType(value) {
|
|
|
2321
2501
|
if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
|
|
2322
2502
|
return null;
|
|
2323
2503
|
}
|
|
2324
|
-
var
|
|
2504
|
+
var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
2325
2505
|
function validateJsonPathKey(key, backendLabel) {
|
|
2326
2506
|
if (key.length === 0) {
|
|
2327
2507
|
throw new FiregraphError(
|
|
@@ -2329,7 +2509,7 @@ function validateJsonPathKey(key, backendLabel) {
|
|
|
2329
2509
|
"INVALID_QUERY"
|
|
2330
2510
|
);
|
|
2331
2511
|
}
|
|
2332
|
-
if (!
|
|
2512
|
+
if (!JSON_PATH_KEY_RE2.test(key)) {
|
|
2333
2513
|
throw new FiregraphError(
|
|
2334
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.).`,
|
|
2335
2515
|
"INVALID_QUERY"
|
|
@@ -2457,79 +2637,18 @@ function formatTagValue(value) {
|
|
|
2457
2637
|
return typeof value;
|
|
2458
2638
|
}
|
|
2459
2639
|
|
|
2460
|
-
// src/
|
|
2461
|
-
var
|
|
2462
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
2746
|
+
function compileSelect(table, filters, options) {
|
|
2628
2747
|
const params = [];
|
|
2629
|
-
const conditions = [
|
|
2630
|
-
params.push(scope);
|
|
2748
|
+
const conditions = [];
|
|
2631
2749
|
for (const f of filters) {
|
|
2632
2750
|
conditions.push(compileFilter(f, params));
|
|
2633
2751
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
sql +=
|
|
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
|
|
2641
|
-
|
|
2642
|
-
if (aliases.length === 0) {
|
|
2758
|
+
function compileExpand(table, params) {
|
|
2759
|
+
if (params.sources.length === 0) {
|
|
2643
2760
|
throw new FiregraphError(
|
|
2644
|
-
"
|
|
2761
|
+
"compileExpand requires a non-empty sources list \u2014 empty IN () is invalid SQL.",
|
|
2645
2762
|
"INVALID_QUERY"
|
|
2646
2763
|
);
|
|
2647
2764
|
}
|
|
2648
|
-
const
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
}
|
|
2662
|
-
|
|
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
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
conditions.push(compileFilter(f, params));
|
|
2781
|
+
if (params.bType !== void 0) {
|
|
2782
|
+
conditions.push(`${bTypeCol} = ?`);
|
|
2783
|
+
sqlParams.push(params.bType);
|
|
2684
2784
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
|
2689
|
-
if (
|
|
2799
|
+
function compileExpandHydrate(table, targetUids) {
|
|
2800
|
+
if (targetUids.length === 0) {
|
|
2690
2801
|
throw new FiregraphError(
|
|
2691
|
-
"
|
|
2802
|
+
"compileExpandHydrate requires a non-empty target list \u2014 empty IN () is invalid SQL.",
|
|
2692
2803
|
"INVALID_QUERY"
|
|
2693
2804
|
);
|
|
2694
2805
|
}
|
|
2695
|
-
const
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
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,
|
|
2817
|
+
function compileSelectByDocId(table, docId) {
|
|
2713
2818
|
return {
|
|
2714
|
-
sql: `SELECT * FROM ${
|
|
2715
|
-
params: [
|
|
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,
|
|
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 = [
|
|
2763
|
-
const conditions = [
|
|
2867
|
+
const params = [];
|
|
2868
|
+
const conditions = [];
|
|
2764
2869
|
for (const f of filters) {
|
|
2765
2870
|
conditions.push(compileFilter(f, params));
|
|
2766
2871
|
}
|
|
2767
|
-
|
|
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 =
|
|
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
|
|
2821
|
-
|
|
2926
|
+
function compileAggregate(table, spec, filters) {
|
|
2927
|
+
const aliases = Object.keys(spec);
|
|
2928
|
+
if (aliases.length === 0) {
|
|
2822
2929
|
throw new FiregraphError(
|
|
2823
|
-
"
|
|
2930
|
+
"aggregate() requires at least one aggregation in the `aggregates` map.",
|
|
2824
2931
|
"INVALID_QUERY"
|
|
2825
2932
|
);
|
|
2826
2933
|
}
|
|
2827
|
-
const
|
|
2828
|
-
const
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
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
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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
|
|
2869
|
-
const
|
|
2870
|
-
|
|
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,
|
|
2880
|
-
assertJsonSafePayload(record.data,
|
|
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 ${
|
|
2883
|
-
doc_id,
|
|
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,
|
|
2916
|
-
const sql = `INSERT INTO ${
|
|
2917
|
-
doc_id,
|
|
2918
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
2919
|
-
ON CONFLICT(
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
3053
|
+
params.push(docId);
|
|
2960
3054
|
return {
|
|
2961
|
-
sql: `UPDATE ${
|
|
3055
|
+
sql: `UPDATE ${quoteIdent2(table)} SET ${setClauses.join(", ")} WHERE "doc_id" = ?`,
|
|
2962
3056
|
params
|
|
2963
3057
|
};
|
|
2964
3058
|
}
|
|
2965
|
-
function compileDelete(table,
|
|
3059
|
+
function compileDelete(table, docId) {
|
|
2966
3060
|
return {
|
|
2967
|
-
sql: `DELETE FROM ${
|
|
2968
|
-
params: [
|
|
3061
|
+
sql: `DELETE FROM ${quoteIdent2(table)} WHERE "doc_id" = ?`,
|
|
3062
|
+
params: [docId]
|
|
2969
3063
|
};
|
|
2970
3064
|
}
|
|
2971
|
-
function compileBulkDelete(table,
|
|
2972
|
-
const params = [
|
|
2973
|
-
const conditions = [
|
|
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 ${
|
|
3073
|
+
sql: `DELETE FROM ${quoteIdent2(table)}${where}`,
|
|
2979
3074
|
params
|
|
2980
3075
|
};
|
|
2981
3076
|
}
|
|
2982
|
-
function compileBulkUpdate(table,
|
|
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,
|
|
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 = [
|
|
3009
|
-
const conditions = [
|
|
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 ${
|
|
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 =
|
|
3039
|
-
const updatedMs =
|
|
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
|
|
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")
|
|
3059
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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} (
|
|
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,
|
|
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,
|
|
3268
|
+
constructor(executor, tableName, ensureSchema) {
|
|
3136
3269
|
this.executor = executor;
|
|
3137
3270
|
this.tableName = tableName;
|
|
3138
|
-
this.
|
|
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,
|
|
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,
|
|
3302
|
+
constructor(executor, rootTable, storageScope, scopePath, registry, coreIndexes) {
|
|
3173
3303
|
this.executor = executor;
|
|
3174
|
-
|
|
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
|
-
/**
|
|
3318
|
+
/** Physical table holding this graph's triples. */
|
|
3185
3319
|
collectionPath;
|
|
3186
3320
|
scopePath;
|
|
3187
|
-
/**
|
|
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
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
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
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
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
|
-
|
|
3203
|
-
this.collectionPath,
|
|
3204
|
-
this.
|
|
3205
|
-
|
|
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
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
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
|
-
|
|
3225
|
-
|
|
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(
|
|
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(
|
|
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
|
|
3287
|
-
const
|
|
3288
|
-
const
|
|
3289
|
-
|
|
3290
|
-
|
|
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,
|
|
3522
|
+
(id) => compileDelete(this.collectionPath, id)
|
|
3294
3523
|
);
|
|
3295
|
-
writeStatements.push(compileDelete(this.collectionPath,
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
writeStatements.push(
|
|
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
|
|
3309
|
-
const deleted = stmtDeleted -
|
|
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
|
|
3338
|
-
* statement contributes 1 to that total even though it may
|
|
3339
|
-
* rows; `removeNodeCascade` patches that up with
|
|
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
|
-
//
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
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
|
-
|
|
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
|
|
3476
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
3495
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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 = {
|