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