@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.
- package/README.md +62 -20
- package/dist/backend-CE3pM9-T.d.ts +167 -0
- package/dist/{backend-DuvHGgK1.d.cts → backend-DNzv8KSR.d.cts} +34 -20
- package/dist/{backend-DuvHGgK1.d.ts → backend-DNzv8KSR.d.ts} +34 -20
- package/dist/backend-EjFfw9yO.d.cts +167 -0
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +2 -2
- package/dist/backend.d.ts +2 -2
- package/dist/backend.js +1 -1
- package/dist/chunk-5JBNLH5W.js +732 -0
- package/dist/chunk-5JBNLH5W.js.map +1 -0
- package/dist/{chunk-3AHHXMWX.js → chunk-6IO74NKD.js} +23 -44
- package/dist/chunk-6IO74NKD.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-NGAJCALM.js +34 -0
- package/dist/chunk-NGAJCALM.js.map +1 -0
- package/dist/chunk-NZVSLWNY.js +867 -0
- package/dist/chunk-NZVSLWNY.js.map +1 -0
- package/dist/{chunk-N5HFDWQX.js → chunk-PWIO46RT.js} +1 -1
- package/dist/{chunk-N5HFDWQX.js.map → chunk-PWIO46RT.js.map} +1 -1
- package/dist/{client-BKi3vk0Q.d.ts → client-CNAwJayO.d.ts} +1 -1
- package/dist/{client-BrsaXtDV.d.cts → client-CaXH5D5C.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 +159 -167
- 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 +54 -589
- 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 +11 -9
- 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 +6 -4
- package/dist/firestore-enterprise/index.js.map +1 -1
- package/dist/firestore-standard/index.cjs +11 -9
- 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 +4 -3
- package/dist/firestore-standard/index.js.map +1 -1
- package/dist/index.cjs +11 -9
- package/dist/index.cjs.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-C2KUPVZj.d.ts → registry-By1i-zge.d.ts} +2 -2
- package/dist/{registry-Bc7h6WTM.d.cts → registry-CNToyEra.d.cts} +2 -2
- package/dist/sqlite/index.cjs +599 -380
- 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 +2262 -0
- package/dist/sqlite/local.cjs.map +1 -0
- package/dist/sqlite/local.d.cts +109 -0
- package/dist/sqlite/local.d.ts +109 -0
- package/dist/sqlite/local.js +546 -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
|
@@ -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
|
|
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.
|
|
2068
|
-
*
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2071
|
-
*
|
|
2072
|
-
*
|
|
2073
|
-
*
|
|
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
|
|
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
|
|
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 (!
|
|
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/
|
|
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";
|
|
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:
|
|
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,
|
|
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,
|
|
2748
|
+
function compileSelect(table, filters, options) {
|
|
2628
2749
|
const params = [];
|
|
2629
|
-
const conditions = [
|
|
2630
|
-
params.push(scope);
|
|
2750
|
+
const conditions = [];
|
|
2631
2751
|
for (const f of filters) {
|
|
2632
2752
|
conditions.push(compileFilter(f, params));
|
|
2633
2753
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
sql +=
|
|
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
|
|
2641
|
-
|
|
2642
|
-
if (aliases.length === 0) {
|
|
2760
|
+
function compileExpand(table, params) {
|
|
2761
|
+
if (params.sources.length === 0) {
|
|
2643
2762
|
throw new FiregraphError(
|
|
2644
|
-
"
|
|
2763
|
+
"compileExpand requires a non-empty sources list \u2014 empty IN () is invalid SQL.",
|
|
2645
2764
|
"INVALID_QUERY"
|
|
2646
2765
|
);
|
|
2647
2766
|
}
|
|
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
|
-
);
|
|
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
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
conditions.push(compileFilter(f, params));
|
|
2783
|
+
if (params.bType !== void 0) {
|
|
2784
|
+
conditions.push(`${bTypeCol} = ?`);
|
|
2785
|
+
sqlParams.push(params.bType);
|
|
2684
2786
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
|
2689
|
-
if (
|
|
2801
|
+
function compileExpandHydrate(table, targetUids) {
|
|
2802
|
+
if (targetUids.length === 0) {
|
|
2690
2803
|
throw new FiregraphError(
|
|
2691
|
-
"
|
|
2804
|
+
"compileExpandHydrate requires a non-empty target list \u2014 empty IN () is invalid SQL.",
|
|
2692
2805
|
"INVALID_QUERY"
|
|
2693
2806
|
);
|
|
2694
2807
|
}
|
|
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 };
|
|
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,
|
|
2819
|
+
function compileSelectByDocId(table, docId) {
|
|
2713
2820
|
return {
|
|
2714
|
-
sql: `SELECT * FROM ${
|
|
2715
|
-
params: [
|
|
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,
|
|
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 = [
|
|
2763
|
-
const conditions = [
|
|
2869
|
+
const params = [];
|
|
2870
|
+
const conditions = [];
|
|
2764
2871
|
for (const f of filters) {
|
|
2765
2872
|
conditions.push(compileFilter(f, params));
|
|
2766
2873
|
}
|
|
2767
|
-
|
|
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 =
|
|
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
|
|
2821
|
-
|
|
2928
|
+
function compileAggregate(table, spec, filters) {
|
|
2929
|
+
const aliases = Object.keys(spec);
|
|
2930
|
+
if (aliases.length === 0) {
|
|
2822
2931
|
throw new FiregraphError(
|
|
2823
|
-
"
|
|
2932
|
+
"aggregate() requires at least one aggregation in the `aggregates` map.",
|
|
2824
2933
|
"INVALID_QUERY"
|
|
2825
2934
|
);
|
|
2826
2935
|
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
);
|
|
2968
|
+
const params = [];
|
|
2969
|
+
const conditions = [];
|
|
2970
|
+
for (const f of filters) {
|
|
2971
|
+
conditions.push(compileFilter(f, params));
|
|
2867
2972
|
}
|
|
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
|
-
};
|
|
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,
|
|
2880
|
-
assertJsonSafePayload(record.data,
|
|
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 ${
|
|
2883
|
-
doc_id,
|
|
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,
|
|
2916
|
-
const sql = `INSERT INTO ${
|
|
2917
|
-
doc_id,
|
|
2918
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
2919
|
-
ON CONFLICT(
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
3055
|
+
params.push(docId);
|
|
2960
3056
|
return {
|
|
2961
|
-
sql: `UPDATE ${
|
|
3057
|
+
sql: `UPDATE ${quoteIdent2(table)} SET ${setClauses.join(", ")} WHERE "doc_id" = ?`,
|
|
2962
3058
|
params
|
|
2963
3059
|
};
|
|
2964
3060
|
}
|
|
2965
|
-
function compileDelete(table,
|
|
3061
|
+
function compileDelete(table, docId) {
|
|
2966
3062
|
return {
|
|
2967
|
-
sql: `DELETE FROM ${
|
|
2968
|
-
params: [
|
|
3063
|
+
sql: `DELETE FROM ${quoteIdent2(table)} WHERE "doc_id" = ?`,
|
|
3064
|
+
params: [docId]
|
|
2969
3065
|
};
|
|
2970
3066
|
}
|
|
2971
|
-
function compileBulkDelete(table,
|
|
2972
|
-
const params = [
|
|
2973
|
-
const conditions = [
|
|
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 ${
|
|
3075
|
+
sql: `DELETE FROM ${quoteIdent2(table)}${where}`,
|
|
2979
3076
|
params
|
|
2980
3077
|
};
|
|
2981
3078
|
}
|
|
2982
|
-
function compileBulkUpdate(table,
|
|
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,
|
|
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 = [
|
|
3009
|
-
const conditions = [
|
|
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 ${
|
|
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 =
|
|
3039
|
-
const updatedMs =
|
|
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
|
|
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")
|
|
3059
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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} (
|
|
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,
|
|
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,
|
|
3270
|
+
constructor(executor, tableName, ensureSchema) {
|
|
3136
3271
|
this.executor = executor;
|
|
3137
3272
|
this.tableName = tableName;
|
|
3138
|
-
this.
|
|
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,
|
|
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,
|
|
3304
|
+
constructor(executor, rootTable, storageScope, scopePath, registry, coreIndexes, extraTableDDL) {
|
|
3173
3305
|
this.executor = executor;
|
|
3174
|
-
|
|
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
|
-
/**
|
|
3321
|
+
/** Physical table holding this graph's triples. */
|
|
3185
3322
|
collectionPath;
|
|
3186
3323
|
scopePath;
|
|
3187
|
-
/**
|
|
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
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
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
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
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
|
-
|
|
3203
|
-
this.collectionPath,
|
|
3204
|
-
this.
|
|
3205
|
-
|
|
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
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
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
|
-
|
|
3225
|
-
|
|
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(
|
|
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(
|
|
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
|
|
3287
|
-
const
|
|
3288
|
-
const
|
|
3289
|
-
|
|
3290
|
-
|
|
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,
|
|
3533
|
+
(id) => compileDelete(this.collectionPath, id)
|
|
3294
3534
|
);
|
|
3295
|
-
writeStatements.push(compileDelete(this.collectionPath,
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
writeStatements.push(
|
|
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
|
|
3309
|
-
const deleted = stmtDeleted -
|
|
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
|
|
3338
|
-
* statement contributes 1 to that total even though it may
|
|
3339
|
-
* rows; `removeNodeCascade` patches that up with
|
|
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
|
-
//
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
3476
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
3495
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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 = {
|