@typicalday/firegraph 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.cjs +2 -3
- 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-WRTFC5NG.js → chunk-5HIRYV2S.js} +13 -36
- package/dist/chunk-5HIRYV2S.js.map +1 -0
- package/dist/{chunk-PAD7WFFU.js → chunk-7IEZ6IYY.js} +36 -10
- 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-TK64DNVK.js → chunk-SIHE4UY4.js} +3 -4
- package/dist/chunk-SIHE4UY4.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 +155 -165
- 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 +42 -40
- 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 +19 -35
- package/dist/firestore-enterprise/index.js.map +1 -1
- package/dist/firestore-standard/index.cjs +34 -37
- 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 +10 -34
- package/dist/firestore-standard/index.js.map +1 -1
- package/dist/index.cjs +2 -3
- 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 +7 -5
- 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 +585 -378
- 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-4MMQ5W74.js +0 -288
- package/dist/chunk-4MMQ5W74.js.map +0 -1
- package/dist/chunk-PAD7WFFU.js.map +0 -1
- package/dist/chunk-TK64DNVK.js.map +0 -1
- package/dist/chunk-WRTFC5NG.js.map +0 -1
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSchemaStatements,
|
|
3
|
+
compileAggregate,
|
|
4
|
+
compileBulkDelete,
|
|
5
|
+
compileBulkUpdate,
|
|
6
|
+
compileDelete,
|
|
7
|
+
compileExpand,
|
|
8
|
+
compileExpandHydrate,
|
|
9
|
+
compileFindEdgesProjected,
|
|
10
|
+
compileSelect,
|
|
11
|
+
compileSelectByDocId,
|
|
12
|
+
compileSet,
|
|
13
|
+
compileUpdate,
|
|
14
|
+
decodeProjectedRow,
|
|
15
|
+
quoteIdent,
|
|
16
|
+
rowToRecord,
|
|
17
|
+
validateTableName
|
|
18
|
+
} from "./chunk-ULRDQ6HZ.js";
|
|
19
|
+
import {
|
|
20
|
+
createCapabilities
|
|
21
|
+
} from "./chunk-N5HFDWQX.js";
|
|
22
|
+
import {
|
|
23
|
+
NODE_RELATION,
|
|
24
|
+
computeEdgeDocId,
|
|
25
|
+
computeNodeDocId
|
|
26
|
+
} from "./chunk-NGAJCALM.js";
|
|
27
|
+
import {
|
|
28
|
+
FiregraphError
|
|
29
|
+
} from "./chunk-SIHE4UY4.js";
|
|
30
|
+
|
|
31
|
+
// src/sqlite/catalog.ts
|
|
32
|
+
function catalogTableName(rootTable) {
|
|
33
|
+
validateTableName(rootTable);
|
|
34
|
+
return `${rootTable}_graphs`;
|
|
35
|
+
}
|
|
36
|
+
function mangleStorageScope(scope) {
|
|
37
|
+
let out = "";
|
|
38
|
+
for (const ch of scope) {
|
|
39
|
+
if (/[A-Za-z0-9]/.test(ch)) out += ch;
|
|
40
|
+
else if (ch === "_") out += "__";
|
|
41
|
+
else if (ch === "-") out += "_h";
|
|
42
|
+
else if (ch === "/") out += "_s";
|
|
43
|
+
else out += `_u${ch.codePointAt(0).toString(16)}_`;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function tableForScope(rootTable, storageScope) {
|
|
48
|
+
validateTableName(rootTable);
|
|
49
|
+
if (storageScope === "") return rootTable;
|
|
50
|
+
return `${rootTable}_g_${mangleStorageScope(storageScope)}`;
|
|
51
|
+
}
|
|
52
|
+
function escapeLikePrefix(prefix) {
|
|
53
|
+
return prefix.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
54
|
+
}
|
|
55
|
+
function buildCatalogDDL(rootTable) {
|
|
56
|
+
const t = quoteIdent(catalogTableName(rootTable));
|
|
57
|
+
return `CREATE TABLE IF NOT EXISTS ${t} (
|
|
58
|
+
storage_scope TEXT NOT NULL PRIMARY KEY,
|
|
59
|
+
table_name TEXT NOT NULL UNIQUE,
|
|
60
|
+
scope_path TEXT NOT NULL
|
|
61
|
+
)`;
|
|
62
|
+
}
|
|
63
|
+
function compileCatalogRegister(rootTable, storageScope, tableName, scopePath) {
|
|
64
|
+
const t = quoteIdent(catalogTableName(rootTable));
|
|
65
|
+
return {
|
|
66
|
+
sql: `INSERT OR IGNORE INTO ${t} (storage_scope, table_name, scope_path) VALUES (?, ?, ?)`,
|
|
67
|
+
params: [storageScope, tableName, scopePath]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function compileCatalogDescendants(rootTable, scopePrefix) {
|
|
71
|
+
const t = quoteIdent(catalogTableName(rootTable));
|
|
72
|
+
return {
|
|
73
|
+
sql: `SELECT storage_scope, table_name FROM ${t} WHERE storage_scope LIKE ? ESCAPE '\\' ORDER BY storage_scope`,
|
|
74
|
+
params: [`${escapeLikePrefix(scopePrefix)}/%`]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function compileCatalogDelete(rootTable, storageScope) {
|
|
78
|
+
const t = quoteIdent(catalogTableName(rootTable));
|
|
79
|
+
return {
|
|
80
|
+
sql: `DELETE FROM ${t} WHERE storage_scope = ?`,
|
|
81
|
+
params: [storageScope]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/sqlite/backend.ts
|
|
86
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
87
|
+
var BASE_RETRY_DELAY_MS = 200;
|
|
88
|
+
var MAX_RETRY_DELAY_MS = 5e3;
|
|
89
|
+
function sleep(ms) {
|
|
90
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
91
|
+
}
|
|
92
|
+
function minDefined(a, b) {
|
|
93
|
+
if (a === void 0) return b;
|
|
94
|
+
if (b === void 0) return a;
|
|
95
|
+
return Math.min(a, b);
|
|
96
|
+
}
|
|
97
|
+
function chunkStatements(statements, maxStatements, maxParams) {
|
|
98
|
+
const stmtCap = maxStatements && maxStatements > 0 && Number.isFinite(maxStatements) ? Math.floor(maxStatements) : Infinity;
|
|
99
|
+
const paramCap = maxParams && maxParams > 0 && Number.isFinite(maxParams) ? Math.floor(maxParams) : Infinity;
|
|
100
|
+
if (stmtCap === Infinity && paramCap === Infinity) {
|
|
101
|
+
return [statements];
|
|
102
|
+
}
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let current = [];
|
|
105
|
+
let currentParamCount = 0;
|
|
106
|
+
for (const stmt of statements) {
|
|
107
|
+
const stmtParams = stmt.params.length;
|
|
108
|
+
const wouldExceedStmt = current.length + 1 > stmtCap;
|
|
109
|
+
const wouldExceedParam = currentParamCount + stmtParams > paramCap;
|
|
110
|
+
if (current.length > 0 && (wouldExceedStmt || wouldExceedParam)) {
|
|
111
|
+
chunks.push(current);
|
|
112
|
+
current = [];
|
|
113
|
+
currentParamCount = 0;
|
|
114
|
+
}
|
|
115
|
+
current.push(stmt);
|
|
116
|
+
currentParamCount += stmtParams;
|
|
117
|
+
}
|
|
118
|
+
if (current.length > 0) chunks.push(current);
|
|
119
|
+
return chunks;
|
|
120
|
+
}
|
|
121
|
+
var SqliteTransactionBackendImpl = class {
|
|
122
|
+
constructor(tx, tableName) {
|
|
123
|
+
this.tx = tx;
|
|
124
|
+
this.tableName = tableName;
|
|
125
|
+
}
|
|
126
|
+
async getDoc(docId) {
|
|
127
|
+
const stmt = compileSelectByDocId(this.tableName, docId);
|
|
128
|
+
const rows = await this.tx.all(stmt.sql, stmt.params);
|
|
129
|
+
return rows.length === 0 ? null : rowToRecord(rows[0]);
|
|
130
|
+
}
|
|
131
|
+
async query(filters, options) {
|
|
132
|
+
const stmt = compileSelect(this.tableName, filters, options);
|
|
133
|
+
const rows = await this.tx.all(stmt.sql, stmt.params);
|
|
134
|
+
return rows.map(rowToRecord);
|
|
135
|
+
}
|
|
136
|
+
async setDoc(docId, record, mode) {
|
|
137
|
+
const stmt = compileSet(this.tableName, docId, record, Date.now(), mode);
|
|
138
|
+
await this.tx.run(stmt.sql, stmt.params);
|
|
139
|
+
}
|
|
140
|
+
async updateDoc(docId, update) {
|
|
141
|
+
const stmt = compileUpdate(this.tableName, docId, update, Date.now());
|
|
142
|
+
const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
|
|
143
|
+
const rows = await this.tx.all(sqlWithReturning, stmt.params);
|
|
144
|
+
if (rows.length === 0) {
|
|
145
|
+
throw new FiregraphError(
|
|
146
|
+
`updateDoc: no document found for doc_id=${docId} (table=${this.tableName})`,
|
|
147
|
+
"NOT_FOUND"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async deleteDoc(docId) {
|
|
152
|
+
const stmt = compileDelete(this.tableName, docId);
|
|
153
|
+
await this.tx.run(stmt.sql, stmt.params);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
var SqliteBatchBackendImpl = class {
|
|
157
|
+
constructor(executor, tableName, ensureSchema) {
|
|
158
|
+
this.executor = executor;
|
|
159
|
+
this.tableName = tableName;
|
|
160
|
+
this.ensureSchema = ensureSchema;
|
|
161
|
+
}
|
|
162
|
+
statements = [];
|
|
163
|
+
setDoc(docId, record, mode) {
|
|
164
|
+
this.statements.push(compileSet(this.tableName, docId, record, Date.now(), mode));
|
|
165
|
+
}
|
|
166
|
+
updateDoc(docId, update) {
|
|
167
|
+
this.statements.push(compileUpdate(this.tableName, docId, update, Date.now()));
|
|
168
|
+
}
|
|
169
|
+
deleteDoc(docId) {
|
|
170
|
+
this.statements.push(compileDelete(this.tableName, docId));
|
|
171
|
+
}
|
|
172
|
+
async commit() {
|
|
173
|
+
if (this.statements.length === 0) return;
|
|
174
|
+
await this.ensureSchema();
|
|
175
|
+
await this.executor.batch(this.statements);
|
|
176
|
+
this.statements.length = 0;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var SQLITE_CORE_CAPS = [
|
|
180
|
+
"core.read",
|
|
181
|
+
"core.write",
|
|
182
|
+
"core.batch",
|
|
183
|
+
"core.subgraph",
|
|
184
|
+
"query.aggregate",
|
|
185
|
+
"query.dml",
|
|
186
|
+
"query.join",
|
|
187
|
+
"query.select",
|
|
188
|
+
"raw.sql"
|
|
189
|
+
];
|
|
190
|
+
var SqliteBackendImpl = class _SqliteBackendImpl {
|
|
191
|
+
constructor(executor, rootTable, storageScope, scopePath, registry, coreIndexes) {
|
|
192
|
+
this.executor = executor;
|
|
193
|
+
validateTableName(rootTable);
|
|
194
|
+
this.rootTable = rootTable;
|
|
195
|
+
this.collectionPath = tableForScope(rootTable, storageScope);
|
|
196
|
+
this.storageScope = storageScope;
|
|
197
|
+
this.scopePath = scopePath;
|
|
198
|
+
this.registry = registry;
|
|
199
|
+
this.coreIndexes = coreIndexes;
|
|
200
|
+
const caps = new Set(SQLITE_CORE_CAPS);
|
|
201
|
+
if (typeof executor.transaction === "function") {
|
|
202
|
+
caps.add("core.transactions");
|
|
203
|
+
}
|
|
204
|
+
this.capabilities = createCapabilities(caps);
|
|
205
|
+
}
|
|
206
|
+
capabilities;
|
|
207
|
+
/** Physical table holding this graph's triples. */
|
|
208
|
+
collectionPath;
|
|
209
|
+
scopePath;
|
|
210
|
+
/** Storage scope (interleaved parent UIDs + subgraph names) — `''` at root. */
|
|
211
|
+
storageScope;
|
|
212
|
+
/** Root graph's table name — prefix for subgraph tables and the catalog. */
|
|
213
|
+
rootTable;
|
|
214
|
+
registry;
|
|
215
|
+
coreIndexes;
|
|
216
|
+
ensured = null;
|
|
217
|
+
/**
|
|
218
|
+
* Lazily create this graph's table + indexes + the catalog, and register
|
|
219
|
+
* the graph in the catalog. Runs once per backend instance; the DDL is
|
|
220
|
+
* all `IF NOT EXISTS` / `INSERT OR IGNORE`, so concurrent instances over
|
|
221
|
+
* the same database converge safely.
|
|
222
|
+
*/
|
|
223
|
+
ensureSchema() {
|
|
224
|
+
if (!this.ensured) {
|
|
225
|
+
this.ensured = this.doEnsureSchema().catch((err) => {
|
|
226
|
+
this.ensured = null;
|
|
227
|
+
throw err;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return this.ensured;
|
|
231
|
+
}
|
|
232
|
+
async doEnsureSchema() {
|
|
233
|
+
const ddl = [
|
|
234
|
+
...buildSchemaStatements(this.collectionPath, {
|
|
235
|
+
coreIndexes: this.coreIndexes,
|
|
236
|
+
registry: this.registry
|
|
237
|
+
}),
|
|
238
|
+
buildCatalogDDL(this.rootTable)
|
|
239
|
+
];
|
|
240
|
+
const statements = ddl.map((sql) => ({ sql, params: [] }));
|
|
241
|
+
statements.push(
|
|
242
|
+
compileCatalogRegister(
|
|
243
|
+
this.rootTable,
|
|
244
|
+
this.storageScope,
|
|
245
|
+
this.collectionPath,
|
|
246
|
+
this.scopePath
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
const chunks = chunkStatements(
|
|
250
|
+
statements,
|
|
251
|
+
this.executor.maxBatchSize,
|
|
252
|
+
this.executor.maxBatchParams
|
|
253
|
+
);
|
|
254
|
+
for (const chunk of chunks) {
|
|
255
|
+
await this.executor.batch(chunk);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Run `op` with the schema bootstrap applied, self-healing when this
|
|
260
|
+
* graph's table was dropped out from under the instance — a parent's
|
|
261
|
+
* cascade delete DROPs descendant tables, but subgraph handles created
|
|
262
|
+
* before the cascade still point at this (now missing) table with a
|
|
263
|
+
* resolved bootstrap cache. On a "no such table: <own table>" error the
|
|
264
|
+
* cache resets, the empty graph is recreated, and the op retries once.
|
|
265
|
+
* This matches Firestore semantics, where a deleted subcollection reads
|
|
266
|
+
* as empty and writes recreate it.
|
|
267
|
+
*/
|
|
268
|
+
async withSchema(op) {
|
|
269
|
+
await this.ensureSchema();
|
|
270
|
+
try {
|
|
271
|
+
return await op();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (!this.isMissingOwnTable(err)) throw err;
|
|
274
|
+
this.ensured = null;
|
|
275
|
+
await this.ensureSchema();
|
|
276
|
+
return op();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** True when `err` is SQLite's missing-table error naming OUR table. */
|
|
280
|
+
isMissingOwnTable(err) {
|
|
281
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
282
|
+
return message.includes(`no such table: ${this.collectionPath}`);
|
|
283
|
+
}
|
|
284
|
+
// --- Reads ---
|
|
285
|
+
async getDoc(docId) {
|
|
286
|
+
return this.withSchema(async () => {
|
|
287
|
+
const stmt = compileSelectByDocId(this.collectionPath, docId);
|
|
288
|
+
const rows = await this.executor.all(stmt.sql, stmt.params);
|
|
289
|
+
return rows.length === 0 ? null : rowToRecord(rows[0]);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async query(filters, options) {
|
|
293
|
+
return this.withSchema(async () => {
|
|
294
|
+
const stmt = compileSelect(this.collectionPath, filters, options);
|
|
295
|
+
const rows = await this.executor.all(stmt.sql, stmt.params);
|
|
296
|
+
return rows.map(rowToRecord);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
// --- Writes ---
|
|
300
|
+
async setDoc(docId, record, mode) {
|
|
301
|
+
return this.withSchema(async () => {
|
|
302
|
+
const stmt = compileSet(this.collectionPath, docId, record, Date.now(), mode);
|
|
303
|
+
await this.executor.run(stmt.sql, stmt.params);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async updateDoc(docId, update) {
|
|
307
|
+
return this.withSchema(async () => {
|
|
308
|
+
const stmt = compileUpdate(this.collectionPath, docId, update, Date.now());
|
|
309
|
+
const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
|
|
310
|
+
const rows = await this.executor.all(sqlWithReturning, stmt.params);
|
|
311
|
+
if (rows.length === 0) {
|
|
312
|
+
throw new FiregraphError(
|
|
313
|
+
`updateDoc: no document found for doc_id=${docId} (table=${this.collectionPath})`,
|
|
314
|
+
"NOT_FOUND"
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async deleteDoc(docId) {
|
|
320
|
+
return this.withSchema(async () => {
|
|
321
|
+
const stmt = compileDelete(this.collectionPath, docId);
|
|
322
|
+
await this.executor.run(stmt.sql, stmt.params);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// --- Transactions / Batches ---
|
|
326
|
+
async runTransaction(fn) {
|
|
327
|
+
if (!this.executor.transaction) {
|
|
328
|
+
throw new FiregraphError(
|
|
329
|
+
"Interactive transactions are not supported by this SQLite driver. D1 in particular has no read-then-conditional-write transactions; use a Durable Object SQLite client instead, or rewrite the code path as a batch().",
|
|
330
|
+
"UNSUPPORTED_OPERATION"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
await this.ensureSchema();
|
|
334
|
+
return this.executor.transaction(async (tx) => {
|
|
335
|
+
const txBackend = new SqliteTransactionBackendImpl(tx, this.collectionPath);
|
|
336
|
+
return fn(txBackend);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
createBatch() {
|
|
340
|
+
return new SqliteBatchBackendImpl(
|
|
341
|
+
this.executor,
|
|
342
|
+
this.collectionPath,
|
|
343
|
+
() => this.ensureSchema()
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
// --- Subgraphs ---
|
|
347
|
+
subgraph(parentNodeUid, name) {
|
|
348
|
+
if (!parentNodeUid || parentNodeUid.includes("/")) {
|
|
349
|
+
throw new FiregraphError(
|
|
350
|
+
`Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
|
|
351
|
+
"INVALID_SUBGRAPH"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (!name || name.includes("/")) {
|
|
355
|
+
throw new FiregraphError(
|
|
356
|
+
`Subgraph name must not contain "/" and must be non-empty: got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
|
|
357
|
+
"INVALID_SUBGRAPH"
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const newStorageScope = this.storageScope ? `${this.storageScope}/${parentNodeUid}/${name}` : `${parentNodeUid}/${name}`;
|
|
361
|
+
const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
362
|
+
return new _SqliteBackendImpl(
|
|
363
|
+
this.executor,
|
|
364
|
+
this.rootTable,
|
|
365
|
+
newStorageScope,
|
|
366
|
+
newScope,
|
|
367
|
+
this.registry,
|
|
368
|
+
this.coreIndexes
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
// --- Cascade & bulk ---
|
|
372
|
+
async removeNodeCascade(uid, reader, options) {
|
|
373
|
+
await this.ensureSchema();
|
|
374
|
+
const [outgoingRaw, incomingRaw] = await Promise.all([
|
|
375
|
+
reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
|
|
376
|
+
reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
|
|
377
|
+
]);
|
|
378
|
+
const seen = /* @__PURE__ */ new Set();
|
|
379
|
+
const edgeDocIds = [];
|
|
380
|
+
for (const edge of [...outgoingRaw, ...incomingRaw]) {
|
|
381
|
+
if (edge.axbType === NODE_RELATION) continue;
|
|
382
|
+
const docId = computeEdgeDocId(edge.aUid, edge.axbType, edge.bUid);
|
|
383
|
+
if (!seen.has(docId)) {
|
|
384
|
+
seen.add(docId);
|
|
385
|
+
edgeDocIds.push(docId);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const nodeDocId = computeNodeDocId(uid);
|
|
389
|
+
const shouldDeleteSubgraphs = options?.deleteSubcollections !== false;
|
|
390
|
+
const descendants = [];
|
|
391
|
+
let subgraphRowCount = 0;
|
|
392
|
+
if (shouldDeleteSubgraphs) {
|
|
393
|
+
const prefix = this.storageScope ? `${this.storageScope}/${uid}` : uid;
|
|
394
|
+
const descStmt = compileCatalogDescendants(this.rootTable, prefix);
|
|
395
|
+
const rows = await this.executor.all(descStmt.sql, descStmt.params);
|
|
396
|
+
for (const row of rows) {
|
|
397
|
+
const tableName = String(row.table_name);
|
|
398
|
+
validateTableName(tableName);
|
|
399
|
+
descendants.push({ storageScope: String(row.storage_scope), tableName });
|
|
400
|
+
}
|
|
401
|
+
for (const d of descendants) {
|
|
402
|
+
const countRows = await this.executor.all(
|
|
403
|
+
`SELECT COUNT(*) AS n FROM ${quoteIdent(d.tableName)}`,
|
|
404
|
+
[]
|
|
405
|
+
);
|
|
406
|
+
const n = countRows[0]?.n;
|
|
407
|
+
subgraphRowCount += typeof n === "bigint" ? Number(n) : Number(n ?? 0);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const writeStatements = edgeDocIds.map(
|
|
411
|
+
(id) => compileDelete(this.collectionPath, id)
|
|
412
|
+
);
|
|
413
|
+
writeStatements.push(compileDelete(this.collectionPath, nodeDocId));
|
|
414
|
+
for (const d of descendants) {
|
|
415
|
+
writeStatements.push({ sql: `DROP TABLE IF EXISTS ${quoteIdent(d.tableName)}`, params: [] });
|
|
416
|
+
writeStatements.push(compileCatalogDelete(this.rootTable, d.storageScope));
|
|
417
|
+
}
|
|
418
|
+
const {
|
|
419
|
+
deleted: stmtDeleted,
|
|
420
|
+
batches,
|
|
421
|
+
errors
|
|
422
|
+
} = await this.executeChunkedBatches(writeStatements, options);
|
|
423
|
+
const allOk = errors.length === 0;
|
|
424
|
+
const edgesDeleted = allOk ? edgeDocIds.length : 0;
|
|
425
|
+
const nodeDeleted = allOk;
|
|
426
|
+
const bookkeepingContribution = allOk ? descendants.length * 2 : 0;
|
|
427
|
+
const deleted = stmtDeleted - bookkeepingContribution + (allOk ? subgraphRowCount : 0);
|
|
428
|
+
return { deleted, batches, errors, edgesDeleted, nodeDeleted };
|
|
429
|
+
}
|
|
430
|
+
async bulkRemoveEdges(params, reader, options) {
|
|
431
|
+
await this.ensureSchema();
|
|
432
|
+
const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
|
|
433
|
+
const edges = await reader.findEdges(effectiveParams);
|
|
434
|
+
const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
435
|
+
if (docIds.length === 0) {
|
|
436
|
+
return { deleted: 0, batches: 0, errors: [] };
|
|
437
|
+
}
|
|
438
|
+
const statements = docIds.map((id) => compileDelete(this.collectionPath, id));
|
|
439
|
+
return this.executeChunkedBatches(statements, options);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Submit `statements` to the executor as one or more `batch()` calls,
|
|
443
|
+
* chunking by `executor.maxBatchSize` (e.g. D1's ~100-statement cap).
|
|
444
|
+
* Drivers that don't advertise a cap submit everything in one batch,
|
|
445
|
+
* preserving cross-batch atomicity.
|
|
446
|
+
*
|
|
447
|
+
* Each chunk is retried with exponential backoff up to `maxRetries`
|
|
448
|
+
* (default 3) before being recorded in `errors`. The loop continues past
|
|
449
|
+
* a permanently failed chunk so the caller still gets partial progress
|
|
450
|
+
* visibility — to halt on first failure, set `maxRetries: 0` and check
|
|
451
|
+
* `result.errors.length` after the call.
|
|
452
|
+
*
|
|
453
|
+
* Returns `BulkResult`-shaped fields. `deleted` reflects only the
|
|
454
|
+
* statement count of *successfully committed* batches — a DROP TABLE
|
|
455
|
+
* statement contributes 1 to that total even though it may remove many
|
|
456
|
+
* rows; `removeNodeCascade` patches that up with pre-counted row totals.
|
|
457
|
+
*
|
|
458
|
+
* **Atomicity caveat (D1):** when chunking kicks in, atomicity is lost
|
|
459
|
+
* across chunk boundaries — one chunk may commit while a later one fails.
|
|
460
|
+
* `removeNodeCascade` is idempotent (deleting the same docs again is a
|
|
461
|
+
* no-op) so a caller can simply retry on partial failure. `bulkRemoveEdges`
|
|
462
|
+
* is also idempotent for the same reason. DO SQLite leaves `maxBatchSize`
|
|
463
|
+
* unset, so everything funnels through one atomic `transactionSync` and
|
|
464
|
+
* this caveat does not apply.
|
|
465
|
+
*/
|
|
466
|
+
async executeChunkedBatches(statements, options) {
|
|
467
|
+
if (statements.length === 0) {
|
|
468
|
+
return { deleted: 0, batches: 0, errors: [] };
|
|
469
|
+
}
|
|
470
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
471
|
+
const callerBatchSize = options?.batchSize;
|
|
472
|
+
const stmtCap = minDefined(callerBatchSize, this.executor.maxBatchSize);
|
|
473
|
+
const chunks = chunkStatements(statements, stmtCap, this.executor.maxBatchParams);
|
|
474
|
+
const errors = [];
|
|
475
|
+
let deleted = 0;
|
|
476
|
+
let batches = 0;
|
|
477
|
+
const totalBatches = chunks.length;
|
|
478
|
+
const driverParamCap = this.executor.maxBatchParams;
|
|
479
|
+
for (let batchIndex = 0; batchIndex < chunks.length; batchIndex++) {
|
|
480
|
+
const chunk = chunks[batchIndex];
|
|
481
|
+
const isUnretriableOversize = chunk.length === 1 && driverParamCap !== void 0 && chunk[0].params.length > driverParamCap;
|
|
482
|
+
let committed = false;
|
|
483
|
+
let lastError = null;
|
|
484
|
+
const effectiveRetries = isUnretriableOversize ? 0 : maxRetries;
|
|
485
|
+
for (let attempt = 0; attempt <= effectiveRetries; attempt++) {
|
|
486
|
+
try {
|
|
487
|
+
await this.executor.batch(chunk);
|
|
488
|
+
committed = true;
|
|
489
|
+
break;
|
|
490
|
+
} catch (err) {
|
|
491
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
492
|
+
if (attempt < effectiveRetries) {
|
|
493
|
+
const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), MAX_RETRY_DELAY_MS);
|
|
494
|
+
await sleep(delay);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (committed) {
|
|
499
|
+
deleted += chunk.length;
|
|
500
|
+
batches += 1;
|
|
501
|
+
} else if (lastError) {
|
|
502
|
+
errors.push({
|
|
503
|
+
batchIndex,
|
|
504
|
+
error: lastError,
|
|
505
|
+
operationCount: chunk.length
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (options?.onProgress) {
|
|
509
|
+
options.onProgress({
|
|
510
|
+
completedBatches: batches,
|
|
511
|
+
totalBatches,
|
|
512
|
+
deletedSoFar: deleted
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return { deleted, batches, errors };
|
|
517
|
+
}
|
|
518
|
+
// `findEdgesGlobal` is deliberately NOT defined on this class. Each graph
|
|
519
|
+
// is its own table, so a "collection group" query would mean scanning every
|
|
520
|
+
// table listed in the catalog — an unbounded fan-out the cross-backend
|
|
521
|
+
// contract treats as unsupported (the Cloudflare DO edition makes the same
|
|
522
|
+
// call: no cross-DO index, no `findEdgesGlobal`). The client surfaces
|
|
523
|
+
// `UNSUPPORTED_OPERATION` when the method is absent.
|
|
524
|
+
// --- Aggregate ---
|
|
525
|
+
/**
|
|
526
|
+
* Run an aggregate query in a single SQL statement. Supports the full
|
|
527
|
+
* count/sum/avg/min/max set — the SQLite engine evaluates each aggregate
|
|
528
|
+
* function over the filtered row set and the executor returns one row
|
|
529
|
+
* with one column per alias. SUM/MIN/MAX of an empty set returns 0
|
|
530
|
+
* (SQLite's `SUM(NULL) = NULL` is mapped to a clean number for the
|
|
531
|
+
* cross-backend contract); AVG returns NaN, matching the mathematical
|
|
532
|
+
* convention and the Firestore Standard helper.
|
|
533
|
+
*/
|
|
534
|
+
async aggregate(spec, filters) {
|
|
535
|
+
const { stmt, aliases } = compileAggregate(this.collectionPath, spec, filters);
|
|
536
|
+
const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
|
|
537
|
+
const row = rows[0] ?? {};
|
|
538
|
+
const out = {};
|
|
539
|
+
for (const alias of aliases) {
|
|
540
|
+
const v = row[alias];
|
|
541
|
+
if (v === null || v === void 0) {
|
|
542
|
+
const op = spec[alias].op;
|
|
543
|
+
out[alias] = op === "avg" ? Number.NaN : 0;
|
|
544
|
+
} else if (typeof v === "bigint") {
|
|
545
|
+
out[alias] = Number(v);
|
|
546
|
+
} else if (typeof v === "number") {
|
|
547
|
+
out[alias] = v;
|
|
548
|
+
} else {
|
|
549
|
+
out[alias] = Number(v);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
// --- Server-side DML ---
|
|
555
|
+
/**
|
|
556
|
+
* Delete every row matching `filters` in a single SQL DELETE statement.
|
|
557
|
+
*
|
|
558
|
+
* Uses `RETURNING "doc_id"` to count rows touched — the SQLite executor's
|
|
559
|
+
* `run` returns void, so RETURNING + `all()` is the portable way to learn
|
|
560
|
+
* how many rows the engine actually deleted. SQLite ≥ 3.35 supports
|
|
561
|
+
* `DELETE … RETURNING`; better-sqlite3, D1, and DO SQLite all run on a
|
|
562
|
+
* recent enough engine.
|
|
563
|
+
*
|
|
564
|
+
* Single-statement DML doesn't chunk: the engine handles N rows in one
|
|
565
|
+
* shot, so `BulkOptions.batchSize` is intentionally ignored. The retry
|
|
566
|
+
* loop here exists only for transient driver errors (e.g. D1 surface
|
|
567
|
+
* congestion); a permanent failure is surfaced via the `errors` array
|
|
568
|
+
* with `batchIndex: 0` so callers see the same shape as `bulkRemoveEdges`.
|
|
569
|
+
*
|
|
570
|
+
* Subgraph isolation is physical — the statement only ever touches this
|
|
571
|
+
* graph's table, so no scoping predicate is needed.
|
|
572
|
+
*/
|
|
573
|
+
async bulkDelete(filters, options) {
|
|
574
|
+
await this.ensureSchema();
|
|
575
|
+
const stmt = compileBulkDelete(this.collectionPath, filters);
|
|
576
|
+
return this.executeDmlWithReturning(stmt, options);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Update every row matching `filters` with `patch.data` in a single SQL
|
|
580
|
+
* UPDATE statement. The patch is deep-merged into each row's `data`
|
|
581
|
+
* column via the same `flattenPatch` → `compileDataOpsExpr` pipeline that
|
|
582
|
+
* `compileUpdate` (single-row) uses.
|
|
583
|
+
*
|
|
584
|
+
* Same contract notes as `bulkDelete` apply: single-statement, no
|
|
585
|
+
* chunking, `RETURNING "doc_id"` for the affected count, retry loop for
|
|
586
|
+
* transient driver errors.
|
|
587
|
+
*/
|
|
588
|
+
async bulkUpdate(filters, patch, options) {
|
|
589
|
+
await this.ensureSchema();
|
|
590
|
+
const stmt = compileBulkUpdate(this.collectionPath, filters, patch.data, Date.now());
|
|
591
|
+
return this.executeDmlWithReturning(stmt, options);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Multi-source fan-out — `query.join` capability.
|
|
595
|
+
*
|
|
596
|
+
* Issues a single `SELECT … WHERE "aUid" IN (?, ?, …)` statement that
|
|
597
|
+
* matches every edge from every source UID in one round trip. When
|
|
598
|
+
* `params.hydrate === true`, follows up with a second statement that
|
|
599
|
+
* fetches the target node rows; both queries hit the same table so
|
|
600
|
+
* the executor amortises connection / parsing cost across them.
|
|
601
|
+
*
|
|
602
|
+
* Empty `params.sources` short-circuits to an empty result without
|
|
603
|
+
* touching the executor — `IN ()` is not valid SQL.
|
|
604
|
+
*
|
|
605
|
+
* Per-source ordering / strict per-source LIMIT enforcement is NOT
|
|
606
|
+
* implemented here; see the `ExpandParams.limitPerSource` JSDoc and
|
|
607
|
+
* `compileExpand` for the cap semantics. Strict per-source caps would
|
|
608
|
+
* require window functions and were judged out of scope for the
|
|
609
|
+
* round-trip-collapse goal.
|
|
610
|
+
*/
|
|
611
|
+
async expand(params) {
|
|
612
|
+
if (params.sources.length === 0) {
|
|
613
|
+
return params.hydrate ? { edges: [], targets: [] } : { edges: [] };
|
|
614
|
+
}
|
|
615
|
+
const stmt = compileExpand(this.collectionPath, params);
|
|
616
|
+
const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
|
|
617
|
+
const edges = rows.map(rowToRecord);
|
|
618
|
+
if (!params.hydrate) {
|
|
619
|
+
return { edges };
|
|
620
|
+
}
|
|
621
|
+
const direction = params.direction ?? "forward";
|
|
622
|
+
const targetUids = edges.map((e) => direction === "forward" ? e.bUid : e.aUid);
|
|
623
|
+
const uniqueTargets = [...new Set(targetUids)];
|
|
624
|
+
if (uniqueTargets.length === 0) {
|
|
625
|
+
return { edges, targets: [] };
|
|
626
|
+
}
|
|
627
|
+
const hydrateStmt = compileExpandHydrate(this.collectionPath, uniqueTargets);
|
|
628
|
+
const hydrateRows = await this.executor.all(hydrateStmt.sql, hydrateStmt.params);
|
|
629
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
630
|
+
for (const row of hydrateRows) {
|
|
631
|
+
const node = rowToRecord(row);
|
|
632
|
+
byUid.set(node.bUid, node);
|
|
633
|
+
}
|
|
634
|
+
const targets = targetUids.map((uid) => byUid.get(uid) ?? null);
|
|
635
|
+
return { edges, targets };
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Server-side projection — `query.select` capability.
|
|
639
|
+
*
|
|
640
|
+
* Issues a single `SELECT json_extract(data, '$.f1'), …` statement that
|
|
641
|
+
* returns only the requested fields. The compiler emits one column per
|
|
642
|
+
* unique field plus a paired `json_type` column for `data.*` projections
|
|
643
|
+
* so the decoder can recover JSON-encoded objects/arrays without a
|
|
644
|
+
* second round trip. Migrations are NOT applied — the caller asked for
|
|
645
|
+
* a partial shape, and rehydrating that into the migration pipeline
|
|
646
|
+
* would require synthesising every absent field.
|
|
647
|
+
*
|
|
648
|
+
* The wire-payload reduction is the entire reason this method exists:
|
|
649
|
+
* a list view that only needs `title` / `date` no longer drags the
|
|
650
|
+
* full `data` JSON across the network. Callers that need the full
|
|
651
|
+
* record should use `findEdges` (with migration support).
|
|
652
|
+
*/
|
|
653
|
+
async findEdgesProjected(select, filters, options) {
|
|
654
|
+
const { stmt, columns } = compileFindEdgesProjected(
|
|
655
|
+
this.collectionPath,
|
|
656
|
+
select,
|
|
657
|
+
filters,
|
|
658
|
+
options
|
|
659
|
+
);
|
|
660
|
+
const rows = await this.withSchema(() => this.executor.all(stmt.sql, stmt.params));
|
|
661
|
+
return rows.map((row) => decodeProjectedRow(row, columns));
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Run a DML statement with `RETURNING "doc_id"` so we can count the
|
|
665
|
+
* rows the engine touched, with the same retry/backoff contract as
|
|
666
|
+
* `executeChunkedBatches`. Single statement, single batch.
|
|
667
|
+
*/
|
|
668
|
+
async executeDmlWithReturning(stmt, options) {
|
|
669
|
+
const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
|
|
670
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
671
|
+
let lastError = null;
|
|
672
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
673
|
+
try {
|
|
674
|
+
const rows = await this.executor.all(sqlWithReturning, stmt.params);
|
|
675
|
+
const deleted = rows.length;
|
|
676
|
+
if (options?.onProgress) {
|
|
677
|
+
options.onProgress({
|
|
678
|
+
completedBatches: 1,
|
|
679
|
+
totalBatches: 1,
|
|
680
|
+
deletedSoFar: deleted
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return { deleted, batches: 1, errors: [] };
|
|
684
|
+
} catch (err) {
|
|
685
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
686
|
+
if (attempt < maxRetries) {
|
|
687
|
+
const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), MAX_RETRY_DELAY_MS);
|
|
688
|
+
await sleep(delay);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
deleted: 0,
|
|
694
|
+
batches: 0,
|
|
695
|
+
errors: [
|
|
696
|
+
{
|
|
697
|
+
batchIndex: 0,
|
|
698
|
+
error: lastError ?? new Error("bulk DML failed for unknown reason"),
|
|
699
|
+
operationCount: 0
|
|
700
|
+
}
|
|
701
|
+
]
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
function createSqliteBackend(executor, tableName, options = {}) {
|
|
706
|
+
const storageScope = options.storageScope ?? "";
|
|
707
|
+
const scopePath = options.scopePath ?? "";
|
|
708
|
+
return new SqliteBackendImpl(
|
|
709
|
+
executor,
|
|
710
|
+
tableName,
|
|
711
|
+
storageScope,
|
|
712
|
+
scopePath,
|
|
713
|
+
options.registry,
|
|
714
|
+
options.coreIndexes
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export {
|
|
719
|
+
createSqliteBackend
|
|
720
|
+
};
|
|
721
|
+
//# sourceMappingURL=chunk-FODIMIWY.js.map
|