@typicalday/firegraph 0.15.0 → 0.16.1
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 +39 -17
- package/dist/{backend-CvImIwTY.d.cts → backend-CE3pM9-T.d.ts} +32 -2
- package/dist/{backend-BpYLdwCW.d.cts → backend-DNzv8KSR.d.cts} +33 -19
- package/dist/{backend-BpYLdwCW.d.ts → backend-DNzv8KSR.d.ts} +33 -19
- package/dist/{backend-YH5HtawN.d.ts → backend-EjFfw9yO.d.cts} +32 -2
- 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-FODIMIWY.js → chunk-5JBNLH5W.js} +17 -6
- package/dist/chunk-5JBNLH5W.js.map +1 -0
- package/dist/{chunk-5HIRYV2S.js → chunk-6IO74NKD.js} +12 -10
- package/dist/{chunk-5HIRYV2S.js.map → chunk-6IO74NKD.js.map} +1 -1
- package/dist/{chunk-ULRDQ6HZ.js → chunk-NZVSLWNY.js} +6 -1
- 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-B5o39X79.d.ts → client-CNAwJayO.d.ts} +1 -1
- package/dist/{client-BGHwxwPg.d.cts → client-CaXH5D5C.d.cts} +1 -1
- package/dist/cloudflare/index.cjs +11 -9
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +3 -3
- package/dist/cloudflare/index.d.ts +3 -3
- package/dist/cloudflare/index.js +3 -3
- 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 +2 -2
- 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 +2 -2
- package/dist/index.cjs +11 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{registry-tKTb5Kx1.d.ts → registry-By1i-zge.d.ts} +1 -1
- package/dist/{registry-BGh7Jqpb.d.cts → registry-CNToyEra.d.cts} +1 -1
- package/dist/sqlite/index.cjs +24 -12
- package/dist/sqlite/index.cjs.map +1 -1
- package/dist/sqlite/index.d.cts +4 -4
- package/dist/sqlite/index.d.ts +4 -4
- package/dist/sqlite/index.js +4 -4
- package/dist/sqlite/local.cjs +484 -47
- package/dist/sqlite/local.cjs.map +1 -1
- package/dist/sqlite/local.d.cts +31 -5
- package/dist/sqlite/local.d.ts +31 -5
- package/dist/sqlite/local.js +439 -4
- package/dist/sqlite/local.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-FODIMIWY.js.map +0 -1
- package/dist/chunk-ULRDQ6HZ.js.map +0 -1
package/dist/sqlite/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { c as createGraphClient } from '../client-
|
|
2
|
-
export { M as META_EDGE_TYPE, a as META_NODE_TYPE, c as createMergedRegistry, b as createRegistry, g as generateId } from '../registry-
|
|
3
|
-
export { S as SqliteBackendOptions, a as SqliteCapability, c as createSqliteBackend } from '../backend-
|
|
4
|
-
import '../backend-
|
|
1
|
+
export { c as createGraphClient } from '../client-CaXH5D5C.cjs';
|
|
2
|
+
export { M as META_EDGE_TYPE, a as META_NODE_TYPE, c as createMergedRegistry, b as createRegistry, g as generateId } from '../registry-CNToyEra.cjs';
|
|
3
|
+
export { S as SqliteBackendOptions, a as SqliteCapability, c as createSqliteBackend } from '../backend-EjFfw9yO.cjs';
|
|
4
|
+
import '../backend-DNzv8KSR.cjs';
|
|
5
5
|
import '@google-cloud/firestore';
|
package/dist/sqlite/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { c as createGraphClient } from '../client-
|
|
2
|
-
export { M as META_EDGE_TYPE, a as META_NODE_TYPE, c as createMergedRegistry, b as createRegistry, g as generateId } from '../registry-
|
|
3
|
-
export { S as SqliteBackendOptions, a as SqliteCapability, c as createSqliteBackend } from '../backend-
|
|
4
|
-
import '../backend-
|
|
1
|
+
export { c as createGraphClient } from '../client-CNAwJayO.js';
|
|
2
|
+
export { M as META_EDGE_TYPE, a as META_NODE_TYPE, c as createMergedRegistry, b as createRegistry, g as generateId } from '../registry-By1i-zge.js';
|
|
3
|
+
export { S as SqliteBackendOptions, a as SqliteCapability, c as createSqliteBackend } from '../backend-CE3pM9-T.js';
|
|
4
|
+
import '../backend-DNzv8KSR.js';
|
|
5
5
|
import '@google-cloud/firestore';
|
package/dist/sqlite/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSqliteBackend
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-5JBNLH5W.js";
|
|
4
|
+
import "../chunk-NZVSLWNY.js";
|
|
5
5
|
import "../chunk-2DHMNTV6.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-PWIO46RT.js";
|
|
7
7
|
import {
|
|
8
8
|
META_EDGE_TYPE,
|
|
9
9
|
META_NODE_TYPE,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
createMergedRegistry,
|
|
12
12
|
createRegistry,
|
|
13
13
|
generateId
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-6IO74NKD.js";
|
|
15
15
|
import "../chunk-NGAJCALM.js";
|
|
16
16
|
import "../chunk-SIHE4UY4.js";
|
|
17
17
|
import "../chunk-EQJUUVFG.js";
|
package/dist/sqlite/local.cjs
CHANGED
|
@@ -44,24 +44,6 @@ var FiregraphError = class extends Error {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
// src/docid.ts
|
|
48
|
-
var import_node_crypto = require("crypto");
|
|
49
|
-
|
|
50
|
-
// src/internal/constants.ts
|
|
51
|
-
var NODE_RELATION = "is";
|
|
52
|
-
var SHARD_SEPARATOR = ":";
|
|
53
|
-
|
|
54
|
-
// src/docid.ts
|
|
55
|
-
function computeNodeDocId(uid) {
|
|
56
|
-
return uid;
|
|
57
|
-
}
|
|
58
|
-
function computeEdgeDocId(aUid, axbType, bUid) {
|
|
59
|
-
const composite = `${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
60
|
-
const hash = (0, import_node_crypto.createHash)("sha256").update(composite).digest("hex");
|
|
61
|
-
const shard = hash[0];
|
|
62
|
-
return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
47
|
// src/internal/backend.ts
|
|
66
48
|
function createCapabilities(caps) {
|
|
67
49
|
return {
|
|
@@ -225,31 +207,6 @@ function quoteColumnAlias(label) {
|
|
|
225
207
|
return `"${label.replace(/"/g, '""')}"`;
|
|
226
208
|
}
|
|
227
209
|
|
|
228
|
-
// src/timestamp.ts
|
|
229
|
-
var GraphTimestampImpl = class _GraphTimestampImpl {
|
|
230
|
-
constructor(seconds, nanoseconds) {
|
|
231
|
-
this.seconds = seconds;
|
|
232
|
-
this.nanoseconds = nanoseconds;
|
|
233
|
-
}
|
|
234
|
-
toDate() {
|
|
235
|
-
return new Date(this.toMillis());
|
|
236
|
-
}
|
|
237
|
-
toMillis() {
|
|
238
|
-
return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
|
|
239
|
-
}
|
|
240
|
-
toJSON() {
|
|
241
|
-
return { seconds: this.seconds, nanoseconds: this.nanoseconds };
|
|
242
|
-
}
|
|
243
|
-
static fromMillis(ms) {
|
|
244
|
-
const seconds = Math.floor(ms / 1e3);
|
|
245
|
-
const nanoseconds = (ms - seconds * 1e3) * 1e6;
|
|
246
|
-
return new _GraphTimestampImpl(seconds, nanoseconds);
|
|
247
|
-
}
|
|
248
|
-
static now() {
|
|
249
|
-
return _GraphTimestampImpl.fromMillis(Date.now());
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
210
|
// src/internal/sqlite-data-ops.ts
|
|
254
211
|
var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
255
212
|
"Timestamp",
|
|
@@ -318,6 +275,35 @@ function compileDataOpsExpr(ops, base, params, backendLabel) {
|
|
|
318
275
|
return expr;
|
|
319
276
|
}
|
|
320
277
|
|
|
278
|
+
// src/timestamp.ts
|
|
279
|
+
var GraphTimestampImpl = class _GraphTimestampImpl {
|
|
280
|
+
constructor(seconds, nanoseconds) {
|
|
281
|
+
this.seconds = seconds;
|
|
282
|
+
this.nanoseconds = nanoseconds;
|
|
283
|
+
}
|
|
284
|
+
toDate() {
|
|
285
|
+
return new Date(this.toMillis());
|
|
286
|
+
}
|
|
287
|
+
toMillis() {
|
|
288
|
+
return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
|
|
289
|
+
}
|
|
290
|
+
toJSON() {
|
|
291
|
+
return { seconds: this.seconds, nanoseconds: this.nanoseconds };
|
|
292
|
+
}
|
|
293
|
+
static fromMillis(ms) {
|
|
294
|
+
const seconds = Math.floor(ms / 1e3);
|
|
295
|
+
const nanoseconds = (ms - seconds * 1e3) * 1e6;
|
|
296
|
+
return new _GraphTimestampImpl(seconds, nanoseconds);
|
|
297
|
+
}
|
|
298
|
+
static now() {
|
|
299
|
+
return _GraphTimestampImpl.fromMillis(Date.now());
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// src/internal/constants.ts
|
|
304
|
+
var NODE_RELATION = "is";
|
|
305
|
+
var SHARD_SEPARATOR = ":";
|
|
306
|
+
|
|
321
307
|
// src/internal/serialization-tag.ts
|
|
322
308
|
var SERIALIZATION_TAG = "__firegraph_ser__";
|
|
323
309
|
var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
|
|
@@ -622,6 +608,9 @@ function compileFilter(filter, params) {
|
|
|
622
608
|
);
|
|
623
609
|
}
|
|
624
610
|
}
|
|
611
|
+
function compileFilterConditions(filters, params) {
|
|
612
|
+
return filters.map((f) => compileFilter(f, params));
|
|
613
|
+
}
|
|
625
614
|
function asArray(value, op) {
|
|
626
615
|
if (!Array.isArray(value) || value.length === 0) {
|
|
627
616
|
throw new FiregraphError(`Operator "${op}" requires a non-empty array value`, "INVALID_QUERY");
|
|
@@ -1036,6 +1025,321 @@ function rowTimestampToMillis(value) {
|
|
|
1036
1025
|
);
|
|
1037
1026
|
}
|
|
1038
1027
|
|
|
1028
|
+
// src/internal/sqlite-search.ts
|
|
1029
|
+
var VECTOR_DISTANCE_UDF = "firegraph_vector_distance";
|
|
1030
|
+
var DISTANCE_ALIAS = "__fg_distance";
|
|
1031
|
+
var BACKEND_ERR_LABEL2 = "SQLite backend";
|
|
1032
|
+
var ENVELOPE_FIELDS = /* @__PURE__ */ new Set([
|
|
1033
|
+
"aType",
|
|
1034
|
+
"aUid",
|
|
1035
|
+
"axbType",
|
|
1036
|
+
"bType",
|
|
1037
|
+
"bUid",
|
|
1038
|
+
"createdAt",
|
|
1039
|
+
"updatedAt",
|
|
1040
|
+
"v"
|
|
1041
|
+
]);
|
|
1042
|
+
function ftsTableName(table) {
|
|
1043
|
+
return `${table}_fts`;
|
|
1044
|
+
}
|
|
1045
|
+
function ftsMapTableName(table) {
|
|
1046
|
+
return `${table}_fts_map`;
|
|
1047
|
+
}
|
|
1048
|
+
function textExtractionExpr(dataRef) {
|
|
1049
|
+
return `(SELECT coalesce(group_concat("value", ' '), '') FROM json_tree(coalesce(${dataRef}, '{}')) WHERE "type" = 'text')`;
|
|
1050
|
+
}
|
|
1051
|
+
function buildFtsDDL(table) {
|
|
1052
|
+
const t = quoteIdent2(table);
|
|
1053
|
+
const fts = quoteIdent2(ftsTableName(table));
|
|
1054
|
+
const map = quoteIdent2(ftsMapTableName(table));
|
|
1055
|
+
const mappedId = `(SELECT "id" FROM ${map} WHERE "doc_id" = new."doc_id")`;
|
|
1056
|
+
const reindexBody = ` INSERT INTO ${map} ("doc_id") SELECT new."doc_id" WHERE NOT EXISTS (SELECT 1 FROM ${map} WHERE "doc_id" = new."doc_id");
|
|
1057
|
+
DELETE FROM ${fts} WHERE rowid = ${mappedId};
|
|
1058
|
+
INSERT INTO ${fts} (rowid, "text") VALUES (${mappedId}, ${textExtractionExpr('new."data"')});
|
|
1059
|
+
`;
|
|
1060
|
+
return [
|
|
1061
|
+
`CREATE TABLE IF NOT EXISTS ${map} (
|
|
1062
|
+
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1063
|
+
"doc_id" TEXT NOT NULL UNIQUE
|
|
1064
|
+
)`,
|
|
1065
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS ${fts} USING fts5("text")`,
|
|
1066
|
+
`CREATE TRIGGER IF NOT EXISTS ${quoteIdent2(`${table}_fts_ai`)} AFTER INSERT ON ${t} BEGIN
|
|
1067
|
+
${reindexBody}END`,
|
|
1068
|
+
`CREATE TRIGGER IF NOT EXISTS ${quoteIdent2(`${table}_fts_au`)} AFTER UPDATE ON ${t} BEGIN
|
|
1069
|
+
${reindexBody}END`,
|
|
1070
|
+
`CREATE TRIGGER IF NOT EXISTS ${quoteIdent2(`${table}_fts_ad`)} AFTER DELETE ON ${t} BEGIN
|
|
1071
|
+
DELETE FROM ${fts} WHERE rowid = (SELECT "id" FROM ${map} WHERE "doc_id" = old."doc_id");
|
|
1072
|
+
DELETE FROM ${map} WHERE "doc_id" = old."doc_id";
|
|
1073
|
+
END`
|
|
1074
|
+
];
|
|
1075
|
+
}
|
|
1076
|
+
function buildFtsSyncStatements(table) {
|
|
1077
|
+
const t = quoteIdent2(table);
|
|
1078
|
+
const fts = quoteIdent2(ftsTableName(table));
|
|
1079
|
+
const map = quoteIdent2(ftsMapTableName(table));
|
|
1080
|
+
return [
|
|
1081
|
+
`DELETE FROM ${fts} WHERE rowid IN (
|
|
1082
|
+
SELECT m."id" FROM ${map} m LEFT JOIN ${t} t ON t."doc_id" = m."doc_id"
|
|
1083
|
+
WHERE t."doc_id" IS NULL
|
|
1084
|
+
)`,
|
|
1085
|
+
`DELETE FROM ${map} WHERE "doc_id" NOT IN (SELECT "doc_id" FROM ${t})`,
|
|
1086
|
+
`INSERT OR IGNORE INTO ${map} ("doc_id") SELECT "doc_id" FROM ${t}`,
|
|
1087
|
+
`INSERT INTO ${fts} (rowid, "text")
|
|
1088
|
+
SELECT m."id", ${textExtractionExpr('t."data"')}
|
|
1089
|
+
FROM ${t} t JOIN ${map} m ON m."doc_id" = t."doc_id"
|
|
1090
|
+
WHERE m."id" NOT IN (SELECT rowid FROM ${fts})`
|
|
1091
|
+
];
|
|
1092
|
+
}
|
|
1093
|
+
function buildLocalSearchDDL(table) {
|
|
1094
|
+
return [...buildFtsDDL(table), ...buildFtsSyncStatements(table)];
|
|
1095
|
+
}
|
|
1096
|
+
function normalizeVectorFieldPath(label, field) {
|
|
1097
|
+
if (ENVELOPE_FIELDS.has(field)) {
|
|
1098
|
+
throw new FiregraphError(
|
|
1099
|
+
`findNearest(): ${label} '${field}' is a built-in envelope field \u2014 vectors must live under \`data.*\`. Use a path like 'data.${field}' if you really meant a nested data field.`,
|
|
1100
|
+
"INVALID_QUERY"
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
if (field === "data" || field.startsWith("data.")) return field;
|
|
1104
|
+
return `data.${field}`;
|
|
1105
|
+
}
|
|
1106
|
+
function normalizeFullTextFieldPath(field) {
|
|
1107
|
+
if (ENVELOPE_FIELDS.has(field)) {
|
|
1108
|
+
throw new FiregraphError(
|
|
1109
|
+
`fullTextSearch(): field '${field}' is a built-in envelope field \u2014 text-indexed fields must live under \`data.*\`. Use a path like 'data.${field}' if you really meant a nested data field.`,
|
|
1110
|
+
"INVALID_QUERY"
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
if (field === "data" || field.startsWith("data.")) return field;
|
|
1114
|
+
return `data.${field}`;
|
|
1115
|
+
}
|
|
1116
|
+
function buildSearchFilters(params) {
|
|
1117
|
+
const filters = [];
|
|
1118
|
+
if (params.aType) filters.push({ field: "aType", op: "==", value: params.aType });
|
|
1119
|
+
if (params.axbType) filters.push({ field: "axbType", op: "==", value: params.axbType });
|
|
1120
|
+
if (params.bType) filters.push({ field: "bType", op: "==", value: params.bType });
|
|
1121
|
+
for (const clause of params.where ?? []) {
|
|
1122
|
+
const field = ENVELOPE_FIELDS.has(clause.field) || clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
1123
|
+
filters.push({ field, op: clause.op, value: clause.value });
|
|
1124
|
+
}
|
|
1125
|
+
return filters;
|
|
1126
|
+
}
|
|
1127
|
+
function compileFullTextSearch(table, params) {
|
|
1128
|
+
if (typeof params.query !== "string" || params.query.length === 0) {
|
|
1129
|
+
throw new FiregraphError(
|
|
1130
|
+
"fullTextSearch(): query must be a non-empty string.",
|
|
1131
|
+
"INVALID_QUERY"
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
if (!Number.isInteger(params.limit) || params.limit <= 0) {
|
|
1135
|
+
throw new FiregraphError(
|
|
1136
|
+
`fullTextSearch(): limit must be a positive integer (got ${params.limit}).`,
|
|
1137
|
+
"INVALID_QUERY"
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
const normalizedFields = params.fields?.map((f) => normalizeFullTextFieldPath(f));
|
|
1141
|
+
if (normalizedFields !== void 0 && normalizedFields.length > 0) {
|
|
1142
|
+
throw new FiregraphError(
|
|
1143
|
+
"fullTextSearch(): the `fields` option is not yet supported \u2014 the local SQLite FTS index stores one combined text column per record. Omit `fields` to search all string values.",
|
|
1144
|
+
"INVALID_QUERY"
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
const t = quoteIdent2(table);
|
|
1148
|
+
const fts = quoteIdent2(ftsTableName(table));
|
|
1149
|
+
const map = quoteIdent2(ftsMapTableName(table));
|
|
1150
|
+
const sqlParams = [params.query];
|
|
1151
|
+
const conditions = [`${fts} MATCH ?`];
|
|
1152
|
+
conditions.push(...compileFilterConditions(buildSearchFilters(params), sqlParams));
|
|
1153
|
+
sqlParams.push(params.limit);
|
|
1154
|
+
const sql = `SELECT ${t}.* FROM ${fts} JOIN ${map} ON ${map}."id" = ${fts}.rowid JOIN ${t} ON ${t}."doc_id" = ${map}."doc_id" WHERE ${conditions.join(" AND ")} ORDER BY bm25(${fts}) ASC, ${t}."doc_id" ASC LIMIT ?`;
|
|
1155
|
+
return { sql, params: sqlParams };
|
|
1156
|
+
}
|
|
1157
|
+
var FTS5_QUERY_ERROR_SIGNATURES = [
|
|
1158
|
+
"fts5: syntax error",
|
|
1159
|
+
"unterminated string",
|
|
1160
|
+
"unknown special query",
|
|
1161
|
+
"no such column"
|
|
1162
|
+
];
|
|
1163
|
+
function isFts5QueryError(message) {
|
|
1164
|
+
const lower = message.toLowerCase();
|
|
1165
|
+
return FTS5_QUERY_ERROR_SIGNATURES.some((sig) => lower.includes(sig));
|
|
1166
|
+
}
|
|
1167
|
+
var DISTANCE_MEASURES = /* @__PURE__ */ new Set(["EUCLIDEAN", "COSINE", "DOT_PRODUCT"]);
|
|
1168
|
+
function toNumberArray(qv) {
|
|
1169
|
+
if (Array.isArray(qv)) return qv;
|
|
1170
|
+
if (typeof qv.toArray === "function") {
|
|
1171
|
+
return qv.toArray();
|
|
1172
|
+
}
|
|
1173
|
+
throw new FiregraphError(
|
|
1174
|
+
"findNearest(): queryVector must be a number[] or a Firestore VectorValue.",
|
|
1175
|
+
"INVALID_QUERY"
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
function compileFindNearest(table, params) {
|
|
1179
|
+
const vec = toNumberArray(params.queryVector);
|
|
1180
|
+
if (vec.length === 0) {
|
|
1181
|
+
throw new FiregraphError(
|
|
1182
|
+
"findNearest(): queryVector is empty \u2014 at least one dimension is required.",
|
|
1183
|
+
"INVALID_QUERY"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
if (!Number.isInteger(params.limit) || params.limit <= 0 || params.limit > 1e3) {
|
|
1187
|
+
throw new FiregraphError(
|
|
1188
|
+
`findNearest(): limit must be a positive integer \u2264 1000 (got ${params.limit}).`,
|
|
1189
|
+
"INVALID_QUERY"
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
if (!DISTANCE_MEASURES.has(params.distanceMeasure)) {
|
|
1193
|
+
throw new FiregraphError(
|
|
1194
|
+
`findNearest(): unknown distanceMeasure '${String(params.distanceMeasure)}' \u2014 expected EUCLIDEAN, COSINE, or DOT_PRODUCT.`,
|
|
1195
|
+
"INVALID_QUERY"
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
const vectorField = normalizeVectorFieldPath("vectorField", params.vectorField);
|
|
1199
|
+
let vectorExpr;
|
|
1200
|
+
if (vectorField === "data") {
|
|
1201
|
+
vectorExpr = '"data"';
|
|
1202
|
+
} else {
|
|
1203
|
+
const suffix = vectorField.slice("data.".length);
|
|
1204
|
+
for (const part of suffix.split(".")) {
|
|
1205
|
+
validateJsonPathKey(part, BACKEND_ERR_LABEL2);
|
|
1206
|
+
}
|
|
1207
|
+
vectorExpr = `json_extract("data", '$.${suffix}')`;
|
|
1208
|
+
}
|
|
1209
|
+
let distancePath = null;
|
|
1210
|
+
if (params.distanceResultField !== void 0) {
|
|
1211
|
+
const normalized = normalizeVectorFieldPath("distanceResultField", params.distanceResultField);
|
|
1212
|
+
if (normalized === "data") {
|
|
1213
|
+
throw new FiregraphError(
|
|
1214
|
+
`findNearest(): distanceResultField 'data' would replace the entire data payload \u2014 use a nested path like 'data.distance'.`,
|
|
1215
|
+
"INVALID_QUERY"
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
distancePath = normalized.slice("data.".length).split(".");
|
|
1219
|
+
for (const part of distancePath) {
|
|
1220
|
+
validateJsonPathKey(part, BACKEND_ERR_LABEL2);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const sqlParams = [JSON.stringify(vec), params.distanceMeasure];
|
|
1224
|
+
const conditions = compileFilterConditions(buildSearchFilters(params), sqlParams);
|
|
1225
|
+
const innerWhere = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
1226
|
+
const dist = quoteIdent2(DISTANCE_ALIAS);
|
|
1227
|
+
const descending = params.distanceMeasure === "DOT_PRODUCT";
|
|
1228
|
+
let sql = `SELECT * FROM (SELECT *, ${VECTOR_DISTANCE_UDF}(${vectorExpr}, ?, ?) AS ${dist} FROM ${quoteIdent2(table)}${innerWhere}) WHERE ${dist} IS NOT NULL`;
|
|
1229
|
+
if (params.distanceThreshold !== void 0) {
|
|
1230
|
+
sql += ` AND ${dist} ${descending ? ">=" : "<="} ?`;
|
|
1231
|
+
sqlParams.push(params.distanceThreshold);
|
|
1232
|
+
}
|
|
1233
|
+
sql += ` ORDER BY ${dist} ${descending ? "DESC" : "ASC"}, "doc_id" ASC LIMIT ?`;
|
|
1234
|
+
sqlParams.push(params.limit);
|
|
1235
|
+
return { stmt: { sql, params: sqlParams }, distancePath };
|
|
1236
|
+
}
|
|
1237
|
+
var memoQueryJson = null;
|
|
1238
|
+
var memoQueryVec = null;
|
|
1239
|
+
function computeVectorDistance(storedJson, queryJson, measure) {
|
|
1240
|
+
if (typeof storedJson !== "string" || typeof queryJson !== "string" || typeof measure !== "string") {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
let query;
|
|
1244
|
+
if (memoQueryJson === queryJson && memoQueryVec !== null) {
|
|
1245
|
+
query = memoQueryVec;
|
|
1246
|
+
} else {
|
|
1247
|
+
let parsed;
|
|
1248
|
+
try {
|
|
1249
|
+
parsed = JSON.parse(queryJson);
|
|
1250
|
+
} catch {
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
if (!Array.isArray(parsed)) return null;
|
|
1254
|
+
query = parsed;
|
|
1255
|
+
memoQueryJson = queryJson;
|
|
1256
|
+
memoQueryVec = query;
|
|
1257
|
+
}
|
|
1258
|
+
let stored;
|
|
1259
|
+
try {
|
|
1260
|
+
stored = JSON.parse(storedJson);
|
|
1261
|
+
} catch {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
if (!Array.isArray(stored) || stored.length !== query.length) return null;
|
|
1265
|
+
let dot = 0;
|
|
1266
|
+
let sumSq = 0;
|
|
1267
|
+
let normStored = 0;
|
|
1268
|
+
let normQuery = 0;
|
|
1269
|
+
for (let i = 0; i < query.length; i++) {
|
|
1270
|
+
const a = stored[i];
|
|
1271
|
+
const b = query[i];
|
|
1272
|
+
if (typeof a !== "number" || !Number.isFinite(a)) return null;
|
|
1273
|
+
if (typeof b !== "number" || !Number.isFinite(b)) return null;
|
|
1274
|
+
dot += a * b;
|
|
1275
|
+
const diff = a - b;
|
|
1276
|
+
sumSq += diff * diff;
|
|
1277
|
+
normStored += a * a;
|
|
1278
|
+
normQuery += b * b;
|
|
1279
|
+
}
|
|
1280
|
+
let result;
|
|
1281
|
+
switch (measure) {
|
|
1282
|
+
case "EUCLIDEAN":
|
|
1283
|
+
result = Math.sqrt(sumSq);
|
|
1284
|
+
break;
|
|
1285
|
+
case "COSINE": {
|
|
1286
|
+
const denom = Math.sqrt(normStored) * Math.sqrt(normQuery);
|
|
1287
|
+
if (denom === 0) return null;
|
|
1288
|
+
result = 1 - dot / denom;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
case "DOT_PRODUCT":
|
|
1292
|
+
result = dot;
|
|
1293
|
+
break;
|
|
1294
|
+
default:
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
return Number.isFinite(result) ? result : null;
|
|
1298
|
+
}
|
|
1299
|
+
function setDataPath(data, path, value) {
|
|
1300
|
+
let cursor = data;
|
|
1301
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1302
|
+
const key = path[i];
|
|
1303
|
+
const next = cursor[key];
|
|
1304
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) {
|
|
1305
|
+
const created = {};
|
|
1306
|
+
cursor[key] = created;
|
|
1307
|
+
cursor = created;
|
|
1308
|
+
} else {
|
|
1309
|
+
cursor = next;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
cursor[path[path.length - 1]] = value;
|
|
1313
|
+
}
|
|
1314
|
+
function findOrphanedFtsTables(allTables, catalogTables, rootTable) {
|
|
1315
|
+
const names = new Set(allTables);
|
|
1316
|
+
const liveGraphTables = new Set(catalogTables);
|
|
1317
|
+
const subgraphPrefix = `${rootTable}_g_`;
|
|
1318
|
+
const orphans = [];
|
|
1319
|
+
for (const name of names) {
|
|
1320
|
+
let base = null;
|
|
1321
|
+
if (name.endsWith("_fts_map")) base = name.slice(0, -"_fts_map".length);
|
|
1322
|
+
else if (name.endsWith("_fts")) base = name.slice(0, -"_fts".length);
|
|
1323
|
+
if (base === null || !base.startsWith(subgraphPrefix)) continue;
|
|
1324
|
+
if (liveGraphTables.has(name)) continue;
|
|
1325
|
+
if (names.has(base)) continue;
|
|
1326
|
+
orphans.push(name);
|
|
1327
|
+
}
|
|
1328
|
+
return orphans.sort();
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/docid.ts
|
|
1332
|
+
var import_node_crypto = require("crypto");
|
|
1333
|
+
function computeNodeDocId(uid) {
|
|
1334
|
+
return uid;
|
|
1335
|
+
}
|
|
1336
|
+
function computeEdgeDocId(aUid, axbType, bUid) {
|
|
1337
|
+
const composite = `${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
1338
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(composite).digest("hex");
|
|
1339
|
+
const shard = hash[0];
|
|
1340
|
+
return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1039
1343
|
// src/sqlite/catalog.ts
|
|
1040
1344
|
function catalogTableName(rootTable) {
|
|
1041
1345
|
validateTableName(rootTable);
|
|
@@ -1196,7 +1500,7 @@ var SQLITE_CORE_CAPS = [
|
|
|
1196
1500
|
"raw.sql"
|
|
1197
1501
|
];
|
|
1198
1502
|
var SqliteBackendImpl = class _SqliteBackendImpl {
|
|
1199
|
-
constructor(executor, rootTable, storageScope, scopePath, registry, coreIndexes) {
|
|
1503
|
+
constructor(executor, rootTable, storageScope, scopePath, registry, coreIndexes, extraTableDDL) {
|
|
1200
1504
|
this.executor = executor;
|
|
1201
1505
|
validateTableName(rootTable);
|
|
1202
1506
|
this.rootTable = rootTable;
|
|
@@ -1205,6 +1509,7 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
|
|
|
1205
1509
|
this.scopePath = scopePath;
|
|
1206
1510
|
this.registry = registry;
|
|
1207
1511
|
this.coreIndexes = coreIndexes;
|
|
1512
|
+
this.extraTableDDL = extraTableDDL;
|
|
1208
1513
|
const caps = new Set(SQLITE_CORE_CAPS);
|
|
1209
1514
|
if (typeof executor.transaction === "function") {
|
|
1210
1515
|
caps.add("core.transactions");
|
|
@@ -1221,6 +1526,7 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
|
|
|
1221
1526
|
rootTable;
|
|
1222
1527
|
registry;
|
|
1223
1528
|
coreIndexes;
|
|
1529
|
+
extraTableDDL;
|
|
1224
1530
|
ensured = null;
|
|
1225
1531
|
/**
|
|
1226
1532
|
* Lazily create this graph's table + indexes + the catalog, and register
|
|
@@ -1237,12 +1543,18 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
|
|
|
1237
1543
|
}
|
|
1238
1544
|
return this.ensured;
|
|
1239
1545
|
}
|
|
1546
|
+
/** @internal See `SqliteStorageBackend.ensureReady`. */
|
|
1547
|
+
async ensureReady(force = false) {
|
|
1548
|
+
if (force) this.ensured = null;
|
|
1549
|
+
await this.ensureSchema();
|
|
1550
|
+
}
|
|
1240
1551
|
async doEnsureSchema() {
|
|
1241
1552
|
const ddl = [
|
|
1242
1553
|
...buildSchemaStatements(this.collectionPath, {
|
|
1243
1554
|
coreIndexes: this.coreIndexes,
|
|
1244
1555
|
registry: this.registry
|
|
1245
1556
|
}),
|
|
1557
|
+
...this.extraTableDDL ? this.extraTableDDL(this.collectionPath) : [],
|
|
1246
1558
|
buildCatalogDDL(this.rootTable)
|
|
1247
1559
|
];
|
|
1248
1560
|
const statements = ddl.map((sql) => ({ sql, params: [] }));
|
|
@@ -1373,7 +1685,8 @@ var SqliteBackendImpl = class _SqliteBackendImpl {
|
|
|
1373
1685
|
newStorageScope,
|
|
1374
1686
|
newScope,
|
|
1375
1687
|
this.registry,
|
|
1376
|
-
this.coreIndexes
|
|
1688
|
+
this.coreIndexes,
|
|
1689
|
+
this.extraTableDDL
|
|
1377
1690
|
);
|
|
1378
1691
|
}
|
|
1379
1692
|
// --- Cascade & bulk ---
|
|
@@ -1719,7 +2032,8 @@ function createSqliteBackend(executor, tableName, options = {}) {
|
|
|
1719
2032
|
storageScope,
|
|
1720
2033
|
scopePath,
|
|
1721
2034
|
options.registry,
|
|
1722
|
-
options.coreIndexes
|
|
2035
|
+
options.coreIndexes,
|
|
2036
|
+
options.extraTableDDL
|
|
1723
2037
|
);
|
|
1724
2038
|
}
|
|
1725
2039
|
|
|
@@ -1779,6 +2093,118 @@ function applyPragmas(db, pragmas) {
|
|
|
1779
2093
|
db.pragma(`${key} = ${value}`);
|
|
1780
2094
|
}
|
|
1781
2095
|
}
|
|
2096
|
+
function registerVectorUdf(db) {
|
|
2097
|
+
try {
|
|
2098
|
+
db.function(
|
|
2099
|
+
VECTOR_DISTANCE_UDF,
|
|
2100
|
+
{ deterministic: true },
|
|
2101
|
+
(stored, query, measure) => computeVectorDistance(stored, query, measure)
|
|
2102
|
+
);
|
|
2103
|
+
} catch {
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
async function sweepOrphanedFtsArtifacts(executor, rootTable) {
|
|
2107
|
+
const tableRows = await executor.all(
|
|
2108
|
+
`SELECT "name" FROM sqlite_master WHERE "type" = 'table'`,
|
|
2109
|
+
[]
|
|
2110
|
+
);
|
|
2111
|
+
const allTables = tableRows.map((r) => String(r.name));
|
|
2112
|
+
const catalogRows = await executor.all(
|
|
2113
|
+
`SELECT "table_name" FROM ${quoteIdent2(catalogTableName(rootTable))}`,
|
|
2114
|
+
[]
|
|
2115
|
+
);
|
|
2116
|
+
const catalogTables = catalogRows.map((r) => String(r.table_name));
|
|
2117
|
+
for (const name of findOrphanedFtsTables(allTables, catalogTables, rootTable)) {
|
|
2118
|
+
validateTableName(name);
|
|
2119
|
+
await executor.run(`DROP TABLE IF EXISTS ${quoteIdent2(name)}`, []);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
function wrapLocalSearchBackend(inner, executor, rootTable) {
|
|
2123
|
+
const caps = /* @__PURE__ */ new Set([
|
|
2124
|
+
...inner.capabilities.values(),
|
|
2125
|
+
"search.fullText",
|
|
2126
|
+
"search.vector"
|
|
2127
|
+
]);
|
|
2128
|
+
const healableTables = /* @__PURE__ */ new Set([
|
|
2129
|
+
inner.collectionPath,
|
|
2130
|
+
ftsTableName(inner.collectionPath),
|
|
2131
|
+
ftsMapTableName(inner.collectionPath)
|
|
2132
|
+
]);
|
|
2133
|
+
const runWithSchema = async (op) => {
|
|
2134
|
+
await inner.ensureReady();
|
|
2135
|
+
try {
|
|
2136
|
+
return await op();
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2139
|
+
const missing = /no such table: (\S+)/.exec(message)?.[1];
|
|
2140
|
+
if (missing === void 0 || !healableTables.has(missing)) throw err;
|
|
2141
|
+
await inner.ensureReady(true);
|
|
2142
|
+
return op();
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
const wrapper = {
|
|
2146
|
+
capabilities: createCapabilities(caps),
|
|
2147
|
+
collectionPath: inner.collectionPath,
|
|
2148
|
+
scopePath: inner.scopePath,
|
|
2149
|
+
getDoc: (docId) => inner.getDoc(docId),
|
|
2150
|
+
query: (filters, options) => inner.query(filters, options),
|
|
2151
|
+
setDoc: (docId, record, mode) => inner.setDoc(docId, record, mode),
|
|
2152
|
+
updateDoc: (docId, update) => inner.updateDoc(docId, update),
|
|
2153
|
+
deleteDoc: (docId) => inner.deleteDoc(docId),
|
|
2154
|
+
runTransaction: (fn) => inner.runTransaction(fn),
|
|
2155
|
+
createBatch: () => inner.createBatch(),
|
|
2156
|
+
subgraph: (parentNodeUid, name) => wrapLocalSearchBackend(inner.subgraph(parentNodeUid, name), executor, rootTable),
|
|
2157
|
+
removeNodeCascade: async (uid, reader, options) => {
|
|
2158
|
+
const result = await inner.removeNodeCascade(uid, reader, options);
|
|
2159
|
+
if (result.errors.length === 0) {
|
|
2160
|
+
await sweepOrphanedFtsArtifacts(executor, rootTable);
|
|
2161
|
+
}
|
|
2162
|
+
return result;
|
|
2163
|
+
},
|
|
2164
|
+
bulkRemoveEdges: (params, reader, options) => inner.bulkRemoveEdges(params, reader, options),
|
|
2165
|
+
aggregate: (spec, filters) => inner.aggregate(spec, filters),
|
|
2166
|
+
bulkDelete: (filters, options) => inner.bulkDelete(filters, options),
|
|
2167
|
+
bulkUpdate: (filters, patch, options) => inner.bulkUpdate(filters, patch, options),
|
|
2168
|
+
expand: (params) => inner.expand(params),
|
|
2169
|
+
findEdgesProjected: (select, filters, options) => inner.findEdgesProjected(select, filters, options),
|
|
2170
|
+
// `findEdgesGlobal` stays absent, same as the inner backend — each graph
|
|
2171
|
+
// is its own table; there is no cross-table index.
|
|
2172
|
+
async findNearest(params) {
|
|
2173
|
+
const { stmt, distancePath } = compileFindNearest(inner.collectionPath, params);
|
|
2174
|
+
const rows = await runWithSchema(() => executor.all(stmt.sql, stmt.params));
|
|
2175
|
+
return rows.map((row) => {
|
|
2176
|
+
const record = rowToRecord(row);
|
|
2177
|
+
if (distancePath) {
|
|
2178
|
+
const distance = row[DISTANCE_ALIAS];
|
|
2179
|
+
setDataPath(
|
|
2180
|
+
record.data,
|
|
2181
|
+
distancePath,
|
|
2182
|
+
typeof distance === "number" ? distance : Number(distance)
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
return record;
|
|
2186
|
+
});
|
|
2187
|
+
},
|
|
2188
|
+
async fullTextSearch(params) {
|
|
2189
|
+
const stmt = compileFullTextSearch(inner.collectionPath, params);
|
|
2190
|
+
let rows;
|
|
2191
|
+
try {
|
|
2192
|
+
rows = await runWithSchema(() => executor.all(stmt.sql, stmt.params));
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2195
|
+
if (isFts5QueryError(message)) {
|
|
2196
|
+
throw new FiregraphError(
|
|
2197
|
+
`fullTextSearch(): invalid FTS5 query syntax \u2014 ${message}`,
|
|
2198
|
+
"INVALID_QUERY"
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
throw err;
|
|
2202
|
+
}
|
|
2203
|
+
return rows.map(rowToRecord);
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
return wrapper;
|
|
2207
|
+
}
|
|
1782
2208
|
async function createLocalSqliteBackend(pathOrDb, options = {}) {
|
|
1783
2209
|
const {
|
|
1784
2210
|
tableName = "firegraph",
|
|
@@ -1815,7 +2241,18 @@ async function createLocalSqliteBackend(pathOrDb, options = {}) {
|
|
|
1815
2241
|
if (pragmas) {
|
|
1816
2242
|
applyPragmas(db, pragmas);
|
|
1817
2243
|
}
|
|
1818
|
-
|
|
2244
|
+
registerVectorUdf(db);
|
|
2245
|
+
const userExtraDDL = backendOptions.extraTableDDL;
|
|
2246
|
+
const optionsWithSearch = {
|
|
2247
|
+
...backendOptions,
|
|
2248
|
+
extraTableDDL: (table) => [
|
|
2249
|
+
...userExtraDDL ? userExtraDDL(table) : [],
|
|
2250
|
+
...buildLocalSearchDDL(table)
|
|
2251
|
+
]
|
|
2252
|
+
};
|
|
2253
|
+
const executor = createBetterSqliteExecutor(db);
|
|
2254
|
+
const inner = createSqliteBackend(executor, tableName, optionsWithSearch);
|
|
2255
|
+
const backend = wrapLocalSearchBackend(inner, executor, tableName);
|
|
1819
2256
|
let closed = false;
|
|
1820
2257
|
return {
|
|
1821
2258
|
backend,
|