@camstack/system 1.2.53 → 1.2.54
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/dist/addon-runner.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +17 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +246 -409
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +246 -409
- package/dist/builtins/sqlite-storage/vector-index-shared.d.ts +47 -0
- package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +55 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-f_YvZJaQ.mjs → dist-BOpRqInW.mjs} +1 -15
- package/dist/{dist-DKJqb0c7.js → dist-CblJsHb-.js} +0 -20
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +3 -2
- package/dist/builtins/sqlite-storage/vector-index.d.ts +0 -197
|
@@ -3,8 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CblJsHb-.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
|
+
let node_module = require("node:module");
|
|
8
9
|
let better_sqlite3 = require("better-sqlite3");
|
|
9
10
|
better_sqlite3 = require_chunk.__toESM(better_sqlite3);
|
|
10
11
|
//#region src/builtins/sqlite-storage/filter-compiler.ts
|
|
@@ -26,7 +27,7 @@ function fieldExprFor(field, shape) {
|
|
|
26
27
|
if (shape.kvBlobColumn !== null) return `json_extract("${shape.kvBlobColumn}", '$.${field}')`;
|
|
27
28
|
return null;
|
|
28
29
|
}
|
|
29
|
-
function compileFilter(filter, shape, mode, serialize) {
|
|
30
|
+
function compileFilter$1(filter, shape, mode, serialize) {
|
|
30
31
|
const clauses = [];
|
|
31
32
|
const params = [];
|
|
32
33
|
const resolve = (field) => {
|
|
@@ -254,7 +255,7 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
254
255
|
async deleteWhere({ namespace, collection, filter }) {
|
|
255
256
|
const scoped = this.scopedName(namespace, collection);
|
|
256
257
|
const decl = this.requireDeclared(scoped);
|
|
257
|
-
const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
|
|
258
|
+
const { whereSql, params } = compileFilter$1(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
|
|
258
259
|
return { deleted: this.getDb().prepare(`DELETE FROM "${scoped}"${whereSql}`).run(...params).changes };
|
|
259
260
|
}
|
|
260
261
|
/**
|
|
@@ -277,7 +278,7 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
277
278
|
setValues.push(this.serializeColumnValue(value));
|
|
278
279
|
}
|
|
279
280
|
if (setClauses.length === 0) throw new UnsafeFilterError("updateWhere was given no fields to set");
|
|
280
|
-
const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
|
|
281
|
+
const { whereSql, params } = compileFilter$1(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
|
|
281
282
|
return { updated: this.getDb().prepare(`UPDATE "${scoped}" SET ${setClauses.join(", ")}${whereSql}`).run(...setValues, ...params).changes };
|
|
282
283
|
}
|
|
283
284
|
async count({ namespace, collection, filter }) {
|
|
@@ -310,7 +311,7 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
310
311
|
const shape = this.shapeOf(decl);
|
|
311
312
|
const col = fieldExprFor(field, shape);
|
|
312
313
|
if (col === null) return [];
|
|
313
|
-
const { whereSql: where, params } = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
|
|
314
|
+
const { whereSql: where, params } = compileFilter$1(filter, shape, "select", (v) => this.serializeColumnValue(v));
|
|
314
315
|
const sql = `SELECT ${`CAST((${col} - ?) / ? AS INTEGER)`} AS bucket, COUNT(*) AS count FROM "${scoped}"${where} GROUP BY bucket ORDER BY bucket`;
|
|
315
316
|
return this.getDb().prepare(sql).all(origin, bucketSize, ...params).map((r) => ({
|
|
316
317
|
bucket: r.bucket,
|
|
@@ -327,7 +328,7 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
327
328
|
let sql = `SELECT ${[`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ")} FROM "${table}"`;
|
|
328
329
|
const params = [];
|
|
329
330
|
const shape = this.shapeOf(decl);
|
|
330
|
-
const compiled = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
|
|
331
|
+
const compiled = compileFilter$1(filter, shape, "select", (v) => this.serializeColumnValue(v));
|
|
331
332
|
sql += compiled.whereSql;
|
|
332
333
|
params.push(...compiled.params);
|
|
333
334
|
if (filter?.orderBy) {
|
|
@@ -647,225 +648,119 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
647
648
|
}
|
|
648
649
|
};
|
|
649
650
|
//#endregion
|
|
650
|
-
//#region src/builtins/sqlite-storage/vector-index.ts
|
|
651
|
-
/**
|
|
652
|
-
* SQLite-backed vector index — compact rows, exhaustive cosine ranking.
|
|
653
|
-
*
|
|
654
|
-
* Lives INSIDE the sqlite-settings builtin, against `SqliteSettingsBackend`
|
|
655
|
-
* directly, and that placement is the point. It first shipped as its own addon
|
|
656
|
-
* and that cost two round trips per write — caller → the `settings-store` door
|
|
657
|
-
* → the engine ([D44](../../../../../docs/decisions/adr-0044.md)) — plus a
|
|
658
|
-
* whole runner process, to reach the very database this addon already holds
|
|
659
|
-
* open. Here an upsert is a function call.
|
|
660
|
-
*
|
|
661
|
-
* Replaceability is unaffected: it is the CAPABILITY that makes the backend
|
|
662
|
-
* swappable, not the package. An ANN provider registers `vector-store` from
|
|
663
|
-
* wherever it likes and `setActiveSingleton` picks it.
|
|
664
|
-
*
|
|
665
|
-
* This is the DEFAULT `vector-store` provider, not the final one. It exists to
|
|
666
|
-
* make a row cheap; replacing exhaustive scan with a real ANN index is a
|
|
667
|
-
* different provider registered against the same capability.
|
|
668
|
-
*
|
|
669
|
-
* What it changes versus the per-store JSON columns it replaces:
|
|
670
|
-
*
|
|
671
|
-
* - the vector is a base64 Float32 TEXT column (~2.7 KB for 512 dims) instead
|
|
672
|
-
* of a JSON array of 512 numbers (~11.7 KB measured on the live hub), and
|
|
673
|
-
* decoding is a `readFloatLE` loop instead of `JSON.parse` of 512 tokens;
|
|
674
|
-
* - filterable metadata is promoted to real columns, so `deviceId` /
|
|
675
|
-
* `timestamp` / `className` prefilters are pushed into SQL and indexed
|
|
676
|
-
* rather than applied after loading everything;
|
|
677
|
-
* - a truncated scan is REPORTED (`truncated`), because a search that silently
|
|
678
|
-
* considered a subset reads as "nothing else matched".
|
|
679
|
-
*
|
|
680
|
-
* Metadata that is not one of the promoted columns rides in a JSON blob and is
|
|
681
|
-
* returned verbatim; it cannot be filtered on. That is deliberate — a filter a
|
|
682
|
-
* backend cannot push down is a filter that gets forgotten.
|
|
683
|
-
*/
|
|
684
|
-
/** Metadata keys promoted to real, indexable columns. */
|
|
651
|
+
//#region src/builtins/sqlite-storage/vector-index-shared.ts
|
|
652
|
+
/** Metadata keys promoted to real, indexable columns in every backend. */
|
|
685
653
|
var PROMOTED_COLUMNS = [
|
|
686
654
|
"deviceId",
|
|
687
655
|
"timestamp",
|
|
688
656
|
"className",
|
|
689
657
|
"modelId"
|
|
690
658
|
];
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
var
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/builtins/sqlite-storage/vector-index-vec.ts
|
|
661
|
+
var tableFor = (index) => `vec_${index.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
662
|
+
var registryTable = "vec_indexes";
|
|
663
|
+
/** `vec0` speaks packed Float32; the capability speaks base64 of the same. */
|
|
664
|
+
function toBlob(base64) {
|
|
665
|
+
return Buffer.from(base64, "base64");
|
|
666
|
+
}
|
|
694
667
|
/**
|
|
695
|
-
*
|
|
668
|
+
* `vec0` metadata columns are NOT NULLABLE.
|
|
696
669
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
670
|
+
* Passing NULL fails the whole insert with "Expected text for TEXT metadata
|
|
671
|
+
* column … received NULL", and our metadata is genuinely optional — a vector
|
|
672
|
+
* may carry no className or modelId. So absence is encoded with a sentinel and
|
|
673
|
+
* stripped again on read:
|
|
674
|
+
*
|
|
675
|
+
* - text → `''`. No real className or modelId is the empty string, so the
|
|
676
|
+
* round-trip is lossless.
|
|
677
|
+
* - integer → `MISSING_INT`. Chosen NEGATIVE because a device id and a
|
|
678
|
+
* millisecond timestamp are both non-negative in this system; 0 would be a
|
|
679
|
+
* plausible timestamp (1970) and could not be told from an absent one.
|
|
703
680
|
*/
|
|
704
|
-
var
|
|
705
|
-
|
|
706
|
-
|
|
681
|
+
var MISSING_INT = -1;
|
|
682
|
+
/**
|
|
683
|
+
* Bind an integer metadata value as BigInt.
|
|
684
|
+
*
|
|
685
|
+
* Not defensive: `better-sqlite3` binds a plain JS number as FLOAT and `vec0`
|
|
686
|
+
* refuses it outright for an INTEGER metadata column.
|
|
687
|
+
*/
|
|
688
|
+
function asInteger(value) {
|
|
689
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return BigInt(MISSING_INT);
|
|
690
|
+
return BigInt(Math.trunc(value));
|
|
691
|
+
}
|
|
692
|
+
function asText(value) {
|
|
693
|
+
return typeof value === "string" ? value : "";
|
|
694
|
+
}
|
|
695
|
+
/** The nullable read of a text column — `''` means the caller never set it. */
|
|
696
|
+
function readText(value) {
|
|
697
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
698
|
+
}
|
|
699
|
+
/** The nullable read of an integer column — the sentinel means absent. */
|
|
700
|
+
function readInt(value) {
|
|
701
|
+
const n = Number(value);
|
|
702
|
+
if (!Number.isFinite(n) || n === MISSING_INT) return null;
|
|
703
|
+
return n;
|
|
704
|
+
}
|
|
705
|
+
var SqliteVecVectorIndex = class {
|
|
706
|
+
db;
|
|
707
707
|
logger;
|
|
708
708
|
specs = /* @__PURE__ */ new Map();
|
|
709
709
|
constructor(deps) {
|
|
710
|
-
this.
|
|
710
|
+
this.db = deps.db;
|
|
711
711
|
this.logger = deps.logger;
|
|
712
712
|
}
|
|
713
|
-
/**
|
|
714
|
-
* Load persisted index declarations. Call once at startup, BEFORE serving:
|
|
715
|
-
* without it every method on an existing index throws until a write
|
|
716
|
-
* re-declares it.
|
|
717
|
-
*/
|
|
713
|
+
/** Recover index declarations so an existing table is usable from boot. */
|
|
718
714
|
async loadIndexRegistry() {
|
|
719
715
|
try {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
notNull: true
|
|
733
|
-
},
|
|
734
|
-
{
|
|
735
|
-
name: "metric",
|
|
736
|
-
type: "TEXT",
|
|
737
|
-
notNull: true
|
|
738
|
-
}
|
|
739
|
-
]
|
|
740
|
-
});
|
|
741
|
-
const rows = await this.store.query({ collection: INDEX_REGISTRY_COLLECTION });
|
|
742
|
-
for (const row of rows) {
|
|
743
|
-
const dim = row.data["dim"];
|
|
744
|
-
const metric = row.data["metric"];
|
|
745
|
-
if (typeof dim !== "number") continue;
|
|
746
|
-
await this.declareIndex(row.id, dim, isMetric(metric) ? metric : "cosine");
|
|
716
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS ${registryTable} (id TEXT PRIMARY KEY, dim INTEGER NOT NULL, metric TEXT NOT NULL)`);
|
|
717
|
+
const rows = this.db.prepare(`SELECT id, dim, metric FROM ${registryTable}`).all();
|
|
718
|
+
for (const raw of rows) {
|
|
719
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
720
|
+
const row = raw;
|
|
721
|
+
const id = asText(row["id"]);
|
|
722
|
+
const dim = typeof row["dim"] === "number" ? row["dim"] : Number(row["dim"]);
|
|
723
|
+
if (id === null || !Number.isFinite(dim)) continue;
|
|
724
|
+
this.specs.set(id, {
|
|
725
|
+
dim,
|
|
726
|
+
metric: metricOf(row["metric"])
|
|
727
|
+
});
|
|
747
728
|
}
|
|
748
|
-
this.logger.info("
|
|
729
|
+
this.logger.info("sqlite-vec index registry loaded", { meta: { indexes: this.specs.size } });
|
|
749
730
|
} catch (err) {
|
|
750
|
-
this.logger.error("
|
|
731
|
+
this.logger.error("sqlite-vec registry failed to load", { meta: { error: String(err) } });
|
|
751
732
|
}
|
|
752
733
|
}
|
|
753
|
-
/**
|
|
754
|
-
* Idempotent. Re-declaring an index with a DIFFERENT dim throws rather than
|
|
755
|
-
* wiping: silently dropping a populated index because a caller shipped a new
|
|
756
|
-
* model would destroy history that cannot be rebuilt.
|
|
757
|
-
*/
|
|
758
734
|
async declareIndex(index, dim, metric) {
|
|
759
735
|
const existing = this.specs.get(index);
|
|
760
736
|
if (existing !== void 0 && existing.dim !== dim) throw new Error(`vector-store: index "${index}" is declared with dim ${existing.dim}; refusing to redeclare as ${dim}`);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
type: "TEXT",
|
|
773
|
-
notNull: true
|
|
774
|
-
},
|
|
775
|
-
{
|
|
776
|
-
name: "deviceId",
|
|
777
|
-
type: "INTEGER"
|
|
778
|
-
},
|
|
779
|
-
{
|
|
780
|
-
name: "timestamp",
|
|
781
|
-
type: "INTEGER"
|
|
782
|
-
},
|
|
783
|
-
{
|
|
784
|
-
name: "className",
|
|
785
|
-
type: "TEXT"
|
|
786
|
-
},
|
|
787
|
-
{
|
|
788
|
-
name: "modelId",
|
|
789
|
-
type: "TEXT"
|
|
790
|
-
},
|
|
791
|
-
{
|
|
792
|
-
name: "extra",
|
|
793
|
-
type: "JSON"
|
|
794
|
-
}
|
|
795
|
-
],
|
|
796
|
-
indexes: [
|
|
797
|
-
{
|
|
798
|
-
name: `idx_vec_${index}_dev_ts`,
|
|
799
|
-
columns: ["deviceId", "timestamp"]
|
|
800
|
-
},
|
|
801
|
-
{
|
|
802
|
-
name: `idx_vec_${index}_ts`,
|
|
803
|
-
columns: ["timestamp"]
|
|
804
|
-
},
|
|
805
|
-
{
|
|
806
|
-
name: `idx_vec_${index}_class`,
|
|
807
|
-
columns: ["className"]
|
|
808
|
-
}
|
|
809
|
-
]
|
|
810
|
-
});
|
|
737
|
+
this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${tableFor(index)} USING vec0(
|
|
738
|
+
id TEXT PRIMARY KEY,
|
|
739
|
+
embedding float[${dim}] distance_metric=${metric === "euclidean" ? "L2" : "cosine"},
|
|
740
|
+
deviceId integer,
|
|
741
|
+
timestamp integer,
|
|
742
|
+
className text,
|
|
743
|
+
modelId text,
|
|
744
|
+
+extra text
|
|
745
|
+
)`);
|
|
746
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS ${registryTable} (id TEXT PRIMARY KEY, dim INTEGER NOT NULL, metric TEXT NOT NULL)`);
|
|
747
|
+
this.db.prepare(`INSERT OR REPLACE INTO ${registryTable}(id, dim, metric) VALUES (?, ?, ?)`).run(index, BigInt(dim), metric);
|
|
811
748
|
this.specs.set(index, {
|
|
812
749
|
dim,
|
|
813
750
|
metric
|
|
814
751
|
});
|
|
815
|
-
try {
|
|
816
|
-
await this.store.set({
|
|
817
|
-
collection: INDEX_REGISTRY_COLLECTION,
|
|
818
|
-
key: index,
|
|
819
|
-
value: {
|
|
820
|
-
dim,
|
|
821
|
-
metric
|
|
822
|
-
}
|
|
823
|
-
});
|
|
824
|
-
} catch (err) {
|
|
825
|
-
this.logger.warn("vector-store index declaration not persisted", { meta: {
|
|
826
|
-
index,
|
|
827
|
-
dim,
|
|
828
|
-
error: String(err)
|
|
829
|
-
} });
|
|
830
|
-
}
|
|
831
752
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
* The recovery is not belt-and-braces, it is required: indexes created before
|
|
837
|
-
* the registry existed have rows and no declaration, and so would be
|
|
838
|
-
* permanently unqueryable — which is exactly how this was found, with 8,494
|
|
839
|
-
* live vectors that `stats` refused to look at. The dimension is derivable
|
|
840
|
-
* from any stored vector, so derive it and persist it rather than demanding
|
|
841
|
-
* the caller redeclare. The metric is NOT derivable and falls back to
|
|
842
|
-
* `cosine`, which is what every current caller declares.
|
|
843
|
-
*/
|
|
844
|
-
async resolveSpec(index) {
|
|
845
|
-
const known = this.specs.get(index);
|
|
846
|
-
if (known !== void 0) return known;
|
|
847
|
-
const sample = (await this.store.query({
|
|
848
|
-
collection: collectionFor(index),
|
|
849
|
-
filter: { limit: 1 }
|
|
850
|
-
}))[0];
|
|
851
|
-
const vector = sample === void 0 ? void 0 : readRow(sample.data)?.vector;
|
|
852
|
-
if (vector === void 0) throw new Error(`vector-store: index "${index}" was never declared`);
|
|
853
|
-
const dim = require_dist.vectorDimFromBase64(vector);
|
|
854
|
-
this.logger.warn("vector-store recovered an undeclared index from its rows", { meta: {
|
|
855
|
-
index,
|
|
856
|
-
dim,
|
|
857
|
-
metric: "cosine"
|
|
858
|
-
} });
|
|
859
|
-
await this.declareIndex(index, dim, "cosine");
|
|
860
|
-
return {
|
|
861
|
-
dim,
|
|
862
|
-
metric: "cosine"
|
|
863
|
-
};
|
|
753
|
+
specOf(index) {
|
|
754
|
+
const spec = this.specs.get(index);
|
|
755
|
+
if (spec === void 0) throw new Error(`vector-store: index "${index}" was never declared`);
|
|
756
|
+
return spec;
|
|
864
757
|
}
|
|
865
|
-
/** Upsert items, refusing any whose vector length disagrees with the index. */
|
|
866
758
|
async upsert(index, items) {
|
|
867
|
-
const spec =
|
|
868
|
-
const
|
|
759
|
+
const spec = this.specOf(index);
|
|
760
|
+
const del = this.db.prepare(`DELETE FROM ${tableFor(index)} WHERE id = ?`);
|
|
761
|
+
const stmt = this.db.prepare(`INSERT INTO ${tableFor(index)}
|
|
762
|
+
(id, embedding, deviceId, timestamp, className, modelId, extra)
|
|
763
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
869
764
|
let upserted = 0;
|
|
870
765
|
let rejected = 0;
|
|
871
766
|
for (const item of items) {
|
|
@@ -873,34 +768,22 @@ var SqliteVectorIndex = class {
|
|
|
873
768
|
rejected += 1;
|
|
874
769
|
continue;
|
|
875
770
|
}
|
|
876
|
-
const { deviceId, timestamp, className, modelId, extra } = splitMetadata(item.metadata);
|
|
877
771
|
try {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
vector: item.vector,
|
|
883
|
-
deviceId,
|
|
884
|
-
timestamp,
|
|
885
|
-
className,
|
|
886
|
-
modelId,
|
|
887
|
-
extra
|
|
888
|
-
}
|
|
889
|
-
});
|
|
772
|
+
const extra = {};
|
|
773
|
+
for (const [k, v] of Object.entries(item.metadata)) if (!isPromoted(k)) extra[k] = v;
|
|
774
|
+
del.run(item.id);
|
|
775
|
+
stmt.run(item.id, toBlob(item.vector), asInteger(item.metadata["deviceId"]), asInteger(item.metadata["timestamp"]), asText(item.metadata["className"]), asText(item.metadata["modelId"]), JSON.stringify(extra));
|
|
890
776
|
upserted += 1;
|
|
891
777
|
} catch (err) {
|
|
892
778
|
rejected += 1;
|
|
893
|
-
this.logger.warn("
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
error: String(err)
|
|
899
|
-
}
|
|
900
|
-
});
|
|
779
|
+
this.logger.warn("sqlite-vec upsert failed", { meta: {
|
|
780
|
+
index,
|
|
781
|
+
id: item.id,
|
|
782
|
+
error: String(err)
|
|
783
|
+
} });
|
|
901
784
|
}
|
|
902
785
|
}
|
|
903
|
-
if (rejected > 0) this.logger.warn("
|
|
786
|
+
if (rejected > 0) this.logger.warn("sqlite-vec rejected items on upsert", { meta: {
|
|
904
787
|
index,
|
|
905
788
|
rejected,
|
|
906
789
|
expectedDim: spec.dim
|
|
@@ -910,237 +793,168 @@ var SqliteVectorIndex = class {
|
|
|
910
793
|
rejected
|
|
911
794
|
};
|
|
912
795
|
}
|
|
913
|
-
/** Exhaustive ranked search over the rows a prefilter admits. */
|
|
914
796
|
async query(params) {
|
|
915
|
-
const spec =
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
field: "timestamp",
|
|
924
|
-
direction: "desc"
|
|
925
|
-
},
|
|
926
|
-
limit: MAX_SCAN_ROWS
|
|
927
|
-
}
|
|
928
|
-
});
|
|
797
|
+
const spec = this.specOf(params.index);
|
|
798
|
+
if (require_dist.decodeVectorBase64(params.vector).length !== spec.dim) throw new Error(`vector-store: query vector has ${require_dist.decodeVectorBase64(params.vector).length} dims, index "${params.index}" has ${spec.dim}`);
|
|
799
|
+
const { clauses, values, unpushable } = compileFilter(params.filter);
|
|
800
|
+
const where = clauses.length > 0 ? ` AND ${clauses.join(" AND ")}` : "";
|
|
801
|
+
const rows = this.db.prepare(`SELECT id, distance, deviceId, timestamp, className, modelId, extra
|
|
802
|
+
FROM ${tableFor(params.index)}
|
|
803
|
+
WHERE embedding MATCH ? AND k = ?${where}
|
|
804
|
+
ORDER BY distance`).all(toBlob(params.vector), BigInt(params.topK), ...values);
|
|
929
805
|
const minScore = params.minScore ?? Number.NEGATIVE_INFINITY;
|
|
930
|
-
const
|
|
931
|
-
for (const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const
|
|
936
|
-
if (
|
|
937
|
-
const
|
|
806
|
+
const matches = [];
|
|
807
|
+
for (const raw of rows) {
|
|
808
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
809
|
+
const row = raw;
|
|
810
|
+
const id = asText(row["id"]);
|
|
811
|
+
const distance = typeof row["distance"] === "number" ? row["distance"] : Number(row["distance"]);
|
|
812
|
+
if (id === null || !Number.isFinite(distance)) continue;
|
|
813
|
+
const metadata = rebuild(row);
|
|
814
|
+
if (!passesUnpushable(metadata, unpushable)) continue;
|
|
815
|
+
const score = spec.metric === "euclidean" ? -distance : 1 - distance;
|
|
938
816
|
if (score < minScore) continue;
|
|
939
|
-
|
|
940
|
-
id
|
|
817
|
+
matches.push({
|
|
818
|
+
id,
|
|
941
819
|
score,
|
|
942
|
-
metadata
|
|
820
|
+
metadata
|
|
943
821
|
});
|
|
944
822
|
}
|
|
945
|
-
scored.sort((a, b) => b.score - a.score);
|
|
946
823
|
return {
|
|
947
|
-
matches
|
|
824
|
+
matches,
|
|
948
825
|
scanned: rows.length,
|
|
949
|
-
truncated:
|
|
826
|
+
truncated: false
|
|
950
827
|
};
|
|
951
828
|
}
|
|
952
|
-
/**
|
|
953
|
-
* Metadata for the given ids, WITHOUT decoding their vectors.
|
|
954
|
-
*
|
|
955
|
-
* The caller is a best-of gate comparing confidences, so the vector is dead
|
|
956
|
-
* weight on that path — reading it would reintroduce exactly the per-row cost
|
|
957
|
-
* this index exists to remove. Missing ids are absent from the result rather
|
|
958
|
-
* than present with an empty metadata object.
|
|
959
|
-
*/
|
|
960
829
|
async getByIds(index, ids) {
|
|
961
|
-
|
|
830
|
+
this.specOf(index);
|
|
962
831
|
if (ids.length === 0) return [];
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
});
|
|
967
|
-
const wanted = new Set(ids);
|
|
832
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
833
|
+
const rows = this.db.prepare(`SELECT id, deviceId, timestamp, className, modelId, extra
|
|
834
|
+
FROM ${tableFor(index)} WHERE id IN (${placeholders})`).all(...ids);
|
|
968
835
|
const out = [];
|
|
969
|
-
for (const
|
|
970
|
-
if (
|
|
971
|
-
const
|
|
972
|
-
|
|
836
|
+
for (const raw of rows) {
|
|
837
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
838
|
+
const row = raw;
|
|
839
|
+
const id = asText(row["id"]);
|
|
840
|
+
if (id === null) continue;
|
|
973
841
|
out.push({
|
|
974
|
-
id
|
|
975
|
-
metadata:
|
|
842
|
+
id,
|
|
843
|
+
metadata: rebuild(row)
|
|
976
844
|
});
|
|
977
845
|
}
|
|
978
846
|
return out;
|
|
979
847
|
}
|
|
980
848
|
async deleteByIds(index, ids) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
collection,
|
|
986
|
-
key: id
|
|
987
|
-
});
|
|
988
|
-
deleted += 1;
|
|
989
|
-
} catch (err) {
|
|
990
|
-
this.logger.warn("vector-store delete failed", { meta: {
|
|
991
|
-
index,
|
|
992
|
-
id,
|
|
993
|
-
error: String(err)
|
|
994
|
-
} });
|
|
995
|
-
}
|
|
996
|
-
return deleted;
|
|
849
|
+
this.specOf(index);
|
|
850
|
+
if (ids.length === 0) return 0;
|
|
851
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
852
|
+
return this.db.prepare(`DELETE FROM ${tableFor(index)} WHERE id IN (${placeholders})`).run(...ids).changes;
|
|
997
853
|
}
|
|
998
|
-
/**
|
|
999
|
-
* Delete every row the filter admits.
|
|
1000
|
-
*
|
|
1001
|
-
* When every clause is pushable, this is ONE bulk statement. When the filter
|
|
1002
|
-
* touches a non-promoted key it falls back to select-then-delete, because
|
|
1003
|
-
* those clauses live in the JSON remainder and only JS can evaluate them —
|
|
1004
|
-
* and an unapplied clause would delete rows the caller wanted kept, which is
|
|
1005
|
-
* the one failure mode a delete must not have.
|
|
1006
|
-
*/
|
|
1007
854
|
async deleteByFilter(index, filter) {
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
if (
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
}).map((r) => r.id);
|
|
1024
|
-
return this.deleteByIds(index, ids);
|
|
855
|
+
this.specOf(index);
|
|
856
|
+
const { clauses, values, unpushable } = compileFilter(filter);
|
|
857
|
+
if (unpushable.length === 0) {
|
|
858
|
+
const where = clauses.length > 0 ? clauses.join(" AND ") : "1=1";
|
|
859
|
+
return this.db.prepare(`DELETE FROM ${tableFor(index)} WHERE ${where}`).run(...values).changes;
|
|
860
|
+
}
|
|
861
|
+
const rows = this.db.prepare(`SELECT id, deviceId, timestamp, className, modelId, extra FROM ${tableFor(index)}${clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : ""}`).all(...values);
|
|
862
|
+
const doomed = [];
|
|
863
|
+
for (const raw of rows) {
|
|
864
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
865
|
+
const row = raw;
|
|
866
|
+
const id = asText(row["id"]);
|
|
867
|
+
if (id !== null && passesUnpushable(rebuild(row), unpushable)) doomed.push(id);
|
|
868
|
+
}
|
|
869
|
+
return this.deleteByIds(index, doomed);
|
|
1025
870
|
}
|
|
1026
871
|
async stats(index) {
|
|
1027
|
-
const spec =
|
|
872
|
+
const spec = this.specOf(index);
|
|
873
|
+
const row = this.db.prepare(`SELECT count(*) AS c FROM ${tableFor(index)}`).get();
|
|
874
|
+
const count = typeof row === "object" && row !== null ? Number(row["c"]) : 0;
|
|
1028
875
|
return {
|
|
1029
|
-
backend: "sqlite-
|
|
1030
|
-
count: (
|
|
1031
|
-
collection: collectionFor(index),
|
|
1032
|
-
filter: { limit: MAX_SCAN_ROWS }
|
|
1033
|
-
})).length,
|
|
876
|
+
backend: "sqlite-vec",
|
|
877
|
+
count: Number.isFinite(count) ? count : 0,
|
|
1034
878
|
dim: spec.dim,
|
|
1035
879
|
metric: spec.metric,
|
|
1036
880
|
exact: true
|
|
1037
881
|
};
|
|
1038
882
|
}
|
|
1039
883
|
};
|
|
1040
|
-
/** Split caller metadata into promoted columns plus a verbatim remainder. */
|
|
1041
|
-
function splitMetadata(metadata) {
|
|
1042
|
-
const extra = {};
|
|
1043
|
-
for (const [key, value] of Object.entries(metadata)) if (!isPromoted(key)) extra[key] = value;
|
|
1044
|
-
return {
|
|
1045
|
-
deviceId: numberOrNull(metadata["deviceId"]),
|
|
1046
|
-
timestamp: numberOrNull(metadata["timestamp"]),
|
|
1047
|
-
className: stringOrNull(metadata["className"]),
|
|
1048
|
-
modelId: stringOrNull(metadata["modelId"]),
|
|
1049
|
-
extra
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
884
|
function isPromoted(key) {
|
|
1053
|
-
for (const
|
|
885
|
+
for (const p of PROMOTED_COLUMNS) if (p === key) return true;
|
|
1054
886
|
return false;
|
|
1055
887
|
}
|
|
1056
|
-
function
|
|
1057
|
-
return
|
|
1058
|
-
}
|
|
1059
|
-
function stringOrNull(value) {
|
|
1060
|
-
return typeof value === "string" ? value : null;
|
|
888
|
+
function metricOf(value) {
|
|
889
|
+
return value === "euclidean" || value === "dot" ? value : "cosine";
|
|
1061
890
|
}
|
|
1062
|
-
/**
|
|
1063
|
-
function
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
if (
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
/** The part of a filter SQLite can apply: promoted columns only. */
|
|
1072
|
-
function buildSqlFilter(filter) {
|
|
1073
|
-
if (filter === void 0) return {};
|
|
1074
|
-
const where = {};
|
|
1075
|
-
const whereBetween = {};
|
|
1076
|
-
for (const [key, value] of Object.entries(filter.equals ?? {})) if (isPromoted(key)) where[key] = value;
|
|
1077
|
-
for (const [key, range] of Object.entries(filter.between ?? {})) if (isPromoted(key)) whereBetween[key] = range;
|
|
1078
|
-
return {
|
|
1079
|
-
...Object.keys(where).length > 0 ? { where } : {},
|
|
1080
|
-
...Object.keys(whereBetween).length > 0 ? { whereBetween } : {}
|
|
891
|
+
/** Split a filter into SQL clauses and the remainder JS must apply. */
|
|
892
|
+
function compileFilter(filter) {
|
|
893
|
+
const clauses = [];
|
|
894
|
+
const values = [];
|
|
895
|
+
const unpushable = [];
|
|
896
|
+
if (filter === void 0) return {
|
|
897
|
+
clauses,
|
|
898
|
+
values,
|
|
899
|
+
unpushable
|
|
1081
900
|
};
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
for (const [key, expected] of Object.entries(filter.equals ?? {})) {
|
|
1093
|
-
if (isPromoted(key)) continue;
|
|
1094
|
-
if (row.extra[key] !== expected) return false;
|
|
901
|
+
for (const [key, value] of Object.entries(filter.equals ?? {})) {
|
|
902
|
+
if (!isPromoted(key)) {
|
|
903
|
+
unpushable.push({
|
|
904
|
+
key,
|
|
905
|
+
equals: value
|
|
906
|
+
});
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
clauses.push(`${key} = ?`);
|
|
910
|
+
values.push(typeof value === "number" ? BigInt(Math.trunc(value)) : value);
|
|
1095
911
|
}
|
|
1096
912
|
for (const [key, [lo, hi]] of Object.entries(filter.between ?? {})) {
|
|
1097
|
-
if (isPromoted(key))
|
|
1098
|
-
|
|
1099
|
-
|
|
913
|
+
if (!isPromoted(key)) {
|
|
914
|
+
unpushable.push({
|
|
915
|
+
key,
|
|
916
|
+
between: [lo, hi]
|
|
917
|
+
});
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
clauses.push(`${key} BETWEEN ? AND ?`);
|
|
921
|
+
values.push(BigInt(Math.trunc(lo)), BigInt(Math.trunc(hi)));
|
|
1100
922
|
}
|
|
1101
|
-
return true;
|
|
1102
|
-
}
|
|
1103
|
-
function readRow(data) {
|
|
1104
|
-
const vector = data["vector"];
|
|
1105
|
-
if (typeof vector !== "string" || vector.length === 0) return null;
|
|
1106
|
-
const extra = data["extra"];
|
|
1107
923
|
return {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
className: stringOrNull(data["className"]),
|
|
1112
|
-
modelId: stringOrNull(data["modelId"]),
|
|
1113
|
-
extra: isMetadata(extra) ? extra : {}
|
|
924
|
+
clauses,
|
|
925
|
+
values,
|
|
926
|
+
unpushable
|
|
1114
927
|
};
|
|
1115
928
|
}
|
|
1116
|
-
function
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
929
|
+
function passesUnpushable(metadata, unpushable) {
|
|
930
|
+
for (const clause of unpushable) {
|
|
931
|
+
const actual = metadata[clause.key];
|
|
932
|
+
if (clause.equals !== void 0 && actual !== clause.equals) return false;
|
|
933
|
+
if (clause.between !== void 0) {
|
|
934
|
+
const [lo, hi] = clause.between;
|
|
935
|
+
if (typeof actual !== "number" || actual < lo || actual > hi) return false;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
1123
938
|
return true;
|
|
1124
939
|
}
|
|
1125
|
-
/**
|
|
1126
|
-
function
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
return distance === 0 ? 0 : -distance;
|
|
940
|
+
/** Rebuild caller metadata from the promoted columns plus the JSON remainder. */
|
|
941
|
+
function rebuild(row) {
|
|
942
|
+
let extra = {};
|
|
943
|
+
const rawExtra = row["extra"];
|
|
944
|
+
if (typeof rawExtra === "string" && rawExtra.length > 0) try {
|
|
945
|
+
const parsed = JSON.parse(rawExtra);
|
|
946
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) extra = parsed;
|
|
947
|
+
} catch {}
|
|
948
|
+
const out = { ...extra };
|
|
949
|
+
const deviceId = readInt(row["deviceId"]);
|
|
950
|
+
const timestamp = readInt(row["timestamp"]);
|
|
951
|
+
const className = readText(row["className"]);
|
|
952
|
+
const modelId = readText(row["modelId"]);
|
|
953
|
+
if (deviceId !== null) out["deviceId"] = deviceId;
|
|
954
|
+
if (timestamp !== null) out["timestamp"] = timestamp;
|
|
955
|
+
if (className !== null) out["className"] = className;
|
|
956
|
+
if (modelId !== null) out["modelId"] = modelId;
|
|
957
|
+
return out;
|
|
1144
958
|
}
|
|
1145
959
|
//#endregion
|
|
1146
960
|
//#region src/builtins/sqlite-storage/sqlite-settings.addon.ts
|
|
@@ -1186,10 +1000,7 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
|
|
|
1186
1000
|
this.ctx.logger.info("DB file status", { meta: { dbExists } });
|
|
1187
1001
|
this.backend = new SqliteSettingsBackend(dbPath, { ...require_dist.RUNTIME_DEFAULTS });
|
|
1188
1002
|
await this.backend.initialize();
|
|
1189
|
-
const vectorIndex =
|
|
1190
|
-
store: this.backend,
|
|
1191
|
-
logger: this.ctx.logger.child("VectorIndex")
|
|
1192
|
-
});
|
|
1003
|
+
const vectorIndex = this.createVectorIndex(this.backend);
|
|
1193
1004
|
await vectorIndex.loadIndexRegistry();
|
|
1194
1005
|
this.vectorIndex = vectorIndex;
|
|
1195
1006
|
const vectorProvider = {
|
|
@@ -1224,6 +1035,32 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
|
|
|
1224
1035
|
getBackend() {
|
|
1225
1036
|
return this.backend;
|
|
1226
1037
|
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Build the vector index.
|
|
1040
|
+
*
|
|
1041
|
+
* ONE implementation, deliberately. A second backend behind a flag means two
|
|
1042
|
+
* code paths that can disagree, two sets of gotchas, and a silent fallback
|
|
1043
|
+
* that turns "the extension is missing" into "search is mysteriously slow" —
|
|
1044
|
+
* the failure this codebase keeps paying for. The capability is the swap
|
|
1045
|
+
* point: another provider replaces this one wholesale, it does not hide
|
|
1046
|
+
* inside it.
|
|
1047
|
+
*
|
|
1048
|
+
* So a failed extension load THROWS. A node without the native binary must
|
|
1049
|
+
* fail at boot with a message naming sqlite-vec, not serve degraded results
|
|
1050
|
+
* nobody knows are degraded.
|
|
1051
|
+
*/
|
|
1052
|
+
createVectorIndex(backend) {
|
|
1053
|
+
const logger = this.ctx.logger.child("VectorIndex");
|
|
1054
|
+
const db = backend.getDatabase();
|
|
1055
|
+
if (db === null) throw new Error("vector-store: the SQLite database is not open");
|
|
1056
|
+
const sv = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("sqlite-vec");
|
|
1057
|
+
db.loadExtension(sv.getLoadablePath());
|
|
1058
|
+
logger.info("sqlite-vec extension loaded");
|
|
1059
|
+
return new SqliteVecVectorIndex({
|
|
1060
|
+
db,
|
|
1061
|
+
logger
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1227
1064
|
/** Exposed for tests; null before `onInitialize`. */
|
|
1228
1065
|
getVectorIndex() {
|
|
1229
1066
|
return this.vectorIndex;
|