@fgv/ts-agent-memory-sqlite-vec 5.1.0-45 → 5.1.0-46

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.
Files changed (25) hide show
  1. package/.rush/temp/{e3385c5c74e11d46b4fbe400b497eb3f33a009b6.tar.log → b39f85377e1ce7a5b4a0aad208a6f48e4fba124b.tar.log} +2 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory-sqlite-vec.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/README.md +16 -2
  7. package/dist/packlets/sqlite-vec-index/sqliteVecFragmentIndex.js +144 -26
  8. package/dist/packlets/sqlite-vec-index/sqliteVecFragmentIndex.js.map +1 -1
  9. package/dist/test/unit/sqliteVecFragmentIndex.test.js +168 -7
  10. package/dist/test/unit/sqliteVecFragmentIndex.test.js.map +1 -1
  11. package/dist/ts-agent-memory-sqlite-vec.d.ts +68 -11
  12. package/lib/packlets/sqlite-vec-index/sqliteVecFragmentIndex.d.ts +68 -11
  13. package/lib/packlets/sqlite-vec-index/sqliteVecFragmentIndex.d.ts.map +1 -1
  14. package/lib/packlets/sqlite-vec-index/sqliteVecFragmentIndex.js +144 -26
  15. package/lib/packlets/sqlite-vec-index/sqliteVecFragmentIndex.js.map +1 -1
  16. package/lib/test/unit/sqliteVecFragmentIndex.test.js +168 -7
  17. package/lib/test/unit/sqliteVecFragmentIndex.test.js.map +1 -1
  18. package/package.json +7 -7
  19. package/rush-logs/ts-agent-memory-sqlite-vec.build.cache.log +1 -1
  20. package/rush-logs/ts-agent-memory-sqlite-vec.build.log +2 -2
  21. package/src/packlets/sqlite-vec-index/sqliteVecFragmentIndex.ts +184 -25
  22. package/src/test/unit/sqliteVecFragmentIndex.test.ts +236 -11
  23. package/temp/build/lint/_eslint-5eVG3S6w.json +2 -2
  24. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  25. package/temp/ts-agent-memory-sqlite-vec.api.json +2 -2
@@ -10,12 +10,25 @@ import { ISqliteVecFragmentIndexCreateParams } from './model';
10
10
  * @remarks
11
11
  * Where {@link SqliteVecVectorIndex} keys one vector per record on a
12
12
  * `target_key` primary key, this index holds **many** vectors per record — one per
13
- * in-record `[start, end)` span — so it keys the `vec0` table on `target_key` as a
14
- * **`PARTITION KEY`** (many rows may share it) and stores each fragment's locator
15
- * offsets in two auxiliary columns (`+start_off`, `+end_off`) that ride alongside
16
- * the vector and are returned on query but never filtered. A query is a brute-force
17
- * `vec0` KNN scan across all partitions returning per-fragment hits, each carrying
18
- * its record `target` and the matched `locator`.
13
+ * fragment — so it keys the `vec0` table on `target_key` as a **`PARTITION KEY`**
14
+ * (many rows may share it) and stores each fragment's identity in three auxiliary
15
+ * columns (`+start_off`, `+end_off`, `+fragment_id`) that ride alongside the vector
16
+ * and are returned on query but never filtered in particular `fragment_id` is
17
+ * stored and returned verbatim, never parsed and never part of the query path. A
18
+ * query is a brute-force `vec0` KNN scan across all partitions returning per-fragment
19
+ * hits, each carrying its record `target` plus whichever identity fields the stored
20
+ * fragment was added with (a fragment must carry at least one).
21
+ *
22
+ * **`vec0` schema changes require a drop-and-re-index.** A
23
+ * `CREATE VIRTUAL TABLE IF NOT EXISTS` is a no-op against an existing table (SQLite
24
+ * does not compare schemas) and `vec0` has no `ALTER TABLE ADD COLUMN`, so a database written by an
25
+ * earlier version of this package keeps its old auxiliary columns. `create` detects
26
+ * that by parsing the stored `CREATE VIRTUAL TABLE` SQL and fails with an actionable
27
+ * message naming the expected and found columns, rather than letting a widened
28
+ * `INSERT` surface an opaque `no such column` at statement-prepare time. There are no
29
+ * in-place migrations: drop the table (or use a fresh `tableName`) and re-index.
30
+ * Fragment vectors are re-derivable from the records, so this costs embedding time,
31
+ * never data.
19
32
  *
20
33
  * Semantics match `InMemoryFragmentCosineIndex` exactly: `addFragments` is
