@hviana/sema 0.1.5 → 0.1.7

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 (170) hide show
  1. package/AGENTS.md +6 -5
  2. package/CITATION.cff +49 -0
  3. package/HOW_IT_WORKS.md +11 -12
  4. package/README.md +7 -5
  5. package/dist/example/demo.js +25 -25
  6. package/dist/example/train_base.d.ts +19 -21
  7. package/dist/example/train_base.js +1700 -1952
  8. package/dist/src/alphabet.d.ts +3 -3
  9. package/dist/src/alphabet.js +24 -27
  10. package/dist/src/alu/src/alu.d.ts +169 -194
  11. package/dist/src/alu/src/alu.js +374 -414
  12. package/dist/src/alu/src/expr.d.ts +36 -43
  13. package/dist/src/alu/src/expr.js +252 -278
  14. package/dist/src/alu/src/index.d.ts +9 -98
  15. package/dist/src/alu/src/index.js +10 -64
  16. package/dist/src/alu/src/kernel-arith.d.ts +1 -5
  17. package/dist/src/alu/src/kernel-arith.js +195 -285
  18. package/dist/src/alu/src/kernel-bits.d.ts +1 -5
  19. package/dist/src/alu/src/kernel-bits.js +81 -115
  20. package/dist/src/alu/src/kernel-logic.js +33 -63
  21. package/dist/src/alu/src/kernel-nd.js +155 -195
  22. package/dist/src/alu/src/kernel-numeric.d.ts +16 -80
  23. package/dist/src/alu/src/kernel-numeric.js +259 -331
  24. package/dist/src/alu/src/operation.d.ts +112 -132
  25. package/dist/src/alu/src/operation.js +149 -163
  26. package/dist/src/alu/src/parser.d.ts +168 -175
  27. package/dist/src/alu/src/parser.js +382 -421
  28. package/dist/src/alu/src/resonance.d.ts +16 -25
  29. package/dist/src/alu/src/resonance.js +49 -56
  30. package/dist/src/alu/src/text.d.ts +3 -7
  31. package/dist/src/alu/src/text.js +31 -37
  32. package/dist/src/alu/src/value.d.ts +14 -14
  33. package/dist/src/alu/src/value.js +150 -160
  34. package/dist/src/alu/test/alu.test.js +543 -656
  35. package/dist/src/bytes.d.ts +1 -5
  36. package/dist/src/bytes.js +33 -40
  37. package/dist/src/config.d.ts +99 -102
  38. package/dist/src/config.js +78 -83
  39. package/dist/src/derive/src/deduction.d.ts +60 -62
  40. package/dist/src/derive/src/deduction.js +103 -109
  41. package/dist/src/derive/src/index.d.ts +1 -7
  42. package/dist/src/derive/src/priority-queue.d.ts +8 -8
  43. package/dist/src/derive/src/priority-queue.js +57 -61
  44. package/dist/src/derive/src/rewrite.d.ts +15 -18
  45. package/dist/src/derive/src/rewrite.js +59 -67
  46. package/dist/src/derive/src/trie.d.ts +56 -56
  47. package/dist/src/derive/src/trie.js +175 -179
  48. package/dist/src/derive/test/derive.test.js +96 -100
  49. package/dist/src/extension.d.ts +13 -16
  50. package/dist/src/geometry.d.ts +29 -43
  51. package/dist/src/geometry.js +324 -268
  52. package/dist/src/index.d.ts +2 -17
  53. package/dist/src/index.js +2 -12
  54. package/dist/src/ingest-cache.d.ts +25 -30
  55. package/dist/src/ingest-cache.js +108 -127
  56. package/dist/src/mind/articulation.d.ts +1 -5
  57. package/dist/src/mind/articulation.js +77 -112
  58. package/dist/src/mind/attention.d.ts +40 -108
  59. package/dist/src/mind/attention.js +754 -872
  60. package/dist/src/mind/canonical.d.ts +4 -19
  61. package/dist/src/mind/canonical.js +27 -31
  62. package/dist/src/mind/graph-search.d.ts +201 -225
  63. package/dist/src/mind/graph-search.js +742 -821
  64. package/dist/src/mind/index.d.ts +2 -10
  65. package/dist/src/mind/junction.d.ts +31 -58
  66. package/dist/src/mind/junction.js +172 -237
  67. package/dist/src/mind/learning.d.ts +16 -52
  68. package/dist/src/mind/learning.js +143 -165
  69. package/dist/src/mind/match.d.ts +20 -69
  70. package/dist/src/mind/match.js +259 -318
  71. package/dist/src/mind/mechanisms/alu.js +16 -16
  72. package/dist/src/mind/mechanisms/cast.d.ts +9 -13
  73. package/dist/src/mind/mechanisms/cast.js +363 -456
  74. package/dist/src/mind/mechanisms/confluence.d.ts +8 -12
  75. package/dist/src/mind/mechanisms/confluence.js +152 -183
  76. package/dist/src/mind/mechanisms/cover.d.ts +2 -8
  77. package/dist/src/mind/mechanisms/cover.js +148 -210
  78. package/dist/src/mind/mechanisms/extraction.d.ts +8 -34
  79. package/dist/src/mind/mechanisms/extraction.js +234 -288
  80. package/dist/src/mind/mechanisms/recall.d.ts +6 -10
  81. package/dist/src/mind/mechanisms/recall.js +141 -185
  82. package/dist/src/mind/mind.d.ts +194 -155
  83. package/dist/src/mind/mind.js +392 -289
  84. package/dist/src/mind/pipeline-mechanism.d.ts +112 -124
  85. package/dist/src/mind/pipeline-mechanism.js +161 -172
  86. package/dist/src/mind/pipeline.d.ts +4 -14
  87. package/dist/src/mind/pipeline.js +125 -189
  88. package/dist/src/mind/primitives.d.ts +20 -33
  89. package/dist/src/mind/primitives.js +124 -120
  90. package/dist/src/mind/rationale.d.ts +86 -98
  91. package/dist/src/mind/rationale.js +113 -121
  92. package/dist/src/mind/reasoning.d.ts +2 -13
  93. package/dist/src/mind/reasoning.js +129 -166
  94. package/dist/src/mind/recognition.d.ts +1 -4
  95. package/dist/src/mind/recognition.js +198 -210
  96. package/dist/src/mind/resonance.d.ts +5 -22
  97. package/dist/src/mind/resonance.js +0 -0
  98. package/dist/src/mind/trace.d.ts +6 -25
  99. package/dist/src/mind/trace.js +50 -58
  100. package/dist/src/mind/traverse.d.ts +29 -58
  101. package/dist/src/mind/traverse.js +344 -356
  102. package/dist/src/mind/types.d.ts +122 -127
  103. package/dist/src/mind/types.js +60 -69
  104. package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
  105. package/dist/src/rabitq-ivf/src/database.js +201 -0
  106. package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
  107. package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.js +1 -3
  108. package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
  109. package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
  110. package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
  111. package/dist/src/rabitq-ivf/src/prng.js +36 -0
  112. package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
  113. package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
  114. package/dist/src/sema.d.ts +9 -13
  115. package/dist/src/sema.js +26 -40
  116. package/dist/src/store-sqlite.d.ts +171 -166
  117. package/dist/src/store-sqlite.js +768 -662
  118. package/dist/src/store.d.ts +597 -630
  119. package/dist/src/store.js +1434 -1581
  120. package/dist/src/vec.d.ts +5 -9
  121. package/dist/src/vec.js +61 -73
  122. package/example/train_base.ts +33 -15
  123. package/index.html +65 -0
  124. package/package.json +1 -1
  125. package/src/alu/README.md +1 -1
  126. package/src/alu/src/index.ts +1 -1
  127. package/src/config.ts +19 -27
  128. package/src/geometry.ts +93 -1
  129. package/src/index.ts +6 -11
  130. package/src/mind/attention.ts +11 -6
  131. package/src/mind/mechanisms/recall.ts +29 -13
  132. package/src/mind/mind.ts +250 -19
  133. package/src/mind/primitives.ts +43 -6
  134. package/src/mind/recognition.ts +26 -8
  135. package/src/mind/traverse.ts +47 -0
  136. package/src/mind/types.ts +20 -21
  137. package/src/rabitq-ivf/README.md +56 -0
  138. package/src/rabitq-ivf/src/database.ts +276 -0
  139. package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
  140. package/src/rabitq-ivf/src/ivf.ts +1330 -0
  141. package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
  142. package/src/store-sqlite.ts +204 -9
  143. package/src/store.ts +27 -34
  144. package/test/08-storage.test.mjs +3 -3
  145. package/test/13-conversation.test.mjs +190 -20
  146. package/test/14-scaling.test.mjs +2 -2
  147. package/test/35-ivf.test.mjs +263 -0
  148. package/test/36-bloom.test.mjs +123 -0
  149. package/dist/src/rabitq-hnsw/src/database.d.ts +0 -202
  150. package/dist/src/rabitq-hnsw/src/database.js +0 -405
  151. package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
  152. package/dist/src/rabitq-hnsw/src/heap.js +0 -94
  153. package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
  154. package/dist/src/rabitq-hnsw/src/hnsw.js +0 -500
  155. package/dist/src/rabitq-hnsw/src/index.d.ts +0 -15
  156. package/dist/src/rabitq-hnsw/src/prng.d.ts +0 -19
  157. package/dist/src/rabitq-hnsw/src/prng.js +0 -38
  158. package/dist/src/rabitq-hnsw/src/rabitq.d.ts +0 -95
  159. package/dist/src/rabitq-hnsw/src/rabitq.js +0 -299
  160. package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
  161. package/dist/src/rabitq-hnsw/src/store.js +0 -916
  162. package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
  163. package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -1197
  164. package/src/rabitq-hnsw/README.md +0 -303
  165. package/src/rabitq-hnsw/src/database.ts +0 -492
  166. package/src/rabitq-hnsw/src/heap.ts +0 -90
  167. package/src/rabitq-hnsw/src/hnsw.ts +0 -514
  168. package/src/rabitq-hnsw/src/store.ts +0 -994
  169. package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
  170. /package/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.ts +0 -0
