@typicalday/firegraph 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/{backend-DuvHGgK1.d.cts → backend-BpYLdwCW.d.cts} +1 -1
- package/dist/{backend-DuvHGgK1.d.ts → backend-BpYLdwCW.d.ts} +1 -1
- package/dist/backend-CvImIwTY.d.cts +137 -0
- package/dist/backend-YH5HtawN.d.ts +137 -0
- package/dist/backend.d.cts +2 -2
- package/dist/backend.d.ts +2 -2
- package/dist/{chunk-3AHHXMWX.js → chunk-5HIRYV2S.js} +12 -35
- package/dist/chunk-5HIRYV2S.js.map +1 -0
- package/dist/{chunk-DJI3VXXA.js → chunk-7IEZ6IYY.js} +2 -2
- package/dist/chunk-7IEZ6IYY.js.map +1 -0
- package/dist/chunk-FODIMIWY.js +721 -0
- package/dist/chunk-FODIMIWY.js.map +1 -0
- package/dist/chunk-NGAJCALM.js +34 -0
- package/dist/chunk-NGAJCALM.js.map +1 -0
- package/dist/chunk-ULRDQ6HZ.js +862 -0
- package/dist/chunk-ULRDQ6HZ.js.map +1 -0
- package/dist/{client-BKi3vk0Q.d.ts → client-B5o39X79.d.ts} +1 -1
- package/dist/{client-BrsaXtDV.d.cts → client-BGHwxwPg.d.cts} +1 -1
- package/dist/{client-Bk2Cm6xv.d.cts → client-DoyEdJ5w.d.cts} +1 -1
- package/dist/{client-Bk2Cm6xv.d.ts → client-DoyEdJ5w.d.ts} +1 -1
- package/dist/cloudflare/index.cjs +148 -158
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +73 -70
- package/dist/cloudflare/index.d.ts +73 -70
- package/dist/cloudflare/index.js +53 -588
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/firestore-enterprise/index.cjs.map +1 -1
- package/dist/firestore-enterprise/index.d.cts +3 -3
- package/dist/firestore-enterprise/index.d.ts +3 -3
- package/dist/firestore-enterprise/index.js +5 -3
- package/dist/firestore-enterprise/index.js.map +1 -1
- package/dist/firestore-standard/index.cjs.map +1 -1
- package/dist/firestore-standard/index.d.cts +3 -3
- package/dist/firestore-standard/index.d.ts +3 -3
- package/dist/firestore-standard/index.js +3 -2
- package/dist/firestore-standard/index.js.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/query-client/index.d.cts +2 -2
- package/dist/query-client/index.d.ts +2 -2
- package/dist/{registry-Bc7h6WTM.d.cts → registry-BGh7Jqpb.d.cts} +2 -2
- package/dist/{registry-C2KUPVZj.d.ts → registry-tKTb5Kx1.d.ts} +2 -2
- package/dist/sqlite/index.cjs +578 -371
- package/dist/sqlite/index.cjs.map +1 -1
- package/dist/sqlite/index.d.cts +4 -110
- package/dist/sqlite/index.d.ts +4 -110
- package/dist/sqlite/index.js +7 -1144
- package/dist/sqlite/index.js.map +1 -1
- package/dist/sqlite/local.cjs +1835 -0
- package/dist/sqlite/local.cjs.map +1 -0
- package/dist/sqlite/local.d.cts +83 -0
- package/dist/sqlite/local.d.ts +83 -0
- package/dist/sqlite/local.js +121 -0
- package/dist/sqlite/local.js.map +1 -0
- package/package.json +15 -1
- package/dist/chunk-3AHHXMWX.js.map +0 -1
- package/dist/chunk-DJI3VXXA.js.map +0 -1
- package/dist/chunk-NNBSUOOF.js +0 -289
- package/dist/chunk-NNBSUOOF.js.map +0 -1
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CORE_INDEXES
|
|
3
|
+
} from "./chunk-2DHMNTV6.js";
|
|
4
|
+
import {
|
|
5
|
+
NODE_RELATION
|
|
6
|
+
} from "./chunk-NGAJCALM.js";
|
|
7
|
+
import {
|
|
8
|
+
FiregraphError,
|
|
9
|
+
assertUpdatePayloadExclusive,
|
|
10
|
+
flattenPatch,
|
|
11
|
+
isDeleteSentinel
|
|
12
|
+
} from "./chunk-SIHE4UY4.js";
|
|
13
|
+
import {
|
|
14
|
+
SERIALIZATION_TAG
|
|
15
|
+
} from "./chunk-EQJUUVFG.js";
|
|
16
|
+
|
|
17
|
+
// src/internal/sqlite-index-ddl.ts
|
|
18
|
+
var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
19
|
+
var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
20
|
+
function quoteIdent(name) {
|
|
21
|
+
if (!IDENT_RE.test(name)) {
|
|
22
|
+
throw new FiregraphError(
|
|
23
|
+
`Invalid SQL identifier in index DDL: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
|
|
24
|
+
"INVALID_INDEX"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return `"${name}"`;
|
|
28
|
+
}
|
|
29
|
+
function fnv1a32(str) {
|
|
30
|
+
let h = 2166136261;
|
|
31
|
+
for (let i = 0; i < str.length; i++) {
|
|
32
|
+
h ^= str.charCodeAt(i);
|
|
33
|
+
h = Math.imul(h, 16777619);
|
|
34
|
+
}
|
|
35
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
36
|
+
}
|
|
37
|
+
function normalizeFields(fields) {
|
|
38
|
+
return fields.map((f) => {
|
|
39
|
+
if (typeof f === "string") return { path: f, desc: false };
|
|
40
|
+
if (!f.path || typeof f.path !== "string") {
|
|
41
|
+
throw new FiregraphError(
|
|
42
|
+
`IndexSpec field must be a string or { path: string, desc?: boolean }; got ${JSON.stringify(f)}`,
|
|
43
|
+
"INVALID_INDEX"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return { path: f.path, desc: !!f.desc };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function specFingerprint(spec) {
|
|
50
|
+
const normalized = {
|
|
51
|
+
lead: [],
|
|
52
|
+
fields: normalizeFields(spec.fields),
|
|
53
|
+
where: spec.where ?? ""
|
|
54
|
+
};
|
|
55
|
+
return fnv1a32(JSON.stringify(normalized));
|
|
56
|
+
}
|
|
57
|
+
function compileFieldExpr(path, fieldToColumn) {
|
|
58
|
+
const col = fieldToColumn[path];
|
|
59
|
+
if (col) return quoteIdent(col);
|
|
60
|
+
if (path === "data") {
|
|
61
|
+
return `json_extract("data", '$')`;
|
|
62
|
+
}
|
|
63
|
+
if (path.startsWith("data.")) {
|
|
64
|
+
const suffix = path.slice(5);
|
|
65
|
+
const parts = suffix.split(".");
|
|
66
|
+
for (const part of parts) {
|
|
67
|
+
if (!JSON_PATH_KEY_RE.test(part)) {
|
|
68
|
+
throw new FiregraphError(
|
|
69
|
+
`IndexSpec data path "${path}" has invalid component "${part}". Each component must match /^[A-Za-z_][A-Za-z0-9_-]*$/.`,
|
|
70
|
+
"INVALID_INDEX"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return `json_extract("data", '$.${suffix}')`;
|
|
75
|
+
}
|
|
76
|
+
throw new FiregraphError(
|
|
77
|
+
`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'.`,
|
|
78
|
+
"INVALID_INDEX"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function buildIndexDDL(spec, options) {
|
|
82
|
+
const { table, fieldToColumn } = options;
|
|
83
|
+
if (!spec.fields || spec.fields.length === 0) {
|
|
84
|
+
throw new FiregraphError("IndexSpec.fields must be a non-empty array", "INVALID_INDEX");
|
|
85
|
+
}
|
|
86
|
+
const normalized = normalizeFields(spec.fields);
|
|
87
|
+
const hash = specFingerprint(spec);
|
|
88
|
+
const indexName = `${table}_idx_${hash}`;
|
|
89
|
+
const cols = [];
|
|
90
|
+
for (const f of normalized) {
|
|
91
|
+
const expr = compileFieldExpr(f.path, fieldToColumn);
|
|
92
|
+
cols.push(f.desc ? `${expr} DESC` : expr);
|
|
93
|
+
}
|
|
94
|
+
let ddl = `CREATE INDEX IF NOT EXISTS ${quoteIdent(indexName)} ON ${quoteIdent(table)}(${cols.join(", ")})`;
|
|
95
|
+
if (spec.where) {
|
|
96
|
+
ddl += ` WHERE ${spec.where}`;
|
|
97
|
+
}
|
|
98
|
+
return ddl;
|
|
99
|
+
}
|
|
100
|
+
function dedupeIndexSpecs(specs) {
|
|
101
|
+
const seen = /* @__PURE__ */ new Set();
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const spec of specs) {
|
|
104
|
+
const fp = specFingerprint(spec);
|
|
105
|
+
if (seen.has(fp)) continue;
|
|
106
|
+
seen.add(fp);
|
|
107
|
+
out.push(spec);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/internal/sqlite-schema.ts
|
|
113
|
+
var FIELD_TO_COLUMN = {
|
|
114
|
+
aType: "a_type",
|
|
115
|
+
aUid: "a_uid",
|
|
116
|
+
axbType: "axb_type",
|
|
117
|
+
bType: "b_type",
|
|
118
|
+
bUid: "b_uid",
|
|
119
|
+
v: "v",
|
|
120
|
+
createdAt: "created_at",
|
|
121
|
+
updatedAt: "updated_at"
|
|
122
|
+
};
|
|
123
|
+
function buildSchemaStatements(table, options = {}) {
|
|
124
|
+
const t = quoteIdent2(table);
|
|
125
|
+
const statements = [
|
|
126
|
+
`CREATE TABLE IF NOT EXISTS ${t} (
|
|
127
|
+
doc_id TEXT NOT NULL PRIMARY KEY,
|
|
128
|
+
a_type TEXT NOT NULL,
|
|
129
|
+
a_uid TEXT NOT NULL,
|
|
130
|
+
axb_type TEXT NOT NULL,
|
|
131
|
+
b_type TEXT NOT NULL,
|
|
132
|
+
b_uid TEXT NOT NULL,
|
|
133
|
+
data TEXT NOT NULL,
|
|
134
|
+
v INTEGER,
|
|
135
|
+
created_at INTEGER NOT NULL,
|
|
136
|
+
updated_at INTEGER NOT NULL
|
|
137
|
+
)`
|
|
138
|
+
];
|
|
139
|
+
const core = options.coreIndexes ?? [...DEFAULT_CORE_INDEXES];
|
|
140
|
+
const fromRegistry = options.registry?.entries().flatMap((e) => e.indexes ?? []) ?? [];
|
|
141
|
+
const deduped = dedupeIndexSpecs([...core, ...fromRegistry]);
|
|
142
|
+
for (const spec of deduped) {
|
|
143
|
+
statements.push(buildIndexDDL(spec, { table, fieldToColumn: FIELD_TO_COLUMN }));
|
|
144
|
+
}
|
|
145
|
+
return statements;
|
|
146
|
+
}
|
|
147
|
+
function quoteIdent2(name) {
|
|
148
|
+
validateTableName(name);
|
|
149
|
+
return `"${name}"`;
|
|
150
|
+
}
|
|
151
|
+
function validateTableName(name) {
|
|
152
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
153
|
+
throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function quoteColumnAlias(label) {
|
|
157
|
+
return `"${label.replace(/"/g, '""')}"`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/timestamp.ts
|
|
161
|
+
var GraphTimestampImpl = class _GraphTimestampImpl {
|
|
162
|
+
constructor(seconds, nanoseconds) {
|
|
163
|
+
this.seconds = seconds;
|
|
164
|
+
this.nanoseconds = nanoseconds;
|
|
165
|
+
}
|
|
166
|
+
toDate() {
|
|
167
|
+
return new Date(this.toMillis());
|
|
168
|
+
}
|
|
169
|
+
toMillis() {
|
|
170
|
+
return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
|
|
171
|
+
}
|
|
172
|
+
toJSON() {
|
|
173
|
+
return { seconds: this.seconds, nanoseconds: this.nanoseconds };
|
|
174
|
+
}
|
|
175
|
+
static fromMillis(ms) {
|
|
176
|
+
const seconds = Math.floor(ms / 1e3);
|
|
177
|
+
const nanoseconds = (ms - seconds * 1e3) * 1e6;
|
|
178
|
+
return new _GraphTimestampImpl(seconds, nanoseconds);
|
|
179
|
+
}
|
|
180
|
+
static now() {
|
|
181
|
+
return _GraphTimestampImpl.fromMillis(Date.now());
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/internal/sqlite-data-ops.ts
|
|
186
|
+
var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
187
|
+
"Timestamp",
|
|
188
|
+
"GeoPoint",
|
|
189
|
+
"VectorValue",
|
|
190
|
+
"DocumentReference",
|
|
191
|
+
"FieldValue"
|
|
192
|
+
]);
|
|
193
|
+
function isFirestoreSpecialType(value) {
|
|
194
|
+
const ctorName = value.constructor?.name;
|
|
195
|
+
if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
199
|
+
function validateJsonPathKey(key, backendLabel) {
|
|
200
|
+
if (key.length === 0) {
|
|
201
|
+
throw new FiregraphError(
|
|
202
|
+
`${backendLabel}: empty JSON path component is not allowed`,
|
|
203
|
+
"INVALID_QUERY"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (!JSON_PATH_KEY_RE2.test(key)) {
|
|
207
|
+
throw new FiregraphError(
|
|
208
|
+
`${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.).`,
|
|
209
|
+
"INVALID_QUERY"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function buildJsonPath(segments) {
|
|
214
|
+
return "$" + segments.map((seg) => "." + JSON.stringify(seg)).join("");
|
|
215
|
+
}
|
|
216
|
+
function jsonBind(value, backendLabel) {
|
|
217
|
+
if (value === void 0) return "null";
|
|
218
|
+
if (value !== null && typeof value === "object") {
|
|
219
|
+
const firestoreType = isFirestoreSpecialType(value);
|
|
220
|
+
if (firestoreType) {
|
|
221
|
+
throw new FiregraphError(
|
|
222
|
+
`${backendLabel} cannot persist a Firestore ${firestoreType} value. Convert to a primitive before writing (e.g. \`ts.toMillis()\` for Timestamp).`,
|
|
223
|
+
"INVALID_ARGUMENT"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return JSON.stringify(value);
|
|
228
|
+
}
|
|
229
|
+
function compileDataOpsExpr(ops, base, params, backendLabel) {
|
|
230
|
+
if (ops.length === 0) return null;
|
|
231
|
+
const deletes = [];
|
|
232
|
+
const sets = [];
|
|
233
|
+
for (const op of ops) (op.delete ? deletes : sets).push(op);
|
|
234
|
+
let expr = base;
|
|
235
|
+
if (deletes.length > 0) {
|
|
236
|
+
const placeholders = deletes.map(() => "?").join(", ");
|
|
237
|
+
expr = `json_remove(${expr}, ${placeholders})`;
|
|
238
|
+
for (const op of deletes) {
|
|
239
|
+
params.push(buildJsonPath(op.path));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (sets.length > 0) {
|
|
243
|
+
const pieces = sets.map(() => "?, json(?)").join(", ");
|
|
244
|
+
expr = `json_set(${expr}, ${pieces})`;
|
|
245
|
+
for (const op of sets) {
|
|
246
|
+
params.push(buildJsonPath(op.path));
|
|
247
|
+
params.push(jsonBind(op.value, backendLabel));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return expr;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/internal/sqlite-payload-guard.ts
|
|
254
|
+
var FIRESTORE_TYPE_NAMES2 = /* @__PURE__ */ new Set([
|
|
255
|
+
"Timestamp",
|
|
256
|
+
"GeoPoint",
|
|
257
|
+
"VectorValue",
|
|
258
|
+
"DocumentReference",
|
|
259
|
+
"FieldValue"
|
|
260
|
+
]);
|
|
261
|
+
function assertJsonSafePayload(data, label) {
|
|
262
|
+
walk(data, [], label);
|
|
263
|
+
}
|
|
264
|
+
function walk(node, path, label) {
|
|
265
|
+
if (node === null || node === void 0) return;
|
|
266
|
+
if (isDeleteSentinel(node)) {
|
|
267
|
+
throw new FiregraphError(
|
|
268
|
+
`${label} backend cannot persist a deleteField() sentinel inside a full-data payload (replaceNode/replaceEdge or first-insert). The sentinel is only valid inside an updateNode/updateEdge dataOps patch. Path: ${formatPath(path)}.`,
|
|
269
|
+
"INVALID_ARGUMENT"
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const t = typeof node;
|
|
273
|
+
if (t === "symbol" || t === "function") {
|
|
274
|
+
throw new FiregraphError(
|
|
275
|
+
`${label} backend cannot persist a value of type ${t}. JSON.stringify drops it silently. Path: ${formatPath(path)}.`,
|
|
276
|
+
"INVALID_ARGUMENT"
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (t === "bigint") {
|
|
280
|
+
throw new FiregraphError(
|
|
281
|
+
`${label} backend cannot persist a value of type bigint. JSON.stringify cannot serialize this type (throws TypeError). Path: ${formatPath(path)}.`,
|
|
282
|
+
"INVALID_ARGUMENT"
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (t !== "object") return;
|
|
286
|
+
if (Array.isArray(node)) {
|
|
287
|
+
for (let i = 0; i < node.length; i++) {
|
|
288
|
+
walk(node[i], [...path, String(i)], label);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const obj = node;
|
|
293
|
+
if (Object.prototype.hasOwnProperty.call(obj, SERIALIZATION_TAG)) {
|
|
294
|
+
const tagValue = obj[SERIALIZATION_TAG];
|
|
295
|
+
throw new FiregraphError(
|
|
296
|
+
`${label} backend cannot persist an object with a \`${SERIALIZATION_TAG}\` key (value: ${formatTagValue(tagValue)}). Recognised tags are valid only on the Firestore backend (migration-sandbox output); a literal \`${SERIALIZATION_TAG}\` field in user data is reserved and not allowed. Path: ${formatPath(path)}.`,
|
|
297
|
+
"INVALID_ARGUMENT"
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const proto = Object.getPrototypeOf(node);
|
|
301
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
302
|
+
const ctor = node.constructor;
|
|
303
|
+
const ctorName = ctor && typeof ctor.name === "string" ? ctor.name : "<anonymous>";
|
|
304
|
+
if (FIRESTORE_TYPE_NAMES2.has(ctorName)) {
|
|
305
|
+
throw new FiregraphError(
|
|
306
|
+
`${label} backend cannot persist a Firestore ${ctorName} value. Convert to a primitive before writing (e.g. \`ts.toMillis()\` for Timestamp, \`{lat,lng}\` for GeoPoint). Path: ${formatPath(path)}.`,
|
|
307
|
+
"INVALID_ARGUMENT"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (node instanceof Date) return;
|
|
311
|
+
throw new FiregraphError(
|
|
312
|
+
`${label} backend cannot persist a class instance of type ${ctorName}. Only plain objects, arrays, and primitives round-trip safely through JSON storage. Path: ${formatPath(path)}.`,
|
|
313
|
+
"INVALID_ARGUMENT"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
for (const key of Object.keys(obj)) {
|
|
317
|
+
walk(obj[key], [...path, key], label);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function formatPath(path) {
|
|
321
|
+
return path.length === 0 ? "<root>" : path.map((p) => JSON.stringify(p)).join(" > ");
|
|
322
|
+
}
|
|
323
|
+
function formatTagValue(value) {
|
|
324
|
+
if (value === null) return "null";
|
|
325
|
+
if (value === void 0) return "undefined";
|
|
326
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
327
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
328
|
+
return String(value);
|
|
329
|
+
}
|
|
330
|
+
return typeof value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/internal/sqlite-sql.ts
|
|
334
|
+
var BACKEND_LABEL = "SQLite";
|
|
335
|
+
var BACKEND_ERR_LABEL = "SQLite backend";
|
|
336
|
+
function compileFieldRef(field) {
|
|
337
|
+
const column = FIELD_TO_COLUMN[field];
|
|
338
|
+
if (column) {
|
|
339
|
+
return { expr: quoteIdent2(column) };
|
|
340
|
+
}
|
|
341
|
+
if (field.startsWith("data.")) {
|
|
342
|
+
const suffix = field.slice(5);
|
|
343
|
+
for (const part of suffix.split(".")) {
|
|
344
|
+
validateJsonPathKey(part, BACKEND_ERR_LABEL);
|
|
345
|
+
}
|
|
346
|
+
return { expr: `json_extract("data", '$.${suffix}')` };
|
|
347
|
+
}
|
|
348
|
+
if (field === "data") {
|
|
349
|
+
return { expr: `json_extract("data", '$')` };
|
|
350
|
+
}
|
|
351
|
+
throw new FiregraphError(`SQLite backend cannot resolve filter field: ${field}`, "INVALID_QUERY");
|
|
352
|
+
}
|
|
353
|
+
function bindValue(value) {
|
|
354
|
+
if (value === null || value === void 0) return null;
|
|
355
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
if (value instanceof Date) return value.getTime();
|
|
359
|
+
if (typeof value === "object") {
|
|
360
|
+
const firestoreType = isFirestoreSpecialType(value);
|
|
361
|
+
if (firestoreType) {
|
|
362
|
+
throw new FiregraphError(
|
|
363
|
+
`SQLite backend cannot bind a Firestore ${firestoreType} value \u2014 JSON serialization would silently drop fields and the resulting bind would never match a stored row. Convert to a primitive (e.g. \`ts.toMillis()\` for Timestamp) before filtering or updating.`,
|
|
364
|
+
"INVALID_QUERY"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return JSON.stringify(value);
|
|
368
|
+
}
|
|
369
|
+
return String(value);
|
|
370
|
+
}
|
|
371
|
+
function compileFilter(filter, params) {
|
|
372
|
+
const { expr } = compileFieldRef(filter.field);
|
|
373
|
+
switch (filter.op) {
|
|
374
|
+
case "==":
|
|
375
|
+
params.push(bindValue(filter.value));
|
|
376
|
+
return `${expr} = ?`;
|
|
377
|
+
case "!=":
|
|
378
|
+
params.push(bindValue(filter.value));
|
|
379
|
+
return `${expr} != ?`;
|
|
380
|
+
case "<":
|
|
381
|
+
params.push(bindValue(filter.value));
|
|
382
|
+
return `${expr} < ?`;
|
|
383
|
+
case "<=":
|
|
384
|
+
params.push(bindValue(filter.value));
|
|
385
|
+
return `${expr} <= ?`;
|
|
386
|
+
case ">":
|
|
387
|
+
params.push(bindValue(filter.value));
|
|
388
|
+
return `${expr} > ?`;
|
|
389
|
+
case ">=":
|
|
390
|
+
params.push(bindValue(filter.value));
|
|
391
|
+
return `${expr} >= ?`;
|
|
392
|
+
case "in": {
|
|
393
|
+
const values = asArray(filter.value, "in");
|
|
394
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
395
|
+
for (const v of values) params.push(bindValue(v));
|
|
396
|
+
return `${expr} IN (${placeholders})`;
|
|
397
|
+
}
|
|
398
|
+
case "not-in": {
|
|
399
|
+
const values = asArray(filter.value, "not-in");
|
|
400
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
401
|
+
for (const v of values) params.push(bindValue(v));
|
|
402
|
+
return `${expr} NOT IN (${placeholders})`;
|
|
403
|
+
}
|
|
404
|
+
case "array-contains": {
|
|
405
|
+
params.push(bindValue(filter.value));
|
|
406
|
+
return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value = ?)`;
|
|
407
|
+
}
|
|
408
|
+
case "array-contains-any": {
|
|
409
|
+
const values = asArray(filter.value, "array-contains-any");
|
|
410
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
411
|
+
for (const v of values) params.push(bindValue(v));
|
|
412
|
+
return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value IN (${placeholders}))`;
|
|
413
|
+
}
|
|
414
|
+
default:
|
|
415
|
+
throw new FiregraphError(
|
|
416
|
+
`SQLite backend does not support filter operator: ${String(filter.op)}`,
|
|
417
|
+
"INVALID_QUERY"
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function asArray(value, op) {
|
|
422
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
423
|
+
throw new FiregraphError(`Operator "${op}" requires a non-empty array value`, "INVALID_QUERY");
|
|
424
|
+
}
|
|
425
|
+
return value;
|
|
426
|
+
}
|
|
427
|
+
function compileOrderBy(options, _params) {
|
|
428
|
+
if (!options?.orderBy) return "";
|
|
429
|
+
const { field, direction } = options.orderBy;
|
|
430
|
+
const { expr } = compileFieldRef(field);
|
|
431
|
+
const dir = direction === "desc" ? "DESC" : "ASC";
|
|
432
|
+
return ` ORDER BY ${expr} ${dir}`;
|
|
433
|
+
}
|
|
434
|
+
function compileLimit(options, params) {
|
|
435
|
+
if (options?.limit === void 0) return "";
|
|
436
|
+
params.push(options.limit);
|
|
437
|
+
return ` LIMIT ?`;
|
|
438
|
+
}
|
|
439
|
+
function compileSelect(table, filters, options) {
|
|
440
|
+
const params = [];
|
|
441
|
+
const conditions = [];
|
|
442
|
+
for (const f of filters) {
|
|
443
|
+
conditions.push(compileFilter(f, params));
|
|
444
|
+
}
|
|
445
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
446
|
+
let sql = `SELECT * FROM ${quoteIdent2(table)}${where}`;
|
|
447
|
+
sql += compileOrderBy(options, params);
|
|
448
|
+
sql += compileLimit(options, params);
|
|
449
|
+
return { sql, params };
|
|
450
|
+
}
|
|
451
|
+
function compileExpand(table, params) {
|
|
452
|
+
if (params.sources.length === 0) {
|
|
453
|
+
throw new FiregraphError(
|
|
454
|
+
"compileExpand requires a non-empty sources list \u2014 empty IN () is invalid SQL.",
|
|
455
|
+
"INVALID_QUERY"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
const direction = params.direction ?? "forward";
|
|
459
|
+
const aUidCol = compileFieldRef("aUid").expr;
|
|
460
|
+
const bUidCol = compileFieldRef("bUid").expr;
|
|
461
|
+
const aTypeCol = compileFieldRef("aType").expr;
|
|
462
|
+
const bTypeCol = compileFieldRef("bType").expr;
|
|
463
|
+
const axbTypeCol = compileFieldRef("axbType").expr;
|
|
464
|
+
const sourceColumn = direction === "forward" ? aUidCol : bUidCol;
|
|
465
|
+
const sqlParams = [params.axbType];
|
|
466
|
+
const conditions = [`${axbTypeCol} = ?`];
|
|
467
|
+
const placeholders = params.sources.map(() => "?").join(", ");
|
|
468
|
+
conditions.push(`${sourceColumn} IN (${placeholders})`);
|
|
469
|
+
for (const uid of params.sources) sqlParams.push(uid);
|
|
470
|
+
if (params.aType !== void 0) {
|
|
471
|
+
conditions.push(`${aTypeCol} = ?`);
|
|
472
|
+
sqlParams.push(params.aType);
|
|
473
|
+
}
|
|
474
|
+
if (params.bType !== void 0) {
|
|
475
|
+
conditions.push(`${bTypeCol} = ?`);
|
|
476
|
+
sqlParams.push(params.bType);
|
|
477
|
+
}
|
|
478
|
+
if (params.axbType === NODE_RELATION) {
|
|
479
|
+
conditions.push(`${aUidCol} != ${bUidCol}`);
|
|
480
|
+
}
|
|
481
|
+
let sql = `SELECT * FROM ${quoteIdent2(table)} WHERE ${conditions.join(" AND ")}`;
|
|
482
|
+
if (params.orderBy) {
|
|
483
|
+
sql += compileOrderBy({ orderBy: params.orderBy }, sqlParams);
|
|
484
|
+
}
|
|
485
|
+
if (params.limitPerSource !== void 0) {
|
|
486
|
+
const totalLimit = params.sources.length * params.limitPerSource;
|
|
487
|
+
sql += ` LIMIT ?`;
|
|
488
|
+
sqlParams.push(totalLimit);
|
|
489
|
+
}
|
|
490
|
+
return { sql, params: sqlParams };
|
|
491
|
+
}
|
|
492
|
+
function compileExpandHydrate(table, targetUids) {
|
|
493
|
+
if (targetUids.length === 0) {
|
|
494
|
+
throw new FiregraphError(
|
|
495
|
+
"compileExpandHydrate requires a non-empty target list \u2014 empty IN () is invalid SQL.",
|
|
496
|
+
"INVALID_QUERY"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const placeholders = targetUids.map(() => "?").join(", ");
|
|
500
|
+
const sqlParams = [NODE_RELATION];
|
|
501
|
+
for (const uid of targetUids) sqlParams.push(uid);
|
|
502
|
+
const aUidCol = compileFieldRef("aUid").expr;
|
|
503
|
+
const bUidCol = compileFieldRef("bUid").expr;
|
|
504
|
+
const axbTypeCol = compileFieldRef("axbType").expr;
|
|
505
|
+
return {
|
|
506
|
+
sql: `SELECT * FROM ${quoteIdent2(table)} WHERE ${axbTypeCol} = ? AND ${aUidCol} = ${bUidCol} AND ${bUidCol} IN (${placeholders})`,
|
|
507
|
+
params: sqlParams
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function compileSelectByDocId(table, docId) {
|
|
511
|
+
return {
|
|
512
|
+
sql: `SELECT * FROM ${quoteIdent2(table)} WHERE "doc_id" = ? LIMIT 1`,
|
|
513
|
+
params: [docId]
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function normalizeProjectionField(field) {
|
|
517
|
+
if (field in FIELD_TO_COLUMN) return field;
|
|
518
|
+
if (field === "data" || field.startsWith("data.")) return field;
|
|
519
|
+
return `data.${field}`;
|
|
520
|
+
}
|
|
521
|
+
function compileFindEdgesProjected(table, select, filters, options) {
|
|
522
|
+
if (select.length === 0) {
|
|
523
|
+
throw new FiregraphError(
|
|
524
|
+
"compileFindEdgesProjected requires a non-empty select list \u2014 an empty projection has no SQL representation distinct from `findEdges`.",
|
|
525
|
+
"INVALID_QUERY"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const seen = /* @__PURE__ */ new Set();
|
|
529
|
+
const uniqueFields = [];
|
|
530
|
+
for (const f of select) {
|
|
531
|
+
if (!seen.has(f)) {
|
|
532
|
+
seen.add(f);
|
|
533
|
+
uniqueFields.push(f);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const projections = [];
|
|
537
|
+
const columns = [];
|
|
538
|
+
for (let idx = 0; idx < uniqueFields.length; idx++) {
|
|
539
|
+
const field = uniqueFields[idx];
|
|
540
|
+
const canonical = normalizeProjectionField(field);
|
|
541
|
+
const { expr } = compileFieldRef(canonical);
|
|
542
|
+
const alias = quoteColumnAlias(field);
|
|
543
|
+
projections.push(`${expr} AS ${alias}`);
|
|
544
|
+
let kind;
|
|
545
|
+
let typeAliasName;
|
|
546
|
+
if (canonical === "data") {
|
|
547
|
+
kind = "data";
|
|
548
|
+
} else if (canonical.startsWith("data.")) {
|
|
549
|
+
kind = "json";
|
|
550
|
+
typeAliasName = `__fg_t_${idx}`;
|
|
551
|
+
const typeAlias = quoteColumnAlias(typeAliasName);
|
|
552
|
+
projections.push(`json_type("data", '$.${canonical.slice(5)}') AS ${typeAlias}`);
|
|
553
|
+
} else {
|
|
554
|
+
if (canonical === "v") kind = "builtin-int";
|
|
555
|
+
else if (canonical === "createdAt" || canonical === "updatedAt") kind = "builtin-timestamp";
|
|
556
|
+
else kind = "builtin-text";
|
|
557
|
+
}
|
|
558
|
+
columns.push({ field, kind, typeAlias: typeAliasName });
|
|
559
|
+
}
|
|
560
|
+
const params = [];
|
|
561
|
+
const conditions = [];
|
|
562
|
+
for (const f of filters) {
|
|
563
|
+
conditions.push(compileFilter(f, params));
|
|
564
|
+
}
|
|
565
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
566
|
+
let sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent2(table)}${where}`;
|
|
567
|
+
sql += compileOrderBy(options, params);
|
|
568
|
+
sql += compileLimit(options, params);
|
|
569
|
+
return { stmt: { sql, params }, columns };
|
|
570
|
+
}
|
|
571
|
+
function decodeProjectedRow(row, columns) {
|
|
572
|
+
const out = {};
|
|
573
|
+
for (const c of columns) {
|
|
574
|
+
const raw = row[c.field];
|
|
575
|
+
switch (c.kind) {
|
|
576
|
+
case "builtin-text":
|
|
577
|
+
out[c.field] = raw === null || raw === void 0 ? null : String(raw);
|
|
578
|
+
break;
|
|
579
|
+
case "builtin-int":
|
|
580
|
+
if (raw === null || raw === void 0) {
|
|
581
|
+
out[c.field] = null;
|
|
582
|
+
} else if (typeof raw === "bigint") {
|
|
583
|
+
out[c.field] = Number(raw);
|
|
584
|
+
} else if (typeof raw === "number") {
|
|
585
|
+
out[c.field] = raw;
|
|
586
|
+
} else {
|
|
587
|
+
out[c.field] = Number(raw);
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
case "builtin-timestamp": {
|
|
591
|
+
const ms = rowTimestampToMillis(raw);
|
|
592
|
+
out[c.field] = GraphTimestampImpl.fromMillis(ms);
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case "data":
|
|
596
|
+
if (raw === null || raw === void 0 || raw === "") {
|
|
597
|
+
out[c.field] = {};
|
|
598
|
+
} else {
|
|
599
|
+
out[c.field] = JSON.parse(raw);
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
case "json": {
|
|
603
|
+
const t = row[c.typeAlias];
|
|
604
|
+
if (raw === null || raw === void 0) {
|
|
605
|
+
out[c.field] = null;
|
|
606
|
+
} else if (t === "object" || t === "array") {
|
|
607
|
+
out[c.field] = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
608
|
+
} else if (t === "integer" && typeof raw === "bigint") {
|
|
609
|
+
out[c.field] = Number(raw);
|
|
610
|
+
} else {
|
|
611
|
+
out[c.field] = raw;
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return out;
|
|
618
|
+
}
|
|
619
|
+
function compileAggregate(table, spec, filters) {
|
|
620
|
+
const aliases = Object.keys(spec);
|
|
621
|
+
if (aliases.length === 0) {
|
|
622
|
+
throw new FiregraphError(
|
|
623
|
+
"aggregate() requires at least one aggregation in the `aggregates` map.",
|
|
624
|
+
"INVALID_QUERY"
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
const projections = [];
|
|
628
|
+
for (const alias of aliases) {
|
|
629
|
+
const { op, field } = spec[alias];
|
|
630
|
+
validateJsonPathKey(alias, BACKEND_ERR_LABEL);
|
|
631
|
+
if (op === "count") {
|
|
632
|
+
if (field !== void 0) {
|
|
633
|
+
throw new FiregraphError(
|
|
634
|
+
`Aggregate '${alias}' op 'count' must not specify a field \u2014 count operates on rows, not a column expression.`,
|
|
635
|
+
"INVALID_QUERY"
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
projections.push(`COUNT(*) AS ${quoteIdent2(alias)}`);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (!field) {
|
|
642
|
+
throw new FiregraphError(
|
|
643
|
+
`Aggregate '${alias}' op '${op}' requires a field.`,
|
|
644
|
+
"INVALID_QUERY"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const { expr } = compileFieldRef(field);
|
|
648
|
+
const numeric = `CAST(${expr} AS REAL)`;
|
|
649
|
+
if (op === "sum") projections.push(`SUM(${numeric}) AS ${quoteIdent2(alias)}`);
|
|
650
|
+
else if (op === "avg") projections.push(`AVG(${numeric}) AS ${quoteIdent2(alias)}`);
|
|
651
|
+
else if (op === "min") projections.push(`MIN(${numeric}) AS ${quoteIdent2(alias)}`);
|
|
652
|
+
else if (op === "max") projections.push(`MAX(${numeric}) AS ${quoteIdent2(alias)}`);
|
|
653
|
+
else
|
|
654
|
+
throw new FiregraphError(
|
|
655
|
+
`SQLite backend does not support aggregate op: ${String(op)}`,
|
|
656
|
+
"INVALID_QUERY"
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
const params = [];
|
|
660
|
+
const conditions = [];
|
|
661
|
+
for (const f of filters) {
|
|
662
|
+
conditions.push(compileFilter(f, params));
|
|
663
|
+
}
|
|
664
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
665
|
+
const sql = `SELECT ${projections.join(", ")} FROM ${quoteIdent2(table)}${where}`;
|
|
666
|
+
return { stmt: { sql, params }, aliases };
|
|
667
|
+
}
|
|
668
|
+
function compileSet(table, docId, record, nowMillis, mode) {
|
|
669
|
+
assertJsonSafePayload(record.data, BACKEND_LABEL);
|
|
670
|
+
if (mode === "replace") {
|
|
671
|
+
const sql2 = `INSERT OR REPLACE INTO ${quoteIdent2(table)} (
|
|
672
|
+
doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
|
|
673
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
674
|
+
const params = [
|
|
675
|
+
docId,
|
|
676
|
+
record.aType,
|
|
677
|
+
record.aUid,
|
|
678
|
+
record.axbType,
|
|
679
|
+
record.bType,
|
|
680
|
+
record.bUid,
|
|
681
|
+
JSON.stringify(record.data ?? {}),
|
|
682
|
+
record.v ?? null,
|
|
683
|
+
nowMillis,
|
|
684
|
+
nowMillis
|
|
685
|
+
];
|
|
686
|
+
return { sql: sql2, params };
|
|
687
|
+
}
|
|
688
|
+
const insertParams = [
|
|
689
|
+
docId,
|
|
690
|
+
record.aType,
|
|
691
|
+
record.aUid,
|
|
692
|
+
record.axbType,
|
|
693
|
+
record.bType,
|
|
694
|
+
record.bUid,
|
|
695
|
+
JSON.stringify(record.data ?? {}),
|
|
696
|
+
record.v ?? null,
|
|
697
|
+
nowMillis,
|
|
698
|
+
nowMillis
|
|
699
|
+
];
|
|
700
|
+
const ops = flattenPatch(record.data ?? {});
|
|
701
|
+
const updateParams = [];
|
|
702
|
+
const dataExpr = compileDataOpsExpr(ops, `COALESCE("data", '{}')`, updateParams, BACKEND_ERR_LABEL) ?? `COALESCE("data", '{}')`;
|
|
703
|
+
const sql = `INSERT INTO ${quoteIdent2(table)} (
|
|
704
|
+
doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
|
|
705
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
706
|
+
ON CONFLICT(doc_id) DO UPDATE SET
|
|
707
|
+
"a_type" = excluded."a_type",
|
|
708
|
+
"a_uid" = excluded."a_uid",
|
|
709
|
+
"axb_type" = excluded."axb_type",
|
|
710
|
+
"b_type" = excluded."b_type",
|
|
711
|
+
"b_uid" = excluded."b_uid",
|
|
712
|
+
"data" = ${dataExpr},
|
|
713
|
+
"v" = COALESCE(excluded."v", "v"),
|
|
714
|
+
"created_at" = excluded."created_at",
|
|
715
|
+
"updated_at" = excluded."updated_at"`;
|
|
716
|
+
return { sql, params: [...insertParams, ...updateParams] };
|
|
717
|
+
}
|
|
718
|
+
function compileUpdate(table, docId, update, nowMillis) {
|
|
719
|
+
assertUpdatePayloadExclusive(update);
|
|
720
|
+
const setClauses = [];
|
|
721
|
+
const params = [];
|
|
722
|
+
if (update.replaceData) {
|
|
723
|
+
assertJsonSafePayload(update.replaceData, BACKEND_LABEL);
|
|
724
|
+
setClauses.push(`"data" = ?`);
|
|
725
|
+
params.push(JSON.stringify(update.replaceData));
|
|
726
|
+
} else if (update.dataOps && update.dataOps.length > 0) {
|
|
727
|
+
for (const op of update.dataOps) {
|
|
728
|
+
if (!op.delete) assertJsonSafePayload(op.value, BACKEND_LABEL);
|
|
729
|
+
}
|
|
730
|
+
const expr = compileDataOpsExpr(
|
|
731
|
+
update.dataOps,
|
|
732
|
+
`COALESCE("data", '{}')`,
|
|
733
|
+
params,
|
|
734
|
+
BACKEND_ERR_LABEL
|
|
735
|
+
);
|
|
736
|
+
if (expr !== null) {
|
|
737
|
+
setClauses.push(`"data" = ${expr}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (update.v !== void 0) {
|
|
741
|
+
setClauses.push(`"v" = ?`);
|
|
742
|
+
params.push(update.v);
|
|
743
|
+
}
|
|
744
|
+
setClauses.push(`"updated_at" = ?`);
|
|
745
|
+
params.push(nowMillis);
|
|
746
|
+
params.push(docId);
|
|
747
|
+
return {
|
|
748
|
+
sql: `UPDATE ${quoteIdent2(table)} SET ${setClauses.join(", ")} WHERE "doc_id" = ?`,
|
|
749
|
+
params
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function compileDelete(table, docId) {
|
|
753
|
+
return {
|
|
754
|
+
sql: `DELETE FROM ${quoteIdent2(table)} WHERE "doc_id" = ?`,
|
|
755
|
+
params: [docId]
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function compileBulkDelete(table, filters) {
|
|
759
|
+
const params = [];
|
|
760
|
+
const conditions = [];
|
|
761
|
+
for (const f of filters) {
|
|
762
|
+
conditions.push(compileFilter(f, params));
|
|
763
|
+
}
|
|
764
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
765
|
+
return {
|
|
766
|
+
sql: `DELETE FROM ${quoteIdent2(table)}${where}`,
|
|
767
|
+
params
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function compileBulkUpdate(table, filters, patchData, nowMillis) {
|
|
771
|
+
const dataOps = flattenPatch(patchData);
|
|
772
|
+
if (dataOps.length === 0) {
|
|
773
|
+
throw new FiregraphError(
|
|
774
|
+
"bulkUpdate() patch.data must contain at least one leaf \u2014 an empty patch would only rewrite `updated_at`, which is almost certainly a bug. Use `setDoc` with merge mode if you want to stamp without editing data.",
|
|
775
|
+
"INVALID_QUERY"
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
for (const op of dataOps) {
|
|
779
|
+
if (!op.delete) assertJsonSafePayload(op.value, BACKEND_LABEL);
|
|
780
|
+
}
|
|
781
|
+
const setParams = [];
|
|
782
|
+
const expr = compileDataOpsExpr(dataOps, `COALESCE("data", '{}')`, setParams, BACKEND_ERR_LABEL);
|
|
783
|
+
if (expr === null) {
|
|
784
|
+
throw new FiregraphError(
|
|
785
|
+
"bulkUpdate() patch produced no SQL operations \u2014 internal invariant violated.",
|
|
786
|
+
"INVALID_ARGUMENT"
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const setClauses = [`"data" = ${expr}`, `"updated_at" = ?`];
|
|
790
|
+
setParams.push(nowMillis);
|
|
791
|
+
const whereParams = [];
|
|
792
|
+
const conditions = [];
|
|
793
|
+
for (const f of filters) {
|
|
794
|
+
conditions.push(compileFilter(f, whereParams));
|
|
795
|
+
}
|
|
796
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
797
|
+
return {
|
|
798
|
+
sql: `UPDATE ${quoteIdent2(table)} SET ${setClauses.join(", ")}${where}`,
|
|
799
|
+
params: [...setParams, ...whereParams]
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function compileDeleteAll(table) {
|
|
803
|
+
return {
|
|
804
|
+
sql: `DELETE FROM ${quoteIdent2(table)}`,
|
|
805
|
+
params: []
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function rowToRecord(row) {
|
|
809
|
+
const dataString = row.data;
|
|
810
|
+
const data = dataString ? JSON.parse(dataString) : {};
|
|
811
|
+
const createdMs = rowTimestampToMillis(row.created_at);
|
|
812
|
+
const updatedMs = rowTimestampToMillis(row.updated_at);
|
|
813
|
+
const record = {
|
|
814
|
+
aType: row.a_type,
|
|
815
|
+
aUid: row.a_uid,
|
|
816
|
+
axbType: row.axb_type,
|
|
817
|
+
bType: row.b_type,
|
|
818
|
+
bUid: row.b_uid,
|
|
819
|
+
data,
|
|
820
|
+
createdAt: GraphTimestampImpl.fromMillis(createdMs),
|
|
821
|
+
updatedAt: GraphTimestampImpl.fromMillis(updatedMs)
|
|
822
|
+
};
|
|
823
|
+
if (row.v !== null && row.v !== void 0) {
|
|
824
|
+
record.v = Number(row.v);
|
|
825
|
+
}
|
|
826
|
+
return record;
|
|
827
|
+
}
|
|
828
|
+
function rowTimestampToMillis(value) {
|
|
829
|
+
if (typeof value === "number") return value;
|
|
830
|
+
if (typeof value === "bigint") return Number(value);
|
|
831
|
+
if (typeof value === "string") {
|
|
832
|
+
const n = Number(value);
|
|
833
|
+
if (Number.isFinite(n)) return n;
|
|
834
|
+
}
|
|
835
|
+
throw new FiregraphError(
|
|
836
|
+
`SQLite row has non-numeric timestamp column: ${typeof value} (${String(value)})`,
|
|
837
|
+
"INVALID_QUERY"
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export {
|
|
842
|
+
GraphTimestampImpl,
|
|
843
|
+
buildSchemaStatements,
|
|
844
|
+
quoteIdent2 as quoteIdent,
|
|
845
|
+
validateTableName,
|
|
846
|
+
compileSelect,
|
|
847
|
+
compileExpand,
|
|
848
|
+
compileExpandHydrate,
|
|
849
|
+
compileSelectByDocId,
|
|
850
|
+
compileFindEdgesProjected,
|
|
851
|
+
decodeProjectedRow,
|
|
852
|
+
compileAggregate,
|
|
853
|
+
compileSet,
|
|
854
|
+
compileUpdate,
|
|
855
|
+
compileDelete,
|
|
856
|
+
compileBulkDelete,
|
|
857
|
+
compileBulkUpdate,
|
|
858
|
+
compileDeleteAll,
|
|
859
|
+
rowToRecord,
|
|
860
|
+
rowTimestampToMillis
|
|
861
|
+
};
|
|
862
|
+
//# sourceMappingURL=chunk-ULRDQ6HZ.js.map
|