21
34
  * whole-record-replace (a single transaction deletes every prior fragment of the
@@ -47,12 +60,15 @@ export declare class SqliteVecFragmentIndex implements IFragmentVectorIndex {
47
60
  /**
48
61
  * Family-convention factory. Loads the `sqlite-vec` extension onto the supplied
49
62
  * `better-sqlite3` connection and, if the fragment table already exists (a
50
- * reopened persistent file), recovers its established dimension so no
51
- * re-embedding is needed on open.
63
+ * reopened persistent file), verifies its auxiliary-column set matches this
64
+ * version's and recovers its established dimension so no re-embedding is needed on
65
+ * open.
52
66
  *
53
67
  * @param params - See {@link ISqliteVecFragmentIndexCreateParams}.
54
68
  * @returns `Success` with the index, or `Failure` if the table name is not a
55
- * simple identifier or the extension fails to load.
69
+ * simple identifier, the extension fails to load, or the existing table was
70
+ * written by a version with a different auxiliary-column set (which requires a
71
+ * drop-and-re-index — `vec0` cannot be altered in place).
56
72
  */
57
73
  static create(params: ISqliteVecFragmentIndexCreateParams): Promise<Result<SqliteVecFragmentIndex>>;
58
74
  /** {@inheritDoc IFragmentVectorIndex.addFragments} */
@@ -61,16 +77,57 @@ export declare class SqliteVecFragmentIndex implements IFragmentVectorIndex {
61
77
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
62
78
  /** {@inheritDoc IFragmentVectorIndex.query} */
63
79
  query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
64
- /** Create the fragment `vec0` virtual table with the established dimension. */
80
+ /**
81
+ * Create the fragment `vec0` virtual table with the established dimension. The
82
+ * auxiliary columns must stay in sync with `AUXILIARY_COLUMNS`, which
83
+ * `create` compares against an existing table's stored DDL.
84
+ */
65
85
  private _createTable;
66
86
  /** Prepare the statements the index reuses. Requires the table to exist. */
67
87
  private _prepare;
68
88
  /**
69
89
  * Recover the established dimension of an existing fragment `vec0` table from its
70
- * stored `CREATE VIRTUAL TABLE` SQL (`float[<n>]`). Returns `undefined` when the
90
+ * stored `CREATE VIRTUAL TABLE` SQL (`float[<n>]`), after checking that the table's
91
+ * auxiliary columns match `AUXILIARY_COLUMNS`. Returns `undefined` when the
71
92
  * table does not exist yet (a fresh database — dimension is set by the first add).
93
+ *
94
+ * Throws when a table of that name exists but is not a usable fragment index (a
95
+ * mismatched auxiliary-column set, or no `vec0` embedding column); the caller runs
96
+ * this inside `captureResult`, so it surfaces as a loud `Failure` from `create`.
97
+ * The same stored DDL answers every one of those questions, so the checks cost
98
+ * nothing extra.
72
99
  */
73
100
  private static _readExistingDimension;
101
+ /**
102
+ * Compare an existing table's auxiliary columns against `AUXILIARY_COLUMNS`.
103
+ *
104
+ * `CREATE VIRTUAL TABLE IF NOT EXISTS` is a no-op against an existing table (SQLite
105
+ * never compares schemas) and `vec0` has no `ALTER TABLE ADD COLUMN`, so a table
106
+ * written by an earlier version of this package silently keeps its old columns and
107
+ * only fails later — as an opaque `no such column` when the widened `INSERT` is
108
+ * prepared. Detect it here instead and say what to do about it. Order is not
109
+ * compared: every statement names its columns explicitly, so only the set matters.
110
+ */
111
+ private static _verifyAuxiliaryColumns;
112
+ /**
113
+ * Rebuild the identity fields of a hit from a persisted row, omitting each field
114
+ * the stored fragment did not carry (so a hit is structurally identical to one this
115
+ * index produced before `fragment_id` existed).
116
+ *
117
+ * A row carrying neither identity violates the write-side invariant and could not
118
+ * be resolved by the caller, so it fails loudly instead of yielding an anonymous
119
+ * hit.
120
+ */
121
+ private static _toIdentity;
122
+ /**
123
+ * Rebuild a fragment's locator from its persisted offsets, or `undefined` when the
124
+ * fragment was stored without one (both offsets `NULL`).
125
+ *
126
+ * The pair is written all-or-nothing, so a half-`NULL` pair can only come from
127
+ * corrupt / externally-edited data. Throw rather than coerce — `Number(null)` is
128
+ * `0`, which would silently fabricate a span starting at the top of the body.
129
+ */
130
+ private static _toLocator;
74
131
  /** Pack a `Float32Array` as the little-endian byte blob `vec0` stores. Copies, so the caller may reuse its buffer. */
75
132
  private static _toBlob;
76
133
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sqliteVecFragmentIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/sqliteVecFragmentIndex.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAgC,MAAM,eAAe,CAAC;AACrE,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EAIhB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,mCAAmC,EAAE,MAAM,SAAS,CAAC;AAsB9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,sBAAuB,YAAW,oBAAoB;IACjE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,yFAAyF;IACzF,OAAO,CAAC,UAAU,CAAqB;IACvC,qFAAqF;IACrF,OAAO,CAAC,MAAM,CAAkC;IAEhD,OAAO;IAOP,yGAAyG;IACzG,IAAW,WAAW,IAAI,MAAM,CAO/B;IAED,kGAAkG;IAClG,IAAW,aAAa,IAAI,MAAM,CAKjC;IAED;;;;;;;;;OASG;WACW,MAAM,CAAC,MAAM,EAAE,mCAAmC,GAAG,OAAO,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAmB1G,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAyD1B,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAahE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAyDlD,+EAA+E;IAC/E,OAAO,CAAC,YAAY;IAQpB,4EAA4E;IAC5E,OAAO,CAAC,QAAQ;IAoChB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAWrC,sHAAsH;IACtH,OAAO,CAAC,MAAM,CAAC,OAAO;IAItB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAWxB;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;CASzB"}
1
+ {"version":3,"file":"sqliteVecFragmentIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/sqliteVecFragmentIndex.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAgC,MAAM,eAAe,CAAC;AACrE,OAAO,EACL,WAAW,EACX,iBAAiB,EAEjB,oBAAoB,EACpB,eAAe,EAIhB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,mCAAmC,EAAE,MAAM,SAAS,CAAC;AAmD9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,sBAAuB,YAAW,oBAAoB;IACjE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,yFAAyF;IACzF,OAAO,CAAC,UAAU,CAAqB;IACvC,qFAAqF;IACrF,OAAO,CAAC,MAAM,CAAkC;IAEhD,OAAO;IAOP,yGAAyG;IACzG,IAAW,WAAW,IAAI,MAAM,CAO/B;IAED,kGAAkG;IAClG,IAAW,aAAa,IAAI,MAAM,CAKjC;IAED;;;;;;;;;;;;OAYG;WACW,MAAM,CAAC,MAAM,EAAE,mCAAmC,GAAG,OAAO,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAmB1G,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAuE1B,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAahE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAsDlD;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAQpB,4EAA4E;IAC5E,OAAO,CAAC,QAAQ;IA0ChB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAsBrC;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAiBtC;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAa1B;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAiBzB,sHAAsH;IACtH,OAAO,CAAC,MAAM,CAAC,OAAO;IAItB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAWxB;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;CASzB"}
@@ -12,6 +12,19 @@ const ts_agent_memory_1 = require("@fgv/ts-agent-memory");
12
12
  const DEFAULT_TABLE_NAME = 'memory_fragments';
13
13
  /** A simple SQL identifier — the only shape allowed for the table name (it is interpolated into DDL). */
14
14
  const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15
+ /**
16
+ * The auxiliary (`+`-prefixed) columns this version of the index writes. A table
17
+ * created by an earlier version carries a different set; see
18
+ * {@link SqliteVecFragmentIndex._readExistingDimension} for why that has to be
19
+ * detected explicitly rather than migrated.
20
+ */
21
+ const AUXILIARY_COLUMNS = ['start_off', 'end_off', 'fragment_id'];
22
+ /**
23
+ * Matches one `+name` auxiliary-column declaration in a `vec0` `CREATE VIRTUAL TABLE`
24
+ * statement. Only ever consumed via `String.matchAll`, which iterates a clone rather
25
+ * than advancing this instance's `lastIndex`, so the shared `/g` regex is reusable.
26
+ */
27
+ const AUXILIARY_COLUMN_RE = /\+\s*([A-Za-z_][A-Za-z0-9_]*)/g;
15
28
  /**
16
29
  * A persistent, `sqlite-vec`-backed `IFragmentVectorIndex` (from
17
30
  * `@fgv/ts-agent-memory`) — the fragment-granular sibling of
@@ -21,12 +34,25 @@ const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
21
34
  * @remarks
22
35
  * Where {@link SqliteVecVectorIndex} keys one vector per record on a
23
36
  * `target_key` primary key, this index holds **many** vectors per record — one per
24
- * in-record `[start, end)` span — so it keys the `vec0` table on `target_key` as a
25
- * **`PARTITION KEY`** (many rows may share it) and stores each fragment's locator
26
- * offsets in two auxiliary columns (`+start_off`, `+end_off`) that ride alongside
27
- * the vector and are returned on query but never filtered. A query is a brute-force
28
- * `vec0` KNN scan across all partitions returning per-fragment hits, each carrying
29
- * its record `target` and the matched `locator`.
37
+ * fragment — so it keys the `vec0` table on `target_key` as a **`PARTITION KEY`**
38
+ * (many rows may share it) and stores each fragment's identity in three auxiliary
39
+ * columns (`+start_off`, `+end_off`, `+fragment_id`) that ride alongside the vector
40
+ * and are returned on query but never filtered in particular `fragment_id` is
41
+ * stored and returned verbatim, never parsed and never part of the query path. A
42
+ * query is a brute-force `vec0` KNN scan across all partitions returning per-fragment
43
+ * hits, each carrying its record `target` plus whichever identity fields the stored
44
+ * fragment was added with (a fragment must carry at least one).
45
+ *
46
+ * **`vec0` schema changes require a drop-and-re-index.** A
47
+ * `CREATE VIRTUAL TABLE IF NOT EXISTS` is a no-op against an existing table (SQLite
48
+ * does not compare schemas) and `vec0` has no `ALTER TABLE ADD COLUMN`, so a database written by an
49
+ * earlier version of this package keeps its old auxiliary columns. `create` detects
50
+ * that by parsing the stored `CREATE VIRTUAL TABLE` SQL and fails with an actionable
51
+ * message naming the expected and found columns, rather than letting a widened
52
+ * `INSERT` surface an opaque `no such column` at statement-prepare time. There are no
53
+ * in-place migrations: drop the table (or use a fresh `tableName`) and re-index.
54
+ * Fragment vectors are re-derivable from the records, so this costs embedding time,
55
+ * never data.
30
56
  *
31
57
  * Semantics match `InMemoryFragmentCosineIndex` exactly: `addFragments` is
32
58
  * whole-record-replace (a single transaction deletes every prior fragment of the
@@ -69,12 +95,15 @@ class SqliteVecFragmentIndex {
69
95
  /**
70
96
  * Family-convention factory. Loads the `sqlite-vec` extension onto the supplied
71
97
  * `better-sqlite3` connection and, if the fragment table already exists (a
72
- * reopened persistent file), recovers its established dimension so no
73
- * re-embedding is needed on open.
98
+ * reopened persistent file), verifies its auxiliary-column set matches this
99
+ * version's and recovers its established dimension so no re-embedding is needed on
100
+ * open.
74
101
  *
75
102
  * @param params - See {@link ISqliteVecFragmentIndexCreateParams}.
76
103
  * @returns `Success` with the index, or `Failure` if the table name is not a
77
- * simple identifier or the extension fails to load.
104
+ * simple identifier, the extension fails to load, or the existing table was
105
+ * written by a version with a different auxiliary-column set (which requires a
106
+ * drop-and-re-index — `vec0` cannot be altered in place).
78
107
  */
79
108
  static create(params) {
80
109
  var _a;
@@ -101,11 +130,19 @@ class SqliteVecFragmentIndex {
101
130
  if (fragment.vector.length === 0) {
102
131
  return Promise.resolve((0, ts_utils_1.fail)(`fragment index: cannot add '${key}': empty fragment vector`));
103
132
  }
133
+ // A fragment carrying neither identity cannot be resolved back to anything by a
134
+ // consumer holding the hit — the same invariant `embeddedFragmentConverter`
135
+ // enforces at the untyped boundary, re-checked here at the index seam.
136
+ if (fragment.locator === undefined && fragment.fragmentId === undefined) {
137
+ return Promise.resolve((0, ts_utils_1.fail)(`fragment index: cannot add '${key}': fragment requires at least one of 'locator' or 'fragmentId'`));
138
+ }
104
139
  // Locator offsets are persisted as SQLite integers (bound via BigInt). Reject a
105
140
  // non-safe-integer offset up front with a clear message, rather than letting
106
141
  // `BigInt(nonInteger)` throw cryptically inside the write transaction OR storing
107
142
  // a value the read-side `_toOffset` guard would later reject on every query.
108
- if (!Number.isSafeInteger(fragment.locator.start) || !Number.isSafeInteger(fragment.locator.end)) {
143
+ // An absent locator persists as a NULL offset pair and skips the check.
144
+ if (fragment.locator !== undefined &&
145
+ (!Number.isSafeInteger(fragment.locator.start) || !Number.isSafeInteger(fragment.locator.end))) {
109
146
  return Promise.resolve((0, ts_utils_1.fail)(`fragment index: cannot add '${key}': locator [${fragment.locator.start}, ${fragment.locator.end}) offsets must be safe integers`));
110
147
  }
111
148
  if (dimension === undefined) {
@@ -183,36 +220,39 @@ class SqliteVecFragmentIndex {
183
220
  perRecord.set(row.target_key, used + 1);
184
221
  }
185
222
  const key = row.target_key;
186
- hits.push({
187
- target: SqliteVecFragmentIndex._parseKey(key),
188
- score: 1 - row.distance,
189
- locator: {
190
- start: SqliteVecFragmentIndex._toOffset(row.start_off, key),
191
- end: SqliteVecFragmentIndex._toOffset(row.end_off, key)
192
- }
193
- });
223
+ hits.push(Object.assign({ target: SqliteVecFragmentIndex._parseKey(key), score: 1 - row.distance }, SqliteVecFragmentIndex._toIdentity(row, key)));
194
224
  }
195
225
  return hits;
196
226
  }).withErrorFormat((e) => `fragment index: query failed: ${e}`));
197
227
  }
198
- /** Create the fragment `vec0` virtual table with the established dimension. */
228
+ /**
229
+ * Create the fragment `vec0` virtual table with the established dimension. The
230
+ * auxiliary columns must stay in sync with `AUXILIARY_COLUMNS`, which
231
+ * `create` compares against an existing table's stored DDL.
232
+ */
199
233
  _createTable(dimension) {
200
234
  this._db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS "${this._table}" USING vec0(` +
201
235
  `target_key TEXT PARTITION KEY, embedding float[${dimension}] distance_metric=cosine, ` +
202
- `+start_off integer, +end_off integer)`);
236
+ `+start_off integer, +end_off integer, +fragment_id text)`);
203
237
  }
204
238
  /** Prepare the statements the index reuses. Requires the table to exist. */
205
239
  _prepare() {
206
240
  const del = this._db.prepare(`DELETE FROM "${this._table}" WHERE target_key = ?`);
207
- const ins = this._db.prepare(`INSERT INTO "${this._table}"(target_key, embedding, start_off, end_off) VALUES (?, ?, ?, ?)`);
241
+ const ins = this._db.prepare(`INSERT INTO "${this._table}"(target_key, embedding, start_off, end_off, fragment_id) ` +
242
+ `VALUES (?, ?, ?, ?, ?)`);
208
243
  // Whole-record replace: drop every prior fragment of the target, then insert the
209
244
  // new set, atomically. An empty set collapses to a pure delete.
210
245
  const replaceTxn = this._db.transaction((key, fragments) => {
246
+ var _a;
211
247
  del.run(key);
212
248
  for (const fragment of fragments) {
213
249
  ins.run(key, SqliteVecFragmentIndex._toBlob(fragment.vector),
214
- // vec0 typed columns reject a JS float; bind the offsets as integers.
215
- BigInt(fragment.locator.start), BigInt(fragment.locator.end));
250
+ // vec0 typed columns reject a JS float; bind the offsets as integers. An
251
+ // absent locator binds the pair as NULL — never a partial pair, so the read
252
+ // side can treat a half-NULL pair as corruption rather than a legal shape.
253
+ fragment.locator === undefined ? null : BigInt(fragment.locator.start), fragment.locator === undefined ? null : BigInt(fragment.locator.end),
254
+ // Stored verbatim and never parsed; absent binds as NULL.
255
+ (_a = fragment.fragmentId) !== null && _a !== void 0 ? _a : null);
216
256
  }
217
257
  });
218
258
  return {
@@ -220,15 +260,23 @@ class SqliteVecFragmentIndex {
220
260
  replace: (key, fragments) => {
221
261
  replaceTxn(key, fragments);
222
262
  },
223
- query: this._db.prepare(`SELECT target_key, start_off, end_off, distance FROM "${this._table}" WHERE embedding MATCH ? AND k = ?`),
263
+ query: this._db.prepare(`SELECT target_key, start_off, end_off, fragment_id, distance FROM "${this._table}" ` +
264
+ `WHERE embedding MATCH ? AND k = ?`),
224
265
  fragmentCount: this._db.prepare(`SELECT count(*) AS c FROM "${this._table}"`),
225
266
  recordCount: this._db.prepare(`SELECT count(DISTINCT target_key) AS c FROM "${this._table}"`)
226
267
  };
227
268
  }
228
269
  /**
229
270
  * Recover the established dimension of an existing fragment `vec0` table from its
230
- * stored `CREATE VIRTUAL TABLE` SQL (`float[<n>]`). Returns `undefined` when the
271
+ * stored `CREATE VIRTUAL TABLE` SQL (`float[<n>]`), after checking that the table's
272
+ * auxiliary columns match `AUXILIARY_COLUMNS`. Returns `undefined` when the
231
273
  * table does not exist yet (a fresh database — dimension is set by the first add).
274
+ *
275
+ * Throws when a table of that name exists but is not a usable fragment index (a
276
+ * mismatched auxiliary-column set, or no `vec0` embedding column); the caller runs
277
+ * this inside `captureResult`, so it surfaces as a loud `Failure` from `create`.
278
+ * The same stored DDL answers every one of those questions, so the checks cost
279
+ * nothing extra.
232
280
  */
233
281
  static _readExistingDimension(db, table) {
234
282
  const row = db
@@ -237,8 +285,78 @@ class SqliteVecFragmentIndex {
237
285
  if (row === undefined) {
238
286
  return undefined;
239
287
  }
288
+ SqliteVecFragmentIndex._verifyAuxiliaryColumns(row.sql, table);
240
289
  const match = row.sql.match(/float\[(\d+)\]/);
241
- return match === null ? undefined : Number(match[1]);
290
+ if (match === null) {
291
+ // The auxiliary columns matched but there is no `float[<n>]` embedding column,
292
+ // so this is not a usable fragment index table. Same remedy as a column
293
+ // mismatch — and failing here beats handing back a dimensionless index whose
294
+ // first add would `CREATE VIRTUAL TABLE IF NOT EXISTS` into a no-op.
295
+ throw new Error(`existing table '${table}' has no vec0 embedding column, so it is not a usable fragment ` +
296
+ `index table. Drop it (or pass a fresh tableName) and re-add every fragment.`);
297
+ }
298
+ return Number(match[1]);
299
+ }
300
+ /**
301
+ * Compare an existing table's auxiliary columns against `AUXILIARY_COLUMNS`.
302
+ *
303
+ * `CREATE VIRTUAL TABLE IF NOT EXISTS` is a no-op against an existing table (SQLite
304
+ * never compares schemas) and `vec0` has no `ALTER TABLE ADD COLUMN`, so a table
305
+ * written by an earlier version of this package silently keeps its old columns and
306
+ * only fails later — as an opaque `no such column` when the widened `INSERT` is
307
+ * prepared. Detect it here instead and say what to do about it. Order is not
308
+ * compared: every statement names its columns explicitly, so only the set matters.
309
+ */
310
+ static _verifyAuxiliaryColumns(sql, table) {
311
+ const found = Array.from(sql.matchAll(AUXILIARY_COLUMN_RE), (m) => m[1]);
312
+ const expected = AUXILIARY_COLUMNS;
313
+ const matches = found.length === expected.length && expected.every((column) => found.includes(column));
314
+ if (!matches) {
315
+ throw new Error(`existing table '${table}' has auxiliary columns [${found.join(', ')}] but this index ` +
316
+ `requires [${expected.join(', ')}] — it was written by a different version of ` +
317
+ `@fgv/ts-agent-memory-sqlite-vec, or it is not a fragment index table at all. vec0 virtual ` +
318
+ `tables cannot be altered in place, so this requires a drop-and-re-index: DROP TABLE ` +
319
+ `"${table}" (or pass a fresh tableName) and re-add every fragment. Fragment vectors are ` +
320
+ `re-derivable from the records, so this costs embedding time, never data.`);
321
+ }
322
+ }
323
+ /**
324
+ * Rebuild the identity fields of a hit from a persisted row, omitting each field
325
+ * the stored fragment did not carry (so a hit is structurally identical to one this
326
+ * index produced before `fragment_id` existed).
327
+ *
328
+ * A row carrying neither identity violates the write-side invariant and could not
329
+ * be resolved by the caller, so it fails loudly instead of yielding an anonymous
330
+ * hit.
331
+ */
332
+ static _toIdentity(row, key) {
333
+ const locator = SqliteVecFragmentIndex._toLocator(row, key);
334
+ if (locator === undefined && row.fragment_id === null) {
335
+ throw new Error(`fragment '${key}': row carries neither a locator nor a fragment id (corrupt persisted data)`);
336
+ }
337
+ return Object.assign(Object.assign({}, (locator !== undefined ? { locator } : {})), (row.fragment_id !== null ? { fragmentId: row.fragment_id } : {}));
338
+ }
339
+ /**
340
+ * Rebuild a fragment's locator from its persisted offsets, or `undefined` when the
341
+ * fragment was stored without one (both offsets `NULL`).
342
+ *
343
+ * The pair is written all-or-nothing, so a half-`NULL` pair can only come from
344
+ * corrupt / externally-edited data. Throw rather than coerce — `Number(null)` is
345
+ * `0`, which would silently fabricate a span starting at the top of the body.
346
+ */
347
+ static _toLocator(row, key) {
348
+ const start = row.start_off;
349
+ const end = row.end_off;
350
+ if (start === null && end === null) {
351
+ return undefined;
352
+ }
353
+ if (start === null || end === null) {
354
+ throw new Error(`fragment '${key}': locator has only one of its start/end offsets (corrupt persisted data)`);
355
+ }
356
+ return {
357
+ start: SqliteVecFragmentIndex._toOffset(start, key),
358
+ end: SqliteVecFragmentIndex._toOffset(end, key)
359
+ };
242
360
  }
243
361
  /** Pack a `Float32Array` as the little-endian byte blob `vec0` stores. Copies, so the caller may reuse its buffer. */
244
362
  static _toBlob(vector) {
@@ -1 +1 @@
1
- {"version":3,"file":"sqliteVecFragmentIndex.js","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/sqliteVecFragmentIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport type BetterSqlite3 from 'better-sqlite3';\nimport { load as loadSqliteVec } from 'sqlite-vec';\nimport { Result, captureResult, fail, succeed } from '@fgv/ts-utils';\nimport {\n IEdgeTarget,\n IEmbeddedFragment,\n IFragmentVectorIndex,\n IVectorQueryHit,\n MemoryId,\n MemoryScopeKey,\n edgeTargetKey\n} from '@fgv/ts-agent-memory';\nimport { ISqliteVecFragmentIndexCreateParams } from './model';\n\n/** Default name for the fragment `vec0` virtual table. */\nconst DEFAULT_TABLE_NAME: string = 'memory_fragments';\n\n/** A simple SQL identifier — the only shape allowed for the table name (it is interpolated into DDL). */\nconst IDENTIFIER_RE: RegExp = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * One KNN row as returned by the fragment `vec0` MATCH query. The offset columns are\n * typed `number | bigint` because `better-sqlite3` returns integer columns as\n * `bigint` when a consumer enables its safe-integer mode (`defaultSafeIntegers`);\n * {@link SqliteVecFragmentIndex._toOffset} coerces them to a plain `number` (and\n * fails loudly on an out-of-safe-range value) before they reach the public locator.\n */\ninterface IKnnRow {\n readonly target_key: string;\n readonly start_off: number | bigint;\n readonly end_off: number | bigint;\n readonly distance: number;\n}\n\n/**\n * A persistent, `sqlite-vec`-backed `IFragmentVectorIndex` (from\n * `@fgv/ts-agent-memory`) — the fragment-granular sibling of\n * {@link SqliteVecVectorIndex}, and the **durable** counterpart to the in-memory\n * `InMemoryFragmentCosineIndex`.\n *\n * @remarks\n * Where {@link SqliteVecVectorIndex} keys one vector per record on a\n * `target_key` primary key, this index holds **many** vectors per record — one per\n * in-record `[start, end)` span — so it keys the `vec0` table on `target_key` as a\n * **`PARTITION KEY`** (many rows may share it) and stores each fragment's locator\n * offsets in two auxiliary columns (`+start_off`, `+end_off`) that ride alongside\n * the vector and are returned on query but never filtered. A query is a brute-force\n * `vec0` KNN scan across all partitions returning per-fragment hits, each carrying\n * its record `target` and the matched `locator`.\n *\n * Semantics match `InMemoryFragmentCosineIndex` exactly: `addFragments` is\n * whole-record-replace (a single transaction deletes every prior fragment of the\n * target, then inserts the new set), `remove` drops every fragment of a target,\n * and `query` applies the optional `maxPerRecord` cap **during selection, before\n * the topK cut** — so one long document cannot crowd others out. The dimension is\n * established by the first `addFragments` (the `vec0` column is fixed-width) and\n * recovered from the table schema when a persistent file is reopened; similarity is\n * cosine (`score = 1 - cosineDistance`), byte-identical to the in-memory index.\n * Large-N ANN indexing is explicitly out of scope, same regime as the record index.\n *\n * The `better-sqlite3` `Database` is consumer-owned (bring-your-own): this index\n * loads the `sqlite-vec` extension onto it and reads/writes the table, but never\n * opens or closes the connection.\n * @public\n */\nexport class SqliteVecFragmentIndex implements IFragmentVectorIndex {\n private readonly _db: BetterSqlite3.Database;\n private readonly _table: string;\n /** The dimension of every stored fragment vector; `undefined` until the table exists. */\n private _dimension: number | undefined;\n /** Prepared statements; created once the table exists (established or recovered). */\n private _stmts: IFragmentStatements | undefined;\n\n private constructor(db: BetterSqlite3.Database, table: string, dimension: number | undefined) {\n this._db = db;\n this._table = table;\n this._dimension = dimension;\n this._stmts = dimension === undefined ? undefined : this._prepare();\n }\n\n /** The number of records that currently have at least one stored fragment. Zero before the first add. */\n public get recordCount(): number {\n if (this._stmts === undefined) {\n return 0;\n }\n // `Number(...)` narrows the count in case the consumer enabled better-sqlite3\n // safe-integer mode (which returns `count(*)` as a `bigint`).\n return Number((this._stmts.recordCount.get() as { c: number | bigint }).c);\n }\n\n /** The total number of fragments currently held across all records. Zero before the first add. */\n public get fragmentCount(): number {\n if (this._stmts === undefined) {\n return 0;\n }\n return Number((this._stmts.fragmentCount.get() as { c: number | bigint }).c);\n }\n\n /**\n * Family-convention factory. Loads the `sqlite-vec` extension onto the supplied\n * `better-sqlite3` connection and, if the fragment table already exists (a\n * reopened persistent file), recovers its established dimension so no\n * re-embedding is needed on open.\n *\n * @param params - See {@link ISqliteVecFragmentIndexCreateParams}.\n * @returns `Success` with the index, or `Failure` if the table name is not a\n * simple identifier or the extension fails to load.\n */\n public static create(params: ISqliteVecFragmentIndexCreateParams): Promise<Result<SqliteVecFragmentIndex>> {\n const table: string = params.tableName ?? DEFAULT_TABLE_NAME;\n if (!IDENTIFIER_RE.test(table)) {\n return Promise.resolve(\n fail(`sqlite-vec fragment index: table name '${table}' is not a simple SQL identifier`)\n );\n }\n return Promise.resolve(\n captureResult(() => {\n loadSqliteVec(params.database);\n const dimension: number | undefined = SqliteVecFragmentIndex._readExistingDimension(\n params.database,\n table\n );\n return new SqliteVecFragmentIndex(params.database, table, dimension);\n }).withErrorFormat((e) => `sqlite-vec fragment index: failed to initialize: ${e}`)\n );\n }\n\n /** {@inheritDoc IFragmentVectorIndex.addFragments} */\n public addFragments(\n target: IEdgeTarget,\n fragments: ReadonlyArray<IEmbeddedFragment>\n ): Promise<Result<number>> {\n const key: string = edgeTargetKey(target);\n // Validate every fragment before touching the database, so a bad fragment never\n // leaves the record half-replaced or the dimension half-established (whole-record\n // replace is all-or-nothing). The effective dimension is the established one, or —\n // on a still-dimensionless index — the first fragment's length; it is committed\n // (via table creation) only once the whole batch validates.\n let dimension: number | undefined = this._dimension;\n for (const fragment of fragments) {\n if (fragment.vector.length === 0) {\n return Promise.resolve(fail(`fragment index: cannot add '${key}': empty fragment vector`));\n }\n // Locator offsets are persisted as SQLite integers (bound via BigInt). Reject a\n // non-safe-integer offset up front with a clear message, rather than letting\n // `BigInt(nonInteger)` throw cryptically inside the write transaction OR storing\n // a value the read-side `_toOffset` guard would later reject on every query.\n if (!Number.isSafeInteger(fragment.locator.start) || !Number.isSafeInteger(fragment.locator.end)) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': locator [${fragment.locator.start}, ${fragment.locator.end}) offsets must be safe integers`\n )\n );\n }\n if (dimension === undefined) {\n dimension = fragment.vector.length;\n } else if (fragment.vector.length !== dimension) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment dimension ${fragment.vector.length} does not match index dimension ${dimension}`\n )\n );\n }\n }\n return Promise.resolve(\n captureResult(() => {\n // A same-target re-author (or an empty batch) still needs the table to exist\n // to delete prior fragments; create it lazily on the first non-empty add.\n if (this._stmts === undefined) {\n if (fragments.length === 0) {\n // Nothing stored yet and nothing to store: no table, no work.\n return 0;\n }\n // `fragments` is non-empty here (the empty case returned above), so the\n // validation loop proved every fragment shares `fragments[0]`'s length —\n // which IS the dimension to establish. Read it straight from the first\n // fragment: no cast, no invariant-dependent narrowing.\n const established: number = fragments[0].vector.length;\n this._createTable(established);\n this._dimension = established;\n this._stmts = this._prepare();\n }\n this._stmts.replace(key, fragments);\n return fragments.length;\n }).withErrorFormat((e) => `fragment index: cannot add '${key}': ${e}`)\n );\n }\n\n /** {@inheritDoc IFragmentVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n return Promise.resolve(\n captureResult(() => {\n // Idempotent: removing a target with no fragments (or before any add created\n // the table) still succeeds.\n if (this._stmts !== undefined) {\n this._stmts.deleteByTarget.run(edgeTargetKey(target));\n }\n return target;\n }).withErrorFormat((e) => `fragment index: cannot remove '${edgeTargetKey(target)}': ${e}`)\n );\n }\n\n /** {@inheritDoc IFragmentVectorIndex.query} */\n public query(\n vector: Float32Array,\n topK: number,\n maxPerRecord?: number\n ): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._stmts === undefined) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `fragment index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const stmts: IFragmentStatements = this._stmts;\n return Promise.resolve(\n captureResult<ReadonlyArray<IVectorQueryHit>>(() => {\n // With a per-record cap the topK winners may lie past the first topK rows (a\n // capped record's later fragments are skipped), so fetch the full ranked set\n // and apply the cap + topK cut here — exactly as the in-memory index does.\n // Uncapped, KNN's own `k = topK` is already the answer.\n const fetchK: number =\n maxPerRecord === undefined ? topK : Number((stmts.fragmentCount.get() as { c: number | bigint }).c);\n if (fetchK <= 0) {\n return [];\n }\n const rows: ReadonlyArray<IKnnRow> = stmts.query.all(\n SqliteVecFragmentIndex._toBlob(vector),\n fetchK\n ) as ReadonlyArray<IKnnRow>;\n // sqlite-vec returns rows ascending by distance (nearest first); score is\n // `1 - cosineDistance`, so this order is already descending score.\n const hits: IVectorQueryHit[] = [];\n const perRecord: Map<string, number> = new Map<string, number>();\n for (const row of rows) {\n if (hits.length >= topK) {\n break;\n }\n if (maxPerRecord !== undefined) {\n const used: number = perRecord.get(row.target_key) ?? 0;\n if (used >= maxPerRecord) {\n continue;\n }\n perRecord.set(row.target_key, used + 1);\n }\n const key: string = row.target_key;\n hits.push({\n target: SqliteVecFragmentIndex._parseKey(key),\n score: 1 - row.distance,\n locator: {\n start: SqliteVecFragmentIndex._toOffset(row.start_off, key),\n end: SqliteVecFragmentIndex._toOffset(row.end_off, key)\n }\n });\n }\n return hits;\n }).withErrorFormat((e) => `fragment index: query failed: ${e}`)\n );\n }\n\n /** Create the fragment `vec0` virtual table with the established dimension. */\n private _createTable(dimension: number): void {\n this._db.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${this._table}\" USING vec0(` +\n `target_key TEXT PARTITION KEY, embedding float[${dimension}] distance_metric=cosine, ` +\n `+start_off integer, +end_off integer)`\n );\n }\n\n /** Prepare the statements the index reuses. Requires the table to exist. */\n private _prepare(): IFragmentStatements {\n const del: BetterSqlite3.Statement = this._db.prepare(\n `DELETE FROM \"${this._table}\" WHERE target_key = ?`\n );\n const ins: BetterSqlite3.Statement = this._db.prepare(\n `INSERT INTO \"${this._table}\"(target_key, embedding, start_off, end_off) VALUES (?, ?, ?, ?)`\n );\n // Whole-record replace: drop every prior fragment of the target, then insert the\n // new set, atomically. An empty set collapses to a pure delete.\n const replaceTxn: BetterSqlite3.Transaction<\n (key: string, fragments: ReadonlyArray<IEmbeddedFragment>) => void\n > = this._db.transaction((key: string, fragments: ReadonlyArray<IEmbeddedFragment>) => {\n del.run(key);\n for (const fragment of fragments) {\n ins.run(\n key,\n SqliteVecFragmentIndex._toBlob(fragment.vector),\n // vec0 typed columns reject a JS float; bind the offsets as integers.\n BigInt(fragment.locator.start),\n BigInt(fragment.locator.end)\n );\n }\n });\n return {\n deleteByTarget: del,\n replace: (key: string, fragments: ReadonlyArray<IEmbeddedFragment>): void => {\n replaceTxn(key, fragments);\n },\n query: this._db.prepare(\n `SELECT target_key, start_off, end_off, distance FROM \"${this._table}\" WHERE embedding MATCH ? AND k = ?`\n ),\n fragmentCount: this._db.prepare(`SELECT count(*) AS c FROM \"${this._table}\"`),\n recordCount: this._db.prepare(`SELECT count(DISTINCT target_key) AS c FROM \"${this._table}\"`)\n };\n }\n\n /**\n * Recover the established dimension of an existing fragment `vec0` table from its\n * stored `CREATE VIRTUAL TABLE` SQL (`float[<n>]`). Returns `undefined` when the\n * table does not exist yet (a fresh database — dimension is set by the first add).\n */\n private static _readExistingDimension(db: BetterSqlite3.Database, table: string): number | undefined {\n const row: { sql: string } | undefined = db\n .prepare(\"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?\")\n .get(table) as { sql: string } | undefined;\n if (row === undefined) {\n return undefined;\n }\n const match: RegExpMatchArray | null = row.sql.match(/float\\[(\\d+)\\]/);\n return match === null ? undefined : Number(match[1]);\n }\n\n /** Pack a `Float32Array` as the little-endian byte blob `vec0` stores. Copies, so the caller may reuse its buffer. */\n private static _toBlob(vector: Float32Array): Uint8Array {\n return new Uint8Array(Float32Array.from(vector).buffer);\n }\n\n /**\n * Reverse `edgeTargetKey` — the canonical key is `scope\\0id` with NUL excluded\n * from both components, so the first NUL splits it unambiguously. A key with no\n * NUL cannot have been written by `edgeTargetKey`; rather than fabricate a wrong\n * `(scope, id)` from corrupt / externally-edited table data, throw so the query\n * surfaces it as a loud `Failure`.\n */\n private static _parseKey(key: string): IEdgeTarget {\n const nul: number = key.indexOf('\\0');\n if (nul < 0) {\n throw new Error(`malformed target key '${key}': missing scope/id separator (corrupt persisted data)`);\n }\n return {\n scope: key.slice(0, nul) as unknown as MemoryScopeKey,\n id: key.slice(nul + 1) as unknown as MemoryId\n };\n }\n\n /**\n * Coerce a persisted locator offset to a plain `number`. `better-sqlite3` returns\n * integer columns as `bigint` under safe-integer mode, so an offset can arrive as\n * either; both narrow to `number` here. A value outside the safe-integer range\n * (only reachable via corrupt / externally-edited data — the index only ever\n * writes in-document offsets) throws rather than silently losing precision, so the\n * query surfaces it as a loud `Failure`.\n */\n private static _toOffset(value: number | bigint, key: string): number {\n const n: number = Number(value);\n if (!Number.isSafeInteger(n)) {\n throw new Error(\n `fragment '${key}': locator offset ${String(value)} is not a safe integer (corrupt persisted data)`\n );\n }\n return n;\n }\n}\n\n/** The prepared statements / helpers the fragment index reuses once its table exists. */\ninterface IFragmentStatements {\n readonly deleteByTarget: BetterSqlite3.Statement;\n readonly replace: (key: string, fragments: ReadonlyArray<IEmbeddedFragment>) => void;\n readonly query: BetterSqlite3.Statement;\n readonly fragmentCount: BetterSqlite3.Statement;\n readonly recordCount: BetterSqlite3.Statement;\n}\n"]}
1
+ {"version":3,"file":"sqliteVecFragmentIndex.js","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/sqliteVecFragmentIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport type BetterSqlite3 from 'better-sqlite3';\nimport { load as loadSqliteVec } from 'sqlite-vec';\nimport { Result, captureResult, fail, succeed } from '@fgv/ts-utils';\nimport {\n IEdgeTarget,\n IEmbeddedFragment,\n IFragmentLocator,\n IFragmentVectorIndex,\n IVectorQueryHit,\n MemoryId,\n MemoryScopeKey,\n edgeTargetKey\n} from '@fgv/ts-agent-memory';\nimport { ISqliteVecFragmentIndexCreateParams } from './model';\n\n/** Default name for the fragment `vec0` virtual table. */\nconst DEFAULT_TABLE_NAME: string = 'memory_fragments';\n\n/** A simple SQL identifier — the only shape allowed for the table name (it is interpolated into DDL). */\nconst IDENTIFIER_RE: RegExp = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * The auxiliary (`+`-prefixed) columns this version of the index writes. A table\n * created by an earlier version carries a different set; see\n * {@link SqliteVecFragmentIndex._readExistingDimension} for why that has to be\n * detected explicitly rather than migrated.\n */\nconst AUXILIARY_COLUMNS: ReadonlyArray<string> = ['start_off', 'end_off', 'fragment_id'];\n\n/**\n * Matches one `+name` auxiliary-column declaration in a `vec0` `CREATE VIRTUAL TABLE`\n * statement. Only ever consumed via `String.matchAll`, which iterates a clone rather\n * than advancing this instance's `lastIndex`, so the shared `/g` regex is reusable.\n */\nconst AUXILIARY_COLUMN_RE: RegExp = /\\+\\s*([A-Za-z_][A-Za-z0-9_]*)/g;\n\n/**\n * One KNN row as returned by the fragment `vec0` MATCH query. The offset columns are\n * typed `number | bigint` because `better-sqlite3` returns integer columns as\n * `bigint` when a consumer enables its safe-integer mode (`defaultSafeIntegers`);\n * {@link SqliteVecFragmentIndex._toOffset} coerces them to a plain `number` (and\n * fails loudly on an out-of-safe-range value) before they reach the public locator.\n * All three identity columns are nullable: a fragment stored without a locator has\n * `NULL` offsets, and one stored without a `fragmentId` has a `NULL` `fragment_id`.\n */\ninterface IKnnRow {\n readonly target_key: string;\n // eslint-disable-next-line @rushstack/no-new-null -- SQLite returns NULL (not undefined) for an absent locator offset\n readonly start_off: number | bigint | null;\n // eslint-disable-next-line @rushstack/no-new-null -- SQLite returns NULL (not undefined) for an absent locator offset\n readonly end_off: number | bigint | null;\n // eslint-disable-next-line @rushstack/no-new-null -- SQLite returns NULL (not undefined) for an absent fragment id\n readonly fragment_id: string | null;\n readonly distance: number;\n}\n\n/**\n * The identity fields of a fragment hit, in `IVectorQueryHit` shape: a field the\n * stored fragment did not carry is *absent*, never present-but-`undefined`, so a hit\n * for a fragment stored without a `fragmentId` is structurally identical to one this\n * index produced before `fragment_id` existed.\n */\ntype FragmentIdentity = Pick<IVectorQueryHit, 'locator' | 'fragmentId'>;\n\n/**\n * A persistent, `sqlite-vec`-backed `IFragmentVectorIndex` (from\n * `@fgv/ts-agent-memory`) — the fragment-granular sibling of\n * {@link SqliteVecVectorIndex}, and the **durable** counterpart to the in-memory\n * `InMemoryFragmentCosineIndex`.\n *\n * @remarks\n * Where {@link SqliteVecVectorIndex} keys one vector per record on a\n * `target_key` primary key, this index holds **many** vectors per record — one per\n * fragment — so it keys the `vec0` table on `target_key` as a **`PARTITION KEY`**\n * (many rows may share it) and stores each fragment's identity in three auxiliary\n * columns (`+start_off`, `+end_off`, `+fragment_id`) that ride alongside the vector\n * and are returned on query but never filtered — in particular `fragment_id` is\n * stored and returned verbatim, never parsed and never part of the query path. A\n * query is a brute-force `vec0` KNN scan across all partitions returning per-fragment\n * hits, each carrying its record `target` plus whichever identity fields the stored\n * fragment was added with (a fragment must carry at least one).\n *\n * **`vec0` schema changes require a drop-and-re-index.** A\n * `CREATE VIRTUAL TABLE IF NOT EXISTS` is a no-op against an existing table (SQLite\n * does not compare schemas) and `vec0` has no `ALTER TABLE ADD COLUMN`, so a database written by an\n * earlier version of this package keeps its old auxiliary columns. `create` detects\n * that by parsing the stored `CREATE VIRTUAL TABLE` SQL and fails with an actionable\n * message naming the expected and found columns, rather than letting a widened\n * `INSERT` surface an opaque `no such column` at statement-prepare time. There are no\n * in-place migrations: drop the table (or use a fresh `tableName`) and re-index.\n * Fragment vectors are re-derivable from the records, so this costs embedding time,\n * never data.\n *\n * Semantics match `InMemoryFragmentCosineIndex` exactly: `addFragments` is\n * whole-record-replace (a single transaction deletes every prior fragment of the\n * target, then inserts the new set), `remove` drops every fragment of a target,\n * and `query` applies the optional `maxPerRecord` cap **during selection, before\n * the topK cut** — so one long document cannot crowd others out. The dimension is\n * established by the first `addFragments` (the `vec0` column is fixed-width) and\n * recovered from the table schema when a persistent file is reopened; similarity is\n * cosine (`score = 1 - cosineDistance`), byte-identical to the in-memory index.\n * Large-N ANN indexing is explicitly out of scope, same regime as the record index.\n *\n * The `better-sqlite3` `Database` is consumer-owned (bring-your-own): this index\n * loads the `sqlite-vec` extension onto it and reads/writes the table, but never\n * opens or closes the connection.\n * @public\n */\nexport class SqliteVecFragmentIndex implements IFragmentVectorIndex {\n private readonly _db: BetterSqlite3.Database;\n private readonly _table: string;\n /** The dimension of every stored fragment vector; `undefined` until the table exists. */\n private _dimension: number | undefined;\n /** Prepared statements; created once the table exists (established or recovered). */\n private _stmts: IFragmentStatements | undefined;\n\n private constructor(db: BetterSqlite3.Database, table: string, dimension: number | undefined) {\n this._db = db;\n this._table = table;\n this._dimension = dimension;\n this._stmts = dimension === undefined ? undefined : this._prepare();\n }\n\n /** The number of records that currently have at least one stored fragment. Zero before the first add. */\n public get recordCount(): number {\n if (this._stmts === undefined) {\n return 0;\n }\n // `Number(...)` narrows the count in case the consumer enabled better-sqlite3\n // safe-integer mode (which returns `count(*)` as a `bigint`).\n return Number((this._stmts.recordCount.get() as { c: number | bigint }).c);\n }\n\n /** The total number of fragments currently held across all records. Zero before the first add. */\n public get fragmentCount(): number {\n if (this._stmts === undefined) {\n return 0;\n }\n return Number((this._stmts.fragmentCount.get() as { c: number | bigint }).c);\n }\n\n /**\n * Family-convention factory. Loads the `sqlite-vec` extension onto the supplied\n * `better-sqlite3` connection and, if the fragment table already exists (a\n * reopened persistent file), verifies its auxiliary-column set matches this\n * version's and recovers its established dimension so no re-embedding is needed on\n * open.\n *\n * @param params - See {@link ISqliteVecFragmentIndexCreateParams}.\n * @returns `Success` with the index, or `Failure` if the table name is not a\n * simple identifier, the extension fails to load, or the existing table was\n * written by a version with a different auxiliary-column set (which requires a\n * drop-and-re-index — `vec0` cannot be altered in place).\n */\n public static create(params: ISqliteVecFragmentIndexCreateParams): Promise<Result<SqliteVecFragmentIndex>> {\n const table: string = params.tableName ?? DEFAULT_TABLE_NAME;\n if (!IDENTIFIER_RE.test(table)) {\n return Promise.resolve(\n fail(`sqlite-vec fragment index: table name '${table}' is not a simple SQL identifier`)\n );\n }\n return Promise.resolve(\n captureResult(() => {\n loadSqliteVec(params.database);\n const dimension: number | undefined = SqliteVecFragmentIndex._readExistingDimension(\n params.database,\n table\n );\n return new SqliteVecFragmentIndex(params.database, table, dimension);\n }).withErrorFormat((e) => `sqlite-vec fragment index: failed to initialize: ${e}`)\n );\n }\n\n /** {@inheritDoc IFragmentVectorIndex.addFragments} */\n public addFragments(\n target: IEdgeTarget,\n fragments: ReadonlyArray<IEmbeddedFragment>\n ): Promise<Result<number>> {\n const key: string = edgeTargetKey(target);\n // Validate every fragment before touching the database, so a bad fragment never\n // leaves the record half-replaced or the dimension half-established (whole-record\n // replace is all-or-nothing). The effective dimension is the established one, or —\n // on a still-dimensionless index — the first fragment's length; it is committed\n // (via table creation) only once the whole batch validates.\n let dimension: number | undefined = this._dimension;\n for (const fragment of fragments) {\n if (fragment.vector.length === 0) {\n return Promise.resolve(fail(`fragment index: cannot add '${key}': empty fragment vector`));\n }\n // A fragment carrying neither identity cannot be resolved back to anything by a\n // consumer holding the hit — the same invariant `embeddedFragmentConverter`\n // enforces at the untyped boundary, re-checked here at the index seam.\n if (fragment.locator === undefined && fragment.fragmentId === undefined) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment requires at least one of 'locator' or 'fragmentId'`\n )\n );\n }\n // Locator offsets are persisted as SQLite integers (bound via BigInt). Reject a\n // non-safe-integer offset up front with a clear message, rather than letting\n // `BigInt(nonInteger)` throw cryptically inside the write transaction OR storing\n // a value the read-side `_toOffset` guard would later reject on every query.\n // An absent locator persists as a NULL offset pair and skips the check.\n if (\n fragment.locator !== undefined &&\n (!Number.isSafeInteger(fragment.locator.start) || !Number.isSafeInteger(fragment.locator.end))\n ) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': locator [${fragment.locator.start}, ${fragment.locator.end}) offsets must be safe integers`\n )\n );\n }\n if (dimension === undefined) {\n dimension = fragment.vector.length;\n } else if (fragment.vector.length !== dimension) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment dimension ${fragment.vector.length} does not match index dimension ${dimension}`\n )\n );\n }\n }\n return Promise.resolve(\n captureResult(() => {\n // A same-target re-author (or an empty batch) still needs the table to exist\n // to delete prior fragments; create it lazily on the first non-empty add.\n if (this._stmts === undefined) {\n if (fragments.length === 0) {\n // Nothing stored yet and nothing to store: no table, no work.\n return 0;\n }\n // `fragments` is non-empty here (the empty case returned above), so the\n // validation loop proved every fragment shares `fragments[0]`'s length —\n // which IS the dimension to establish. Read it straight from the first\n // fragment: no cast, no invariant-dependent narrowing.\n const established: number = fragments[0].vector.length;\n this._createTable(established);\n this._dimension = established;\n this._stmts = this._prepare();\n }\n this._stmts.replace(key, fragments);\n return fragments.length;\n }).withErrorFormat((e) => `fragment index: cannot add '${key}': ${e}`)\n );\n }\n\n /** {@inheritDoc IFragmentVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n return Promise.resolve(\n captureResult(() => {\n // Idempotent: removing a target with no fragments (or before any add created\n // the table) still succeeds.\n if (this._stmts !== undefined) {\n this._stmts.deleteByTarget.run(edgeTargetKey(target));\n }\n return target;\n }).withErrorFormat((e) => `fragment index: cannot remove '${edgeTargetKey(target)}': ${e}`)\n );\n }\n\n /** {@inheritDoc IFragmentVectorIndex.query} */\n public query(\n vector: Float32Array,\n topK: number,\n maxPerRecord?: number\n ): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._stmts === undefined) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `fragment index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const stmts: IFragmentStatements = this._stmts;\n return Promise.resolve(\n captureResult<ReadonlyArray<IVectorQueryHit>>(() => {\n // With a per-record cap the topK winners may lie past the first topK rows (a\n // capped record's later fragments are skipped), so fetch the full ranked set\n // and apply the cap + topK cut here — exactly as the in-memory index does.\n // Uncapped, KNN's own `k = topK` is already the answer.\n const fetchK: number =\n maxPerRecord === undefined ? topK : Number((stmts.fragmentCount.get() as { c: number | bigint }).c);\n if (fetchK <= 0) {\n return [];\n }\n const rows: ReadonlyArray<IKnnRow> = stmts.query.all(\n SqliteVecFragmentIndex._toBlob(vector),\n fetchK\n ) as ReadonlyArray<IKnnRow>;\n // sqlite-vec returns rows ascending by distance (nearest first); score is\n // `1 - cosineDistance`, so this order is already descending score.\n const hits: IVectorQueryHit[] = [];\n const perRecord: Map<string, number> = new Map<string, number>();\n for (const row of rows) {\n if (hits.length >= topK) {\n break;\n }\n if (maxPerRecord !== undefined) {\n const used: number = perRecord.get(row.target_key) ?? 0;\n if (used >= maxPerRecord) {\n continue;\n }\n perRecord.set(row.target_key, used + 1);\n }\n const key: string = row.target_key;\n hits.push({\n target: SqliteVecFragmentIndex._parseKey(key),\n score: 1 - row.distance,\n ...SqliteVecFragmentIndex._toIdentity(row, key)\n });\n }\n return hits;\n }).withErrorFormat((e) => `fragment index: query failed: ${e}`)\n );\n }\n\n /**\n * Create the fragment `vec0` virtual table with the established dimension. The\n * auxiliary columns must stay in sync with `AUXILIARY_COLUMNS`, which\n * `create` compares against an existing table's stored DDL.\n */\n private _createTable(dimension: number): void {\n this._db.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${this._table}\" USING vec0(` +\n `target_key TEXT PARTITION KEY, embedding float[${dimension}] distance_metric=cosine, ` +\n `+start_off integer, +end_off integer, +fragment_id text)`\n );\n }\n\n /** Prepare the statements the index reuses. Requires the table to exist. */\n private _prepare(): IFragmentStatements {\n const del: BetterSqlite3.Statement = this._db.prepare(\n `DELETE FROM \"${this._table}\" WHERE target_key = ?`\n );\n const ins: BetterSqlite3.Statement = this._db.prepare(\n `INSERT INTO \"${this._table}\"(target_key, embedding, start_off, end_off, fragment_id) ` +\n `VALUES (?, ?, ?, ?, ?)`\n );\n // Whole-record replace: drop every prior fragment of the target, then insert the\n // new set, atomically. An empty set collapses to a pure delete.\n const replaceTxn: BetterSqlite3.Transaction<\n (key: string, fragments: ReadonlyArray<IEmbeddedFragment>) => void\n > = this._db.transaction((key: string, fragments: ReadonlyArray<IEmbeddedFragment>) => {\n del.run(key);\n for (const fragment of fragments) {\n ins.run(\n key,\n SqliteVecFragmentIndex._toBlob(fragment.vector),\n // vec0 typed columns reject a JS float; bind the offsets as integers. An\n // absent locator binds the pair as NULL — never a partial pair, so the read\n // side can treat a half-NULL pair as corruption rather than a legal shape.\n fragment.locator === undefined ? null : BigInt(fragment.locator.start),\n fragment.locator === undefined ? null : BigInt(fragment.locator.end),\n // Stored verbatim and never parsed; absent binds as NULL.\n fragment.fragmentId ?? null\n );\n }\n });\n return {\n deleteByTarget: del,\n replace: (key: string, fragments: ReadonlyArray<IEmbeddedFragment>): void => {\n replaceTxn(key, fragments);\n },\n query: this._db.prepare(\n `SELECT target_key, start_off, end_off, fragment_id, distance FROM \"${this._table}\" ` +\n `WHERE embedding MATCH ? AND k = ?`\n ),\n fragmentCount: this._db.prepare(`SELECT count(*) AS c FROM \"${this._table}\"`),\n recordCount: this._db.prepare(`SELECT count(DISTINCT target_key) AS c FROM \"${this._table}\"`)\n };\n }\n\n /**\n * Recover the established dimension of an existing fragment `vec0` table from its\n * stored `CREATE VIRTUAL TABLE` SQL (`float[<n>]`), after checking that the table's\n * auxiliary columns match `AUXILIARY_COLUMNS`. Returns `undefined` when the\n * table does not exist yet (a fresh database — dimension is set by the first add).\n *\n * Throws when a table of that name exists but is not a usable fragment index (a\n * mismatched auxiliary-column set, or no `vec0` embedding column); the caller runs\n * this inside `captureResult`, so it surfaces as a loud `Failure` from `create`.\n * The same stored DDL answers every one of those questions, so the checks cost\n * nothing extra.\n */\n private static _readExistingDimension(db: BetterSqlite3.Database, table: string): number | undefined {\n const row: { sql: string } | undefined = db\n .prepare(\"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?\")\n .get(table) as { sql: string } | undefined;\n if (row === undefined) {\n return undefined;\n }\n SqliteVecFragmentIndex._verifyAuxiliaryColumns(row.sql, table);\n const match: RegExpMatchArray | null = row.sql.match(/float\\[(\\d+)\\]/);\n if (match === null) {\n // The auxiliary columns matched but there is no `float[<n>]` embedding column,\n // so this is not a usable fragment index table. Same remedy as a column\n // mismatch — and failing here beats handing back a dimensionless index whose\n // first add would `CREATE VIRTUAL TABLE IF NOT EXISTS` into a no-op.\n throw new Error(\n `existing table '${table}' has no vec0 embedding column, so it is not a usable fragment ` +\n `index table. Drop it (or pass a fresh tableName) and re-add every fragment.`\n );\n }\n return Number(match[1]);\n }\n\n /**\n * Compare an existing table's auxiliary columns against `AUXILIARY_COLUMNS`.\n *\n * `CREATE VIRTUAL TABLE IF NOT EXISTS` is a no-op against an existing table (SQLite\n * never compares schemas) and `vec0` has no `ALTER TABLE ADD COLUMN`, so a table\n * written by an earlier version of this package silently keeps its old columns and\n * only fails later — as an opaque `no such column` when the widened `INSERT` is\n * prepared. Detect it here instead and say what to do about it. Order is not\n * compared: every statement names its columns explicitly, so only the set matters.\n */\n private static _verifyAuxiliaryColumns(sql: string, table: string): void {\n const found: string[] = Array.from(sql.matchAll(AUXILIARY_COLUMN_RE), (m) => m[1]);\n const expected: ReadonlyArray<string> = AUXILIARY_COLUMNS;\n const matches: boolean =\n found.length === expected.length && expected.every((column) => found.includes(column));\n if (!matches) {\n throw new Error(\n `existing table '${table}' has auxiliary columns [${found.join(', ')}] but this index ` +\n `requires [${expected.join(', ')}] — it was written by a different version of ` +\n `@fgv/ts-agent-memory-sqlite-vec, or it is not a fragment index table at all. vec0 virtual ` +\n `tables cannot be altered in place, so this requires a drop-and-re-index: DROP TABLE ` +\n `\"${table}\" (or pass a fresh tableName) and re-add every fragment. Fragment vectors are ` +\n `re-derivable from the records, so this costs embedding time, never data.`\n );\n }\n }\n\n /**\n * Rebuild the identity fields of a hit from a persisted row, omitting each field\n * the stored fragment did not carry (so a hit is structurally identical to one this\n * index produced before `fragment_id` existed).\n *\n * A row carrying neither identity violates the write-side invariant and could not\n * be resolved by the caller, so it fails loudly instead of yielding an anonymous\n * hit.\n */\n private static _toIdentity(row: IKnnRow, key: string): FragmentIdentity {\n const locator: IFragmentLocator | undefined = SqliteVecFragmentIndex._toLocator(row, key);\n if (locator === undefined && row.fragment_id === null) {\n throw new Error(\n `fragment '${key}': row carries neither a locator nor a fragment id (corrupt persisted data)`\n );\n }\n return {\n ...(locator !== undefined ? { locator } : {}),\n ...(row.fragment_id !== null ? { fragmentId: row.fragment_id } : {})\n };\n }\n\n /**\n * Rebuild a fragment's locator from its persisted offsets, or `undefined` when the\n * fragment was stored without one (both offsets `NULL`).\n *\n * The pair is written all-or-nothing, so a half-`NULL` pair can only come from\n * corrupt / externally-edited data. Throw rather than coerce — `Number(null)` is\n * `0`, which would silently fabricate a span starting at the top of the body.\n */\n private static _toLocator(row: IKnnRow, key: string): IFragmentLocator | undefined {\n const start: number | bigint | null = row.start_off;\n const end: number | bigint | null = row.end_off;\n if (start === null && end === null) {\n return undefined;\n }\n if (start === null || end === null) {\n throw new Error(\n `fragment '${key}': locator has only one of its start/end offsets (corrupt persisted data)`\n );\n }\n return {\n start: SqliteVecFragmentIndex._toOffset(start, key),\n end: SqliteVecFragmentIndex._toOffset(end, key)\n };\n }\n\n /** Pack a `Float32Array` as the little-endian byte blob `vec0` stores. Copies, so the caller may reuse its buffer. */\n private static _toBlob(vector: Float32Array): Uint8Array {\n return new Uint8Array(Float32Array.from(vector).buffer);\n }\n\n /**\n * Reverse `edgeTargetKey` — the canonical key is `scope\\0id` with NUL excluded\n * from both components, so the first NUL splits it unambiguously. A key with no\n * NUL cannot have been written by `edgeTargetKey`; rather than fabricate a wrong\n * `(scope, id)` from corrupt / externally-edited table data, throw so the query\n * surfaces it as a loud `Failure`.\n */\n private static _parseKey(key: string): IEdgeTarget {\n const nul: number = key.indexOf('\\0');\n if (nul < 0) {\n throw new Error(`malformed target key '${key}': missing scope/id separator (corrupt persisted data)`);\n }\n return {\n scope: key.slice(0, nul) as unknown as MemoryScopeKey,\n id: key.slice(nul + 1) as unknown as MemoryId\n };\n }\n\n /**\n * Coerce a persisted locator offset to a plain `number`. `better-sqlite3` returns\n * integer columns as `bigint` under safe-integer mode, so an offset can arrive as\n * either; both narrow to `number` here. A value outside the safe-integer range\n * (only reachable via corrupt / externally-edited data — the index only ever\n * writes in-document offsets) throws rather than silently losing precision, so the\n * query surfaces it as a loud `Failure`.\n */\n private static _toOffset(value: number | bigint, key: string): number {\n const n: number = Number(value);\n if (!Number.isSafeInteger(n)) {\n throw new Error(\n `fragment '${key}': locator offset ${String(value)} is not a safe integer (corrupt persisted data)`\n );\n }\n return n;\n }\n}\n\n/** The prepared statements / helpers the fragment index reuses once its table exists. */\ninterface IFragmentStatements {\n readonly deleteByTarget: BetterSqlite3.Statement;\n readonly replace: (key: string, fragments: ReadonlyArray<IEmbeddedFragment>) => void;\n readonly query: BetterSqlite3.Statement;\n readonly fragmentCount: BetterSqlite3.Statement;\n readonly recordCount: BetterSqlite3.Statement;\n}\n"]}