@@ -1,916 +0,0 @@
1
- import { DatabaseSync } from "node:sqlite";
2
- const NODES = (name) => `
3
- CREATE TABLE ${name} (
4
- id INTEGER PRIMARY KEY,
5
- ext INTEGER,
6
- level INTEGER NOT NULL,
7
- deleted INTEGER NOT NULL DEFAULT 0,
8
- code BLOB NOT NULL
9
- )`;
10
- const LINKS = (name) => `
11
- CREATE TABLE ${name} (
12
- node INTEGER NOT NULL,
13
- layer INTEGER NOT NULL,
14
- nbrs BLOB NOT NULL,
15
- PRIMARY KEY (node, layer)
16
- ) WITHOUT ROWID`;
17
- const EXT_INDEX =
18
- "CREATE UNIQUE INDEX idx_nodes_ext ON nodes(ext) WHERE ext IS NOT NULL";
19
- /** Direct-mapped, typed-array-backed node cache: `slot = id % capacity`, a
20
- * colliding insert overwrites. No per-entry heap objects, no recency
21
- * bookkeeping — see the cache commentary in {@link Store}. */
22
- class CodeSlab {
23
- codeBytes;
24
- keys; // node id per slot; -1 = empty
25
- ext; // NaN = null ext
26
- deleted;
27
- codes; // capacity × codeBytes, flat
28
- capacity;
29
- constructor(capacity, codeBytes) {
30
- this.codeBytes = codeBytes;
31
- this.capacity = capacity;
32
- this.keys = new Float64Array(capacity).fill(-1);
33
- this.ext = new Float64Array(capacity);
34
- this.deleted = new Uint8Array(capacity);
35
- this.codes = new Uint8Array(capacity * codeBytes);
36
- }
37
- /** The cached record, or undefined. The returned NodeRec is a fresh small
38
- * object whose code is a read-only VIEW into the slab (callers never
39
- * mutate codes; an overwrite of this slot only happens on a later insert,
40
- * after the view's one-operation lifetime). */
41
- get(id) {
42
- const slot = id % this.capacity;
43
- if (this.keys[slot] !== id) {
44
- return undefined;
45
- }
46
- const e = this.ext[slot];
47
- return {
48
- code: this.codes.subarray(
49
- slot * this.codeBytes,
50
- (slot + 1) * this.codeBytes,
51
- ),
52
- deleted: this.deleted[slot],
53
- ext: Number.isNaN(e) ? null : e,
54
- };
55
- }
56
- has(id) {
57
- return this.keys[id % this.capacity] === id;
58
- }
59
- set(id, code, deleted, ext) {
60
- if (code.length !== this.codeBytes) {
61
- return; // foreign geometry — skip
62
- }
63
- const slot = id % this.capacity;
64
- this.keys[slot] = id;
65
- this.ext[slot] = ext === null ? NaN : ext;
66
- this.deleted[slot] = deleted;
67
- this.codes.set(code, slot * this.codeBytes);
68
- }
69
- /** Mark a cached id tombstoned in place (mirrors the row update). */
70
- markDeleted(id) {
71
- const slot = id % this.capacity;
72
- if (this.keys[slot] === id) {
73
- this.deleted[slot] = 1;
74
- this.ext[slot] = NaN;
75
- }
76
- }
77
- clear() {
78
- this.keys.fill(-1);
79
- }
80
- }
81
- /** Direct-mapped cache of decoded neighbour lists, same design as
82
- * {@link CodeSlab}: `slot = (node·64+layer) % capacity`, fixed-width rows,
83
- * a colliding insert overwrites, nothing heap-allocated per entry. */
84
- class NbrSlab {
85
- width;
86
- keys; // node·64+layer per slot; -1 = empty
87
- counts; // 0xff = cached "no list" (null)
88
- data; // capacity × width, flat
89
- capacity;
90
- constructor(capacity, width) {
91
- this.width = width;
92
- this.capacity = capacity;
93
- this.keys = new Float64Array(capacity).fill(-1);
94
- this.counts = new Uint8Array(capacity);
95
- this.data = new Uint32Array(capacity * width);
96
- }
97
- /** The cached list (a fresh copy — safe across later overwrites), null for
98
- * a cached "no list here", undefined on a miss. */
99
- get(key) {
100
- const slot = key % this.capacity;
101
- if (this.keys[slot] !== key) {
102
- return undefined;
103
- }
104
- const n = this.counts[slot];
105
- if (n === 0xff) {
106
- return null;
107
- }
108
- const base = slot * this.width;
109
- return this.data.slice(base, base + n);
110
- }
111
- set(key, ids) {
112
- if (ids !== null && ids.length > this.width) {
113
- return; // oversize — skip
114
- }
115
- const slot = key % this.capacity;
116
- this.keys[slot] = key;
117
- if (ids === null) {
118
- this.counts[slot] = 0xff;
119
- return;
120
- }
121
- this.counts[slot] = ids.length;
122
- const base = slot * this.width;
123
- for (let i = 0; i < ids.length; i++) {
124
- this.data[base + i] = ids[i];
125
- }
126
- }
127
- delete(key) {
128
- const slot = key % this.capacity;
129
- if (this.keys[slot] === key) {
130
- this.keys[slot] = -1;
131
- }
132
- }
133
- clear() {
134
- this.keys.fill(-1);
135
- }
136
- }
137
- export class Store {
138
- db;
139
- /**
140
- * Number of row reads served from the database (node and neighbour-list
141
- * fetches). This counts *storage* accesses only, so it is unaffected by any
142
- * caching layer above it -- the honest measure of how the engine scales.
143
- */
144
- reads = 0;
145
- sInsNode;
146
- sNode;
147
- sIdByExt;
148
- sTombstone;
149
- sNbrs;
150
- sSetNbrs;
151
- sState;
152
- // A node's 1-bit code is IMMUTABLE once written, yet building and searching the
153
- // graph re-read the same hub nodes constantly: an insert touches ~ef distinct
154
- // nodes, and across consecutive inserts (especially clustered data) those sets
155
- // overlap heavily, so the same code is fetched again and again. Each fetch is a
156
- // SQLite point-query that decodes the row and copies the BLOB, while the
157
- // distance it feeds is ~0.04µs. So getNode, not arithmetic, dominates a build.
158
- //
159
- // A DIRECT-MAPPED SLAB CACHE removes it. Earlier revisions used a Map-based
160
- // LRU of NodeRec objects; measured at trained-store scale (2.4M+ cached
161
- // entries) the Map itself became the bottleneck — per-op hash cost, the
162
- // delete+set recency churn, eviction bookkeeping, and millions of long-lived
163
- // heap objects for the GC to trace. Filling the cache made ingest SLOWER
164
- // than leaving it cold. The slab holds everything in a handful of typed
165
- // arrays (keys, ext, deleted flags, and one flat code slab), slot = id mod
166
- // capacity: a lookup is one modulo and one key compare; an insert on a
167
- // colliding slot simply overwrites it (dense monotone ids make modulo a
168
- // uniform spread, so collisions stay rare while occupancy < capacity); there
169
- // is NO recency bookkeeping and NOTHING for the GC to trace. Two slabs:
170
- // the MAIN one, and an UPPER-LAYER one for level ≥ 1 nodes — the ~12% of
171
- // the collection every operation's descent touches — so layer-0 fan-out
172
- // traffic can never evict the descent's working set however far the
173
- // collection outgrows the budget.
174
- //
175
- // The honesty rules are unchanged from the LRU it replaces:
176
- // • A LATENCY layer only — `reads` (the scalability witness) counts
177
- // SQLite fall-throughs, so it is UNAFFECTED by the cache; the rabitq
178
- // suite asserts scaling with the cache off.
179
- // • BOUNDED BY THE SAME MEMORY BUDGET as the page cache: slot count is
180
- // derived from `cacheSizeMb` and the code size (no second knob), the
181
- // upper-layer slab takes at most half. cacheSizeMb=0 disables both.
182
- // • The only mutable fields (deleted/ext) are updated in place by
183
- // tombstone(); both slabs are dropped at compaction. A collision or a
184
- // miss only repeats work, never changes a result.
185
- cacheBudgetBytes;
186
- /** Whether the shared memory budget is non-zero — consumers that trade RAM
187
- * for speed (the graph's visited-tag array) key off this so the
188
- * `cacheSizeMb: 0` flat-memory mode stays exactly flat. */
189
- get cacheEnabled() {
190
- return this.cacheBudgetBytes > 0;
191
- }
192
- slab = null; // main section (level 0)
193
- slabHot = null; // upper-layer section (level ≥ 1)
194
- nbrSlab = null; // neighbour-list cache
195
- slabsDerived = false; // false until the first cacheRec sizes them
196
- /**
197
- * @param dbPath path to the SQLite file (":memory:" for transient).
198
- * @param cacheSizeMb the ONE memory knob (MiB). It sizes BOTH SQLite's page
199
- * cache AND the immutable-code LRU above (whose entry-capacity is derived
200
- * from this budget and the code size — no second knob). Both are pure
201
- * speed enhancements: correctness and the per-operation storage-read
202
- * count do not depend on them. Pass 0 to run with essentially no cache,
203
- * which is how the tests run so a poor access pattern can never hide
204
- * behind a warm cache.
205
- */
206
- constructor(dbPath, cacheSizeMb = 64) {
207
- this.cacheBudgetBytes = Math.max(0, Math.floor(cacheSizeMb * 1024 * 1024));
208
- this.db = new DatabaseSync(dbPath);
209
- // WAL + NORMAL sync is crash-safe and fast; temp structures stay in memory.
210
- // The page cache is the single memory budget; mmap is tied to it so "off"
211
- // means every row read is an honest B-tree descent against the file.
212
- const kb = Math.max(0, Math.floor(cacheSizeMb * 1024));
213
- const cacheSize = kb > 0 ? `-${kb}` : "0";
214
- const mmap = kb > 0 ? cacheSizeMb * 1024 * 1024 : 0;
215
- // Rows here are one code (~dim/8 bytes) or one packed neighbour list —
216
- // small against SQLite's default 4 KiB page, which then carries mostly
217
- // slack. 1 KiB pages keep leaf-page fill high; must be set before the
218
- // first table is created (no-op on an existing database).
219
- this.db.exec("PRAGMA page_size = 1024;");
220
- this.db.exec(`
221
- PRAGMA journal_mode = WAL;
222
- PRAGMA synchronous = NORMAL;
223
- PRAGMA temp_store = MEMORY;
224
- PRAGMA cache_size = ${cacheSize};
225
- PRAGMA mmap_size = ${mmap};
226
- PRAGMA foreign_keys = OFF;
227
- `);
228
- this.db.exec(`
229
- CREATE TABLE IF NOT EXISTS meta (
230
- id INTEGER PRIMARY KEY CHECK (id = 0),
231
- dim INTEGER NOT NULL,
232
- m INTEGER NOT NULL,
233
- ef_construction INTEGER NOT NULL,
234
- ef_search INTEGER NOT NULL,
235
- query_bits INTEGER NOT NULL,
236
- rotation_rounds INTEGER NOT NULL,
237
- seed INTEGER NOT NULL,
238
- centroid BLOB,
239
- code_words INTEGER NOT NULL,
240
- padded_dim INTEGER NOT NULL,
241
- entry_point INTEGER NOT NULL DEFAULT -1,
242
- max_level INTEGER NOT NULL DEFAULT -1,
243
- live INTEGER NOT NULL DEFAULT 0,
244
- total INTEGER NOT NULL DEFAULT 0,
245
- rng INTEGER NOT NULL DEFAULT 0
246
- );
247
- `);
248
- if (!this.tableExists("nodes")) {
249
- this.db.exec(NODES("nodes"));
250
- this.db.exec(EXT_INDEX);
251
- this.db.exec(LINKS("links"));
252
- }
253
- this.prepareAll();
254
- }
255
- tableExists(name) {
256
- const row = this.db
257
- .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
258
- .get(name);
259
- return row !== undefined;
260
- }
261
- /** (Re)compile the hot statements against the current schema. */
262
- prepareAll() {
263
- this.batchStmts.clear();
264
- // Recompiled statements mean a table swap (compaction) — every cached
265
- // decoded list may now describe a dropped table's rows.
266
- this.nbrSlab?.clear();
267
- this.sInsNode = this.db.prepare(
268
- "INSERT INTO nodes(ext, level, code) VALUES (?, ?, ?)",
269
- );
270
- this.sNode = this.db.prepare(
271
- "SELECT ext, level, deleted, code FROM nodes WHERE id = ?",
272
- );
273
- // Positional rows: the hot read path pays for a plain array instead of a
274
- // per-row object with named properties.
275
- this.sNode.setReturnArrays(true);
276
- this.sIdByExt = this.db.prepare("SELECT id FROM nodes WHERE ext = ?");
277
- this.sTombstone = this.db.prepare(
278
- "UPDATE nodes SET deleted = 1, ext = NULL WHERE id = ?",
279
- );
280
- this.sNbrs = this.db.prepare(
281
- "SELECT nbrs FROM links WHERE node = ? AND layer = ?",
282
- );
283
- this.sNbrs.setReturnArrays(true);
284
- this.sSetNbrs = this.db.prepare(
285
- "INSERT INTO links(node, layer, nbrs) VALUES (?, ?, ?) " +
286
- "ON CONFLICT(node, layer) DO UPDATE SET nbrs = excluded.nbrs",
287
- );
288
- this.sState = this.db.prepare(
289
- "UPDATE meta SET entry_point = ?, max_level = ?, live = ?, total = ?, rng = ? WHERE id = 0",
290
- );
291
- }
292
- // ----------------------------- config / state ----------------------------
293
- /** Load the persisted configuration, or null for a fresh database. */
294
- loadConfig() {
295
- const row = this.db.prepare("SELECT * FROM meta WHERE id = 0").get();
296
- if (!row) {
297
- return null;
298
- }
299
- const cb = row.centroid;
300
- let centroid = null;
301
- if (cb) {
302
- centroid = new Float64Array(row.dim);
303
- new Uint8Array(centroid.buffer).set(cb);
304
- }
305
- return {
306
- dim: row.dim,
307
- m: row.m,
308
- efConstruction: row.ef_construction,
309
- efSearch: row.ef_search,
310
- queryBits: row.query_bits,
311
- rotationRounds: row.rotation_rounds,
312
- seed: row.seed,
313
- centroid,
314
- codeWords: row.code_words,
315
- paddedDim: row.padded_dim,
316
- };
317
- }
318
- /** Initialise the meta row for a new database. */
319
- initConfig(c) {
320
- const centroidBlob = c.centroid
321
- ? new Uint8Array(
322
- c.centroid.buffer,
323
- c.centroid.byteOffset,
324
- c.centroid.byteLength,
325
- )
326
- : null;
327
- this.db
328
- .prepare(
329
- "INSERT INTO meta(id, dim, m, ef_construction, ef_search, query_bits, rotation_rounds, " +
330
- "seed, centroid, code_words, padded_dim, rng) VALUES (0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
331
- )
332
- .run(
333
- c.dim,
334
- c.m,
335
- c.efConstruction,
336
- c.efSearch,
337
- c.queryBits,
338
- c.rotationRounds,
339
- c.seed,
340
- centroidBlob,
341
- c.codeWords,
342
- c.paddedDim,
343
- c.seed >>> 0,
344
- );
345
- }
346
- loadState() {
347
- const row = this.db
348
- .prepare(
349
- "SELECT entry_point, max_level, live, total, rng FROM meta WHERE id = 0",
350
- )
351
- .get();
352
- return {
353
- entryPoint: row.entry_point,
354
- maxLevel: row.max_level,
355
- live: row.live,
356
- total: row.total,
357
- rng: row.rng >>> 0,
358
- };
359
- }
360
- saveState(s) {
361
- this.sState.run(s.entryPoint, s.maxLevel, s.live, s.total, s.rng | 0);
362
- }
363
- setEfSearch(ef) {
364
- this.db.prepare("UPDATE meta SET ef_search = ? WHERE id = 0").run(ef);
365
- }
366
- setQueryBits(bits) {
367
- this.db.prepare("UPDATE meta SET query_bits = ? WHERE id = 0").run(bits);
368
- }
369
- resetReads() {
370
- this.reads = 0;
371
- }
372
- /** Stream every live external id in id order, in bounded-memory batches. */
373
- *liveExts(batch = 1024) {
374
- const q = this.db.prepare(
375
- "SELECT id, ext FROM nodes WHERE deleted = 0 AND id > ? ORDER BY id LIMIT ?",
376
- );
377
- let cursor = 0;
378
- for (;;) {
379
- const rows = q.all(cursor, batch);
380
- if (rows.length === 0) {
381
- break;
382
- }
383
- for (const r of rows) {
384
- yield r.ext;
385
- }
386
- cursor = rows[rows.length - 1].id;
387
- }
388
- }
389
- // -------------------------------- nodes -----------------------------------
390
- /** Size the two slabs from the shared memory budget and the actual code
391
- * size (known on the first record cached): total slots = budget / bytes
392
- * per slot, the upper-layer slab taking half. A slot is the code bytes
393
- * plus 17 B of typed-array bookkeeping (key + ext + deleted). */
394
- deriveSlabs(codeBytes) {
395
- this.slabsDerived = true;
396
- if (this.cacheBudgetBytes === 0) {
397
- return; // flat-memory mode — no slabs
398
- }
399
- const perSlot = codeBytes + 17;
400
- const total = Math.max(2, Math.floor(this.cacheBudgetBytes / perSlot));
401
- this.slabHot = new CodeSlab(total >> 1, codeBytes);
402
- this.slab = new CodeSlab(total - (total >> 1), codeBytes);
403
- // One-quarter of the budget for neighbour lists (neighbour ids).
404
- // A layer-0 list is ~4·Mmax0 entries; we budget 4·16 ids per row.
405
- this.nbrSlab = new NbrSlab(Math.max(1, total >> 2), 64);
406
- }
407
- /** Record a node in the slab cache. Upper-layer nodes (level ≥ 1) go to
408
- * the pinned section layer-0 traffic never touches; a colliding slot is
409
- * simply overwritten (direct-mapped eviction — no bookkeeping). */
410
- cacheRec(id, code, deleted, ext, level) {
411
- if (!this.slabsDerived) {
412
- this.deriveSlabs(code.byteLength);
413
- }
414
- const slab = level >= 1 ? this.slabHot : this.slab;
415
- slab?.set(id, code, deleted, ext);
416
- }
417
- /** Insert a node, returning its assigned id (rowid). */
418
- addNode(ext, level, code) {
419
- const info = this.sInsNode.run(ext, level, code);
420
- const id = Number(info.lastInsertRowid);
421
- // The new node is read back immediately and repeatedly while it is wired in;
422
- // seed its (immutable) code now so those reads hit the cache, not SQLite.
423
- this.cacheRec(id, code, 0, ext, level);
424
- return id;
425
- }
426
- /** Fetch a node's code (copied), tombstone flag and external id, or null.
427
- * Served from the slab cache when present; a miss reads SQLite, counts
428
- * one `reads` (the cache-independent scalability witness), and caches the
429
- * result. The cache is a latency layer ONLY — `reads` does not count hits, so
430
- * the index's storage-read scaling is identical with the cache off. */
431
- getNode(id) {
432
- // Pinned upper-layer section first, then the main slab.
433
- const cached = this.slabHot?.get(id) ?? this.slab?.get(id);
434
- if (cached !== undefined) {
435
- return cached;
436
- }
437
- this.reads++;
438
- // Positional row: [ext, level, deleted, code]. node:sqlite hands back an
439
- // OWNED Uint8Array per BLOB read (verified: mutating one read never shows
440
- // in another), so the code is kept as returned — no defensive copy.
441
- const row = this.sNode.get(id);
442
- if (!row) {
443
- return null;
444
- }
445
- this.cacheRec(id, row[3], row[2], row[0], row[1]);
446
- return { code: row[3], deleted: row[2], ext: row[0] };
447
- }
448
- idByExt(ext) {
449
- const row = this.sIdByExt.get(ext);
450
- return row ? row.id : null;
451
- }
452
- // Batched code fetch. An insert's search layer expands a popped node's
453
- // whole neighbour list at once, and each cache-missing neighbour used to be
454
- // its own point query — the statement dispatch (not the B-tree descent)
455
- // dominated a trained-store build at ~50% of total CPU. One IN(...) query
456
- // per expansion fetches all misses together; the per-row `reads` accounting
457
- // is unchanged, so the cache-independent scalability witness is identical.
458
- batchStmts = new Map();
459
- static BATCH_MAX = 64;
460
- batchStmt(n) {
461
- let s = this.batchStmts.get(n);
462
- if (s === undefined) {
463
- s = this.db.prepare(
464
- "SELECT id, ext, level, deleted, code FROM nodes WHERE id IN (" +
465
- "?,".repeat(n - 1) + "?)",
466
- );
467
- s.setReturnArrays(true);
468
- this.batchStmts.set(n, s);
469
- }
470
- return s;
471
- }
472
- /** Fetch several nodes into `out` (skipping ids already present). Serves
473
- * from the code LRU first; the misses are read with one IN query per
474
- * chunk. Ids with no row are simply absent from `out`. Each id that
475
- * reaches SQLite counts one `reads`, exactly like a getNode miss. */
476
- getNodesInto(ids, out, count = ids.length) {
477
- let miss = null;
478
- for (let i = 0; i < count; i++) {
479
- const id = ids[i];
480
- if (out.has(id)) {
481
- continue;
482
- }
483
- const cached = this.slabHot?.get(id) ?? this.slab?.get(id);
484
- if (cached !== undefined) {
485
- out.set(id, cached);
486
- continue;
487
- }
488
- (miss ??= []).push(id);
489
- }
490
- if (miss === null) {
491
- return;
492
- }
493
- for (let o = 0; o < miss.length; o += Store.BATCH_MAX) {
494
- const chunk = miss.slice(o, o + Store.BATCH_MAX);
495
- this.reads += chunk.length;
496
- // Positional rows: [id, ext, level, deleted, code]; the code BLOB is an
497
- // owned buffer (see getNode), kept without a defensive copy.
498
- const rows = this.batchStmt(chunk.length).all(...chunk);
499
- for (const row of rows) {
500
- this.cacheRec(row[0], row[4], row[3], row[1], row[2]);
501
- out.set(row[0], { code: row[4], deleted: row[3], ext: row[1] });
502
- }
503
- }
504
- }
505
- /** Pre-fill the code and neighbour-list caches with ONE sequential table
506
- * scan each, up to their existing budget-derived caps. A cold session
507
- * otherwise warms the caches through hundreds of thousands of RANDOM point
508
- * reads spread across its first minutes of inserts/queries; a sequential
509
- * scan streams the same rows at C speed in seconds. A pure latency
510
- * optimisation with the exact same caps and coherence rules as demand
511
- * filling — nothing about results, `reads` discipline, or memory ceilings
512
- * changes. Two passes over `nodes` so the upper-layer pinned section is
513
- * filled before layer-0 rows compete for the LRU. Returns rows warmed. */
514
- warmCache() {
515
- if (this.cacheBudgetBytes === 0) {
516
- return 0;
517
- }
518
- let warmed = 0;
519
- // One scan over the node rows; cacheRec routes each to its slab (the
520
- // sections are direct-mapped, so there is no fill-order competition).
521
- {
522
- const scan = this.db.prepare(
523
- "SELECT id, ext, level, deleted, code FROM nodes",
524
- );
525
- scan.setReturnArrays(true);
526
- for (const row of scan.iterate()) {
527
- this.cacheRec(row[0], row[4], row[3], row[1], row[2]);
528
- warmed++;
529
- }
530
- }
531
- // Neighbour lists: one pass; nbrSlab is direct-mapped so fill order
532
- // doesn't matter (later entries simply overwrite earlier collisions).
533
- {
534
- const scan = this.db.prepare("SELECT node, layer, nbrs FROM links");
535
- scan.setReturnArrays(true);
536
- for (const row of scan.iterate()) {
537
- if (row[1] >= 64) {
538
- continue;
539
- }
540
- this.nbrSlab?.set(
541
- row[0] * 64 + row[1],
542
- Uint32Array.from(this.decodeNbrs(row[2])),
543
- );
544
- warmed++;
545
- }
546
- }
547
- return warmed;
548
- }
549
- tombstone(id) {
550
- this.sTombstone.run(id);
551
- // Keep any cached record coherent with the row: a later hit must show the
552
- // same deleted/ext a fresh SQLite read would (search routes through but never
553
- // returns tombstoned nodes).
554
- this.slab?.markDeleted(id);
555
- this.slabHot?.markDeleted(id);
556
- }
557
- // -------------------------------- links -----------------------------------
558
- // Neighbour lists are SETS to the graph algorithms: search visits every
559
- // member and pruning recomputes distances from the codes, so storage order
560
- // carries no information. That freedom pays for compression: the list is
561
- // stored sorted as delta-VARINTS — consecutive graph ids are small deltas, so
562
- // a neighbour costs ~1–2 bytes instead of a fixed 4, and the cost scales with
563
- // log(collection) instead of a fixed word width. Links are written once per
564
- // wiring update and read on every hop, and decoding is a linear byte scan —
565
- // noise against the row fetch it rides on.
566
- // DECODED NEIGHBOUR-LIST CACHE. A direct-mapped NbrSlab (same design as
567
- // the code slab). Budget: one quarter of the shared cacheSizeMb, sized in
568
- // deriveSlabs alongside the code slabs. Each list is a fresh copy from the
569
- // slab, so callers are safe across later overwrites. `setNeighbors` writes
570
- // through, so a hit always equals a fresh row read.
571
- /** A node's neighbour ids at a layer, or null if it has no list there. */
572
- getNeighbors(node, layer) {
573
- if (layer < 64) {
574
- const hit = this.nbrSlab?.get(node * 64 + layer);
575
- if (hit !== undefined) {
576
- return hit;
577
- }
578
- }
579
- this.reads++;
580
- const row = this.sNbrs.get(node, layer);
581
- if (!row) {
582
- this.nbrSlab?.set(node * 64 + layer, null);
583
- return null;
584
- }
585
- const blob = row[0];
586
- // Entry count = terminator bytes (high bit clear): one cheap scan sizes
587
- // the result exactly, so the hot path pays ONE allocation instead of a
588
- // growable number[] plus a Uint32Array copy per graph hop.
589
- let count = 0;
590
- for (let i = 0; i < blob.length; i++) {
591
- if ((blob[i] & 0x80) === 0) {
592
- count++;
593
- }
594
- }
595
- const ids = new Uint32Array(count);
596
- let acc = 0, shift = 0, prev = 0, n = 0;
597
- for (let i = 0; i < blob.length; i++) {
598
- const b = blob[i];
599
- acc |= (b & 0x7f) << shift;
600
- if (b & 0x80) {
601
- shift += 7;
602
- } else {
603
- prev += acc;
604
- ids[n++] = prev;
605
- acc = 0;
606
- shift = 0;
607
- }
608
- }
609
- this.nbrSlab?.set(node * 64 + layer, ids);
610
- return ids;
611
- }
612
- setNeighbors(node, layer, ids) {
613
- const s = ids.slice().sort((a, b) => a - b);
614
- const buf = new Uint8Array(s.length * 5);
615
- let o = 0, prev = 0;
616
- for (let i = 0; i < s.length; i++) {
617
- let d = s[i] - prev;
618
- prev = s[i];
619
- while (d >= 0x80) {
620
- buf[o++] = (d & 0x7f) | 0x80;
621
- d >>>= 7;
622
- }
623
- buf[o++] = d;
624
- }
625
- this.sSetNbrs.run(node, layer, buf.subarray(0, o));
626
- // Write through the decoded-list cache: a later hit must equal what a
627
- // fresh row read would decode (same sorted order).
628
- // Write through the decoded-list cache: a later hit must equal what a
629
- // fresh row read would decode (same sorted order).
630
- if (layer < 64) {
631
- this.nbrSlab?.set(node * 64 + layer, Uint32Array.from(s));
632
- }
633
- }
634
- // ----------------------------- transactions -------------------------------
635
- begin() {
636
- this.db.exec("BEGIN");
637
- }
638
- commit() {
639
- this.db.exec("COMMIT");
640
- }
641
- rollback() {
642
- this.db.exec("ROLLBACK");
643
- }
644
- // ------------------------------- compaction -------------------------------
645
- // spliceCompact() reclaims tombstones by a GRAPH-PRESERVING structural copy:
646
- // live node rows are copied verbatim (SAME internal ids — nothing external
647
- // ever remaps), and each live neighbour list is rewritten with its dead
648
- // members replaced by those members' own live neighbours (a 1-hop bridge, the
649
- // standard tombstone splice). Codes are untouched — the index stays exactly
650
- // as lossy as RaBitQ made it, never more — and the graph keeps the wiring the
651
- // original inserts built, minus the dead routing. The previous compact
652
- // replayed every live code through a full HNSW insert: O(live · ef · log N)
653
- // storage reads — HOURS on a multi-million-node trained store, inside ONE
654
- // transaction whose WAL grew by the whole rebuild. The splice is one
655
- // streaming pass over the node and link rows (batched commits, bounded WAL).
656
- /** Encode a neighbour id list to the delta-varint blob format. */
657
- encodeNbrs(ids) {
658
- const s = ids.slice().sort((a, b) => a - b);
659
- const buf = new Uint8Array(s.length * 5);
660
- let o = 0, prev = 0;
661
- for (let i = 0; i < s.length; i++) {
662
- let d = s[i] - prev;
663
- prev = s[i];
664
- while (d >= 0x80) {
665
- buf[o++] = (d & 0x7f) | 0x80;
666
- d >>>= 7;
667
- }
668
- buf[o++] = d;
669
- }
670
- return buf.subarray(0, o);
671
- }
672
- /** Decode a delta-varint neighbour blob into a number[]. */
673
- decodeNbrs(blob) {
674
- const ids = [];
675
- let acc = 0, shift = 0, prev = 0;
676
- for (let i = 0; i < blob.length; i++) {
677
- const b = blob[i];
678
- acc |= (b & 0x7f) << shift;
679
- if (b & 0x80) {
680
- shift += 7;
681
- } else {
682
- prev += acc;
683
- ids.push(prev);
684
- acc = 0;
685
- shift = 0;
686
- }
687
- }
688
- return ids;
689
- }
690
- /** Tombstone-splice compaction. `M`/`Mmax0` cap a rewritten list on upper
691
- * layers / layer 0. Returns the new global state scalars (internal ids are
692
- * preserved, so `entry` survives unless it was itself dead). The caller
693
- * persists state and vacuums. */
694
- spliceCompact(M, Mmax0, entry) {
695
- const db = this.db;
696
- this.slab?.clear();
697
- this.slabHot?.clear();
698
- this.nbrSlab?.clear();
699
- // Fresh target tables (leftovers from an interrupted compact are dropped).
700
- db.exec(`
701
- DROP TABLE IF EXISTS nodes_new;
702
- DROP TABLE IF EXISTS links_new;
703
- ${NODES("nodes_new")};
704
- ${LINKS("links_new")};
705
- `);
706
- // Dead internal ids, sorted, for O(log n) membership. A neighbour id
707
- // always references an existing row, so "not dead" ⇔ live.
708
- const deadCount =
709
- db.prepare("SELECT COUNT(*) AS n FROM nodes WHERE deleted = 1").get().n;
710
- const dead = new Float64Array(deadCount);
711
- {
712
- let i = 0;
713
- for (
714
- const row of db.prepare(
715
- "SELECT id FROM nodes WHERE deleted = 1 ORDER BY id",
716
- ).iterate()
717
- ) {
718
- dead[i++] = row.id;
719
- }
720
- }
721
- const isDead = (id) => {
722
- let lo = 0, hi = deadCount - 1;
723
- while (lo <= hi) {
724
- const mid = (lo + hi) >> 1;
725
- const v = dead[mid];
726
- if (v === id) {
727
- return true;
728
- }
729
- if (v < id) {
730
- lo = mid + 1;
731
- } else {
732
- hi = mid - 1;
733
- }
734
- }
735
- return false;
736
- };
737
- // 1. Copy live node rows verbatim (same ids), in batched transactions so
738
- // the WAL stays bounded by the batch, not the table.
739
- const copy = db.prepare(
740
- "INSERT INTO nodes_new (id, ext, level, deleted, code) " +
741
- "SELECT id, ext, level, 0, code FROM nodes " +
742
- "WHERE deleted = 0 AND id > ? ORDER BY id LIMIT ?",
743
- );
744
- const maxCopied = db.prepare("SELECT MAX(id) AS m FROM nodes_new");
745
- const COPY_BATCH = 50_000;
746
- let cursor = 0;
747
- for (;;) {
748
- db.exec("BEGIN");
749
- const n = Number(copy.run(cursor, COPY_BATCH).changes);
750
- db.exec("COMMIT");
751
- if (n === 0) {
752
- break;
753
- }
754
- cursor = maxCopied.get().m ?? cursor;
755
- if (n < COPY_BATCH) {
756
- break;
757
- }
758
- }
759
- // 2. Rewrite live neighbour lists. A list with no dead member is copied
760
- // blob-verbatim (the common case). A dead member is replaced by its own
761
- // live neighbours at the same layer (1-hop splice), appended after the
762
- // surviving originals in ascending-id order and truncated at the layer
763
- // cap — deterministic, and the originals (chosen by the build heuristic)
764
- // always take precedence.
765
- const scan = db.prepare(
766
- "SELECT node, layer, nbrs FROM links " +
767
- "WHERE node > ? OR (node = ? AND layer > ?) " +
768
- "ORDER BY node, layer LIMIT ?",
769
- );
770
- const insNew = db.prepare(
771
- "INSERT INTO links_new (node, layer, nbrs) VALUES (?, ?, ?)",
772
- );
773
- // Small cache of dead nodes' lists — a dead hub is spliced by many of its
774
- // former neighbours in a row.
775
- const deadLists = new Map();
776
- const DEAD_CACHE_MAX = 65_536;
777
- const deadListOf = (id, layer) => {
778
- const key = id + ":" + layer;
779
- const hit = deadLists.get(key);
780
- if (hit !== undefined) {
781
- return hit;
782
- }
783
- const row = this.sNbrs.get(id, layer);
784
- const ids = row ? this.decodeNbrs(row[0]) : [];
785
- if (deadLists.size >= DEAD_CACHE_MAX) {
786
- deadLists.clear();
787
- }
788
- deadLists.set(key, ids);
789
- return ids;
790
- };
791
- const SCAN_BATCH = 20_000;
792
- let curNode = -1, curLayer = -1;
793
- for (;;) {
794
- const rows = scan.all(curNode, curNode, curLayer, SCAN_BATCH);
795
- if (rows.length === 0) {
796
- break;
797
- }
798
- db.exec("BEGIN");
799
- for (const row of rows) {
800
- curNode = row.node;
801
- curLayer = row.layer;
802
- if (isDead(row.node)) {
803
- continue; // dead nodes leave no list behind
804
- }
805
- const ids = this.decodeNbrs(row.nbrs);
806
- let hasDead = false;
807
- for (let i = 0; i < ids.length; i++) {
808
- if (isDead(ids[i])) {
809
- hasDead = true;
810
- break;
811
- }
812
- }
813
- if (!hasDead) {
814
- insNew.run(row.node, row.layer, row.nbrs);
815
- continue;
816
- }
817
- const cap = row.layer === 0 ? Mmax0 : M;
818
- const keep = [];
819
- const seen = new Set([row.node]);
820
- const deadHere = [];
821
- for (const id of ids) {
822
- if (isDead(id)) {
823
- deadHere.push(id);
824
- } else if (!seen.has(id)) {
825
- seen.add(id);
826
- keep.push(id);
827
- }
828
- }
829
- if (keep.length < cap) {
830
- // Bridge candidates: the dead members' own live neighbours, in
831
- // ascending id order for determinism.
832
- const bridge = [];
833
- for (const d of deadHere) {
834
- for (const n of deadListOf(d, row.layer)) {
835
- if (!isDead(n) && !seen.has(n)) {
836
- seen.add(n);
837
- bridge.push(n);
838
- }
839
- }
840
- }
841
- bridge.sort((a, b) => a - b);
842
- for (const b of bridge) {
843
- if (keep.length >= cap) {
844
- break;
845
- }
846
- keep.push(b);
847
- }
848
- }
849
- if (keep.length > 0) {
850
- insNew.run(row.node, row.layer, this.encodeNbrs(keep));
851
- }
852
- }
853
- db.exec("COMMIT");
854
- if (rows.length < SCAN_BATCH) {
855
- break;
856
- }
857
- }
858
- // 3. Atomic swap: the real tables flip to the compacted copies in one
859
- // transaction (the partial-index name frees up when the old table
860
- // drops, so it is recreated on the renamed table here).
861
- db.exec(`
862
- BEGIN;
863
- DROP TABLE nodes;
864
- DROP TABLE links;
865
- ALTER TABLE nodes_new RENAME TO nodes;
866
- ALTER TABLE links_new RENAME TO links;
867
- ${EXT_INDEX};
868
- COMMIT;
869
- `);
870
- this.prepareAll();
871
- const live = db.prepare("SELECT COUNT(*) AS n FROM nodes").get().n;
872
- let newEntry = entry;
873
- let maxLevel = -1;
874
- if (live === 0) {
875
- newEntry = -1;
876
- } else if (entry === -1 || isDead(entry)) {
877
- // The entry point died: promote the lowest-id node of the top level —
878
- // deterministic, corpus-determined.
879
- const top = db.prepare(
880
- "SELECT id, level FROM nodes ORDER BY level DESC, id ASC LIMIT 1",
881
- ).get();
882
- newEntry = top.id;
883
- maxLevel = top.level;
884
- } else {
885
- maxLevel =
886
- db.prepare("SELECT level FROM nodes WHERE id = ?").get(entry).level;
887
- }
888
- return { entry: newEntry, maxLevel, live };
889
- }
890
- /** Stream live external ids with internal id > `after`, in internal-id
891
- * order, in bounded batches. Internal ids are assigned monotonically and
892
- * PRESERVED by {@link spliceCompact}, so a caller can use the largest
893
- * internal id it has seen as a durable incremental watermark. */
894
- *liveExtsSince(after, batch = 1024) {
895
- const q = this.db.prepare(
896
- "SELECT id, ext FROM nodes WHERE deleted = 0 AND id > ? ORDER BY id LIMIT ?",
897
- );
898
- let cursor = after;
899
- for (;;) {
900
- const rows = q.all(cursor, batch);
901
- if (rows.length === 0) {
902
- break;
903
- }
904
- for (const r of rows) {
905
- yield { ext: r.ext, internal: r.id };
906
- }
907
- cursor = rows[rows.length - 1].id;
908
- }
909
- }
910
- vacuum() {
911
- this.db.exec("VACUUM");
912
- }
913
- close() {
914
- this.db.close();
915
- }
916
- }