@statewalker/indexer-sqlite 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -2
  3. package/dist/block-ids.d.ts +20 -0
  4. package/dist/block-ids.d.ts.map +1 -0
  5. package/dist/block-ids.js +47 -0
  6. package/dist/block-ids.js.map +1 -0
  7. package/dist/create-sqlite-indexer.d.ts +18 -0
  8. package/dist/create-sqlite-indexer.d.ts.map +1 -0
  9. package/dist/create-sqlite-indexer.js +152 -0
  10. package/dist/create-sqlite-indexer.js.map +1 -0
  11. package/dist/enumeration.d.ts +18 -0
  12. package/dist/enumeration.d.ts.map +1 -0
  13. package/dist/enumeration.js +42 -0
  14. package/dist/enumeration.js.map +1 -0
  15. package/dist/fts-provider.d.ts +6 -0
  16. package/dist/fts-provider.d.ts.map +1 -0
  17. package/dist/fts-provider.js +143 -0
  18. package/dist/fts-provider.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +8 -0
  21. package/dist/ngram-provider.d.ts +12 -0
  22. package/dist/ngram-provider.d.ts.map +1 -0
  23. package/dist/ngram-provider.js +142 -0
  24. package/dist/ngram-provider.js.map +1 -0
  25. package/dist/path-sql.d.ts +15 -0
  26. package/dist/path-sql.d.ts.map +1 -0
  27. package/dist/path-sql.js +23 -0
  28. package/dist/path-sql.js.map +1 -0
  29. package/dist/types.d.ts +24 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/vector-provider.d.ts +12 -0
  32. package/dist/vector-provider.d.ts.map +1 -0
  33. package/dist/vector-provider.js +114 -0
  34. package/dist/vector-provider.js.map +1 -0
  35. package/package.json +61 -3
  36. package/src/block-ids.ts +75 -0
  37. package/src/create-sqlite-indexer.ts +218 -0
  38. package/src/enumeration.ts +54 -0
  39. package/src/fts-provider.ts +170 -0
  40. package/src/index.ts +9 -0
  41. package/src/ngram-provider.ts +153 -0
  42. package/src/path-sql.ts +17 -0
  43. package/src/types.ts +26 -0
  44. package/src/vector-provider.ts +132 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikhail Kotelnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,240 @@
1
- # Temporary Holding Version
1
+ # @statewalker/indexer-sqlite
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ## What it is
4
+
5
+ An `Indexer` whose full-text (FTS5), vector and n-gram/grep sub-indexes all live in one SQLite
6
+ `Db` from `@statewalker/db-api`: `node:sqlite` on Node.js, sqlite-wasm in a browser worker, or a
7
+ Durable Object's `ctx.storage.sql`. Each modality is a provider you register; an Index combines
8
+ any number of named sub-indexes and fuses their results by reciprocal rank fusion (RRF).
9
+
10
+ ## Why it exists
11
+
12
+ One SQLite file holds every modality of every Index, so there is nothing to save or load: a new
13
+ indexer over the same database lists the indexes and searches the old data. Inside a Durable
14
+ Object it runs on the object's own SQLite storage, so a cold start opens the indexes where they
15
+ are, with nothing to load. The n-gram sub-index answers exact substring and regex queries (`fooBar(`, `\bTODO\b`)
16
+ that word-based full-text search cannot.
17
+
18
+ ## How to use
19
+
20
+ ```sh
21
+ pnpm add @statewalker/indexer-sqlite
22
+ ```
23
+
24
+ - `createSqliteIndexer({ db, providers }): Promise<Indexer>` — `db` is a `Db` from
25
+ `@statewalker/db-sqlite-node`, `@statewalker/db-sqlite-do` or another `@statewalker/db-sqlite*`
26
+ driver. Pass one provider per modality your indexes use; for all three:
27
+ `[newSqliteFullTextProvider(), newSqliteVectorProvider(), newSqliteNgramProvider()]`. A
28
+ sub-index type with no provider fails `createIndex` and `getIndex` (see below).
29
+ - The indexer never closes `db`. `indexer.close()` closes the open indexes; close the `Db`
30
+ yourself.
31
+ - Sub-indexes are configured, queried and read with the modality packages:
32
+ `@statewalker/indexer-fulltext`, `@statewalker/indexer-vector`, `@statewalker/indexer-ngram`
33
+ (including its `grep` helper).
34
+ - `SqliteModalityProvider`, `openBlockIds`, `newEnumeration` and `pathPrefixSql` are exported for
35
+ writing another provider.
36
+
37
+ ## Examples
38
+
39
+ Node.js, with a file database and all three modalities:
40
+
41
+ ```ts
42
+ import { newNodeSqliteDb } from "@statewalker/db-sqlite-node";
43
+ import { newFullTextAccess, setFullTextConfig } from "@statewalker/indexer-fulltext";
44
+ import { grep, newNgramAccess, setNgramConfig } from "@statewalker/indexer-ngram";
45
+ import { newVectorAccess, setVectorConfig } from "@statewalker/indexer-vector";
46
+ import {
47
+ createSqliteIndexer,
48
+ newSqliteFullTextProvider,
49
+ newSqliteNgramProvider,
50
+ newSqliteVectorProvider,
51
+ } from "@statewalker/indexer-sqlite";
52
+
53
+ const db = await newNodeSqliteDb({ path: "./search.sqlite" });
54
+ const indexer = await createSqliteIndexer({
55
+ db,
56
+ providers: [newSqliteFullTextProvider(), newSqliteVectorProvider(), newSqliteNgramProvider()],
57
+ });
58
+
59
+ const params = { name: "docs" };
60
+ setFullTextConfig(params, "text", { language: "en" });
61
+ setVectorConfig(params, "emb", { dimensionality: 3, model: "my-model" });
62
+ setNgramConfig(params, "code", { store: "fts5" });
63
+ const index = await indexer.createIndex(params);
64
+
65
+ const text = newFullTextAccess("text");
66
+ const emb = newVectorAccess("emb");
67
+ const code = newNgramAccess("code");
68
+ const block = { path: "/src/a.ts" as const, blockId: "1" };
69
+ const content = "function fooBar() {\n return running(fooBar);\n}\n";
70
+ await text.get(index).addDocument([{ ...block, content }]);
71
+ await emb.get(index).addDocument([{ ...block, embedding: Float32Array.of(0.1, 0.9, 0) }]);
72
+ await code.get(index).addDocument([{ ...block, content }]);
73
+
74
+ // One fused search over all three sub-indexes.
75
+ const request = { topK: 10 };
76
+ text.setQuery(request, { queries: ["run"] }); // Porter stemming: matches "running"
77
+ emb.setQuery(request, { embeddings: [Float32Array.of(0, 1, 0)] });
78
+ code.setQuery(request, { pattern: "fooBar" });
79
+ for await (const r of index.search(request)) {
80
+ console.log(r.path, r.blockId, r.score, code.getResult(r)?.count); // RRF score; 2 matches
81
+ }
82
+
83
+ // grep: every match with offsets and lines, plus whether the search was cut short.
84
+ const { hits, truncated } = await grep(index, "code", "fooBar\\(", { regex: true });
85
+ console.log(hits[0]?.matches, truncated); // [{ start: 9, end: 16, line: 1 }] false
86
+
87
+ await indexer.close();
88
+ await db.close();
89
+ ```
90
+
91
+ Inside a Durable Object (`newDoSqliteDb` is synchronous; the indexer is created on first use):
92
+
93
+ ```ts
94
+ import { DurableObject } from "cloudflare:workers";
95
+ import { newDoSqliteDb } from "@statewalker/db-sqlite-do";
96
+ import type { Indexer } from "@statewalker/indexer-api";
97
+ import {
98
+ createSqliteIndexer,
99
+ newSqliteFullTextProvider,
100
+ newSqliteNgramProvider,
101
+ newSqliteVectorProvider,
102
+ } from "@statewalker/indexer-sqlite";
103
+
104
+ export class Search extends DurableObject {
105
+ db = newDoSqliteDb(this.ctx.storage);
106
+ #indexer?: Promise<Indexer>;
107
+
108
+ indexer(): Promise<Indexer> {
109
+ this.#indexer ??= createSqliteIndexer({
110
+ db: this.db,
111
+ providers: [newSqliteFullTextProvider(), newSqliteVectorProvider(), newSqliteNgramProvider()],
112
+ });
113
+ return this.#indexer;
114
+ }
115
+ }
116
+ ```
117
+
118
+ In Node.js tests, `newDoSqliteDb(newFakeDoStorage())` (from `@statewalker/db-sqlite-do/fake`)
119
+ gives a `Db` with the Durable Object's limits.
120
+
121
+ ## Internals
122
+
123
+ ### Tables are prefixed per index and sub-index, and names are case-distinct
124
+
125
+ ```
126
+ __indexer_manifest(name, config) one row per Index: its sub-index configs
127
+ idx_<index>__blocks(id INTEGER PRIMARY KEY, path, block_id) shared by every sub-index
128
+ idx_<index>__<sub>_fts FTS5 table, rowid = block id
129
+ idx_<index>__<sub>_vec_* the vector engine's tables
130
+ idx_<index>__<sub>_ng_* the n-gram engine's tables
131
+ ```
132
+
133
+ Index and sub-index names are escaped into the prefixes, upper-case letters included: SQLite table names ignore case, so `Docs` and `docs` become distinct prefixes and are independent indexes. The vector and n-gram sub-indexes
134
+ compute size and listings by joining the engine's own id table with the blocks table, so nothing
135
+ lists a block the engine does not hold, and there is no second write that a crash could separate
136
+ from the engine's write.
137
+
138
+ ### Why blocks get integer ids
139
+
140
+ The engines (`@statewalker/db-sqlite-index`) index integer ids, not `{path, blockId}` pairs. The
141
+ blocks table maps one to the other, and its rows are kept until `deleteIndex`, even after a
142
+ block is deleted from every sub-index: an id stays stable for its block, so an engine never sees
143
+ one id reused for different content. Re-adding a `{path, blockId}` replaces its content; it never
144
+ creates a second entry.
145
+
146
+ ### Why each modality uses its own table prefix
147
+
148
+ The vector and n-gram engines both create a `{prefix}meta` table with different columns. Each
149
+ provider therefore appends its own suffix (`fts`, `vec_`, `ng_`) to the sub-index prefix; a new
150
+ provider must do the same.
151
+
152
+ ### Path scope uses `substr`, not `LIKE`, and one JSON parameter
153
+
154
+ A path prefix matches on component boundaries (`/foo` matches `/foo` and `/foo/x`, never
155
+ `/foobar`) and case-sensitively (`/Docs` does not match `/docs/a`). SQLite's `LIKE` ignores ASCII
156
+ case, and `PRAGMA case_sensitive_like` is not authorised in a Durable Object, so the condition is
157
+ built with `substr`. All prefixes are passed as a single JSON parameter (`json_each`), because
158
+ a Durable Object rejects statements with more than 100 bound variables; any number of `paths` works.
159
+
160
+ ### Full text runs on FTS5 with stemming for exactly "en"
161
+
162
+ - Only the exact language `"en"` gets the Porter stemmer (`porter unicode61 remove_diacritics
163
+ 2`). Every other value, including `"en-US"` and unknown codes, gets `unicode61
164
+ remove_diacritics 2` without stemming; no language throws.
165
+ - Each query string is split into words; every word with a letter or a digit is double-quoted
166
+ and the words are OR-joined, so FTS5 syntax in user text (`"`, `*`, `AND`, `NEAR(`, `-`) is
167
+ searched as plain words and never raises `fts5: syntax error`. Text with no letters or digits
168
+ finds nothing.
169
+ - `score = -bm25(...)`; the snippet is `snippet(<table>, 0, '', '', '…', 16)`.
170
+ - Several `queries` in one sub-query **add up** (SUM): a block matching more of them ranks
171
+ higher. The PGlite and DuckDB indexers keep each block's best single query instead, so the same
172
+ request can rank differently there. Each query is cut to `topK` before summing, so a block just
173
+ below the cut-off in one query loses that part of its score.
174
+
175
+ ### Vector sub-indexes use cosine scores and do not store block metadata
176
+
177
+ Cosine metric; `score = 1 − distance`. Several embeddings in one query keep each block's best
178
+ score. The engine commits an `upsert` in transactions of 200 vectors: if a call fails part-way,
179
+ the earlier batches stay written.
180
+ `VectorBlock.metadata` is not stored. `getDocumentsBlocks` returns the cosine-normalised vectors, not the embeddings that were added.
181
+
182
+ ### N-gram sub-indexes answer grep, with a bounded candidate budget
183
+
184
+ - `store` is `"fts5"` (default) or `"postings"`; both give the same results.
185
+ - `caseSensitive` defaults to `true`, as in grep. A literal pattern is matched exactly; with
186
+ `regex: true` it is a JavaScript regex without the `u` flag, and `\p{…}` / `\P{…}` are rejected
187
+ with a `SyntaxError`.
188
+ - Matches stay inside one block. Offsets are UTF-16 code units of the stored text (lone
189
+ surrogates are stored as U+FFFD), `end` exclusive; `line` is 1-based, counted from `\n`.
190
+ - `score = count`, the number of matches in the block. The engine returns its first `topK`
191
+ matching blocks (default 100) in ascending id order; the provider then ranks those by count.
192
+ When `topK` or the engine's candidate budget (10,000 candidates) cut the search short, `grep`
193
+ returns `truncated: true` and the hits are the ones found so far, not necessarily the blocks with
194
+ the most matches.
195
+ - `NgramBlock.metadata` is not stored (the full-text sub-index does store it).
196
+ - A `paths`-scoped search spends the candidate budget on out-of-scope blocks too: the engine
197
+ filters by path after it cuts to the budget, so a narrow scope in a large index can return few
198
+ hits together with `truncated: true` although more matches exist in scope.
199
+ - `addDocument` hands the engine at most 500 blocks per write transaction; each batch commits on
200
+ its own, so if a later batch fails the earlier ones stay written.
201
+
202
+ ### Dependencies: the Db, the engines and the modality contracts
203
+
204
+ - `@statewalker/db-api` — the `Db` it runs on.
205
+ - `@statewalker/db-sqlite` — `inTransaction` (FTS writes).
206
+ - `@statewalker/db-sqlite-index` — the vector (`./vector`) and n-gram (`./ngram`) engines.
207
+ - `@statewalker/indexer-api`, `@statewalker/indexer-core` — the `Indexer` contract, RRF
208
+ composition, shared helpers.
209
+ - `@statewalker/indexer-fulltext`, `@statewalker/indexer-vector`, `@statewalker/indexer-ngram` —
210
+ the modality contracts the providers implement.
211
+
212
+ ## What will surprise you
213
+
214
+ - **A wrapped `Db` loses the transaction hook.** Pass the driver's `Db` itself, not an object
215
+ that forwards to it. With a wrapper, creating an Index with a vector or n-gram sub-index, or
216
+ the first write to a full-text sub-index, rejects with:
217
+ `inTransaction: this Db has no SQLite transaction hook. It is not a @statewalker/db-sqlite* Db,
218
+ or it wraps one (a wrapper object loses the hook - pass the driver's Db itself), or it is the
219
+ handle of a running transaction (nesting is not supported).`
220
+ - **Never call an indexer, an Index or a sub-index from inside your own
221
+ `inTransaction(db, …)` callback.** The sub-indexes run their own transactions and queries on
222
+ `db`; on Node.js and in the browser those wait for your transaction to end, and it waits for
223
+ them. The call hangs, with no error.
224
+ - **A provider's `open` and `drop` must not call back into the indexer** (`getIndex`,
225
+ `createIndex`): the indexer runs them inside its own one-at-a-time queue, and a nested call waits
226
+ for itself forever.
227
+ - **Durable Object limits apply to every write.** A single string or BLOB value over about 2 MB
228
+ rejects with `string or blob too big`, so very large blocks must be split before indexing. An
229
+ operation that runs past the platform's deadline (15 s on celld) is retried rather than failed;
230
+ keep each `addDocument` call modest.
231
+ - **`truncated: true` is easy to miss.** `for await (const hit of sub.search(q))` drops the
232
+ summary the generator returns; use `grep` to read it.
233
+ - **Re-opening needs the same providers.** `getIndex` on an Index with a sub-index whose type has
234
+ no registered provider throws `Cannot load index: sub-index "<name>" requires modality
235
+ "<type>" but only [...] are registered.`; pass `{ onMissingProvider: "warn" }` to open it
236
+ without that sub-index.
237
+
238
+ ## License
239
+
240
+ MIT
@@ -0,0 +1,20 @@
1
+ import { Db } from "@statewalker/db-api";
2
+ import { BlockReference, PathSelector } from "@statewalker/indexer-api";
3
+ //#region src/block-ids.d.ts
4
+ /**
5
+ * The Index-wide `{path, blockId} ↔ integer id` table shared by every sub-index.
6
+ * Every method takes the `Db` to run on, so it works on `tx` inside `inTransaction`.
7
+ * Rows are never deleted before `deleteIndex`: an id stays stable for its block.
8
+ */
9
+ export interface BlockIds {
10
+ readonly table: string;
11
+ /** Ids for `refs`, in order; missing rows are created. */
12
+ ensure(db: Db, refs: readonly BlockReference[]): Promise<number[]>;
13
+ /** Ids of every known block matched by any selector (path prefix, or exact block). */
14
+ select(db: Db, selectors: readonly PathSelector[]): Promise<number[]>;
15
+ /** References of `ids`; unknown ids are absent. */
16
+ refs(db: Db, ids: readonly number[]): Promise<Map<number, BlockReference>>;
17
+ }
18
+ export declare function openBlockIds(db: Db, table: string): Promise<BlockIds>;
19
+ //#endregion
20
+ //# sourceMappingURL=block-ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-ids.d.ts","names":[],"sources":["../src/block-ids.ts"],"mappings":";;;;;;;;iBAUiB;WACN;;EAET,OAAO,IAAI,IAAI,eAAe,mBAAmB;;EAEjD,OAAO,IAAI,IAAI,oBAAoB,iBAAiB;;EAEpD,KAAK,IAAI,IAAI,yBAAyB,QAAQ,YAAY;;wBAGtC,aAAa,IAAI,IAAI,gBAAgB,QAAQ"}
@@ -0,0 +1,47 @@
1
+ import { pathPrefixSql } from "./path-sql.js";
2
+ import { compositeKey } from "@statewalker/indexer-core";
3
+ //#region src/block-ids.ts
4
+ async function openBlockIds(db, table) {
5
+ await db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id INTEGER PRIMARY KEY, path TEXT NOT NULL, block_id TEXT NOT NULL, UNIQUE (path, block_id))`);
6
+ return {
7
+ table,
8
+ async ensure(tx, refs) {
9
+ if (refs.length === 0) return [];
10
+ const json = JSON.stringify(refs.map((r) => [r.path, r.blockId]));
11
+ await tx.query(`INSERT INTO ${table} (path, block_id) SELECT json_extract(value, '$[0]'), json_extract(value, '$[1]') FROM json_each(?) WHERE true ON CONFLICT (path, block_id) DO NOTHING`, [json]);
12
+ const rows = await tx.query(`SELECT b.id, b.path, b.block_id FROM ${table} b JOIN json_each(?) j ON b.path = json_extract(j.value, '$[0]') AND b.block_id = json_extract(j.value, '$[1]')`, [json]);
13
+ const byKey = new Map(rows.map((r) => [compositeKey(r.path, r.block_id), Number(r.id)]));
14
+ return refs.map((r) => {
15
+ const id = byKey.get(compositeKey(r.path, r.blockId));
16
+ if (id === void 0) throw new Error(`block id missing for ${r.path}#${r.blockId}`);
17
+ return id;
18
+ });
19
+ },
20
+ async select(tx, selectors) {
21
+ const ids = /* @__PURE__ */ new Set();
22
+ for (const sel of selectors) {
23
+ const where = sel.blockId !== void 0 ? {
24
+ sql: "b.path = ? AND b.block_id = ?",
25
+ params: [sel.path, sel.blockId]
26
+ } : pathPrefixSql("b.path", [sel.path]);
27
+ const rows = await tx.query(`SELECT b.id FROM ${table} b WHERE ${where.sql}`, where.params);
28
+ for (const r of rows) ids.add(Number(r.id));
29
+ }
30
+ return [...ids];
31
+ },
32
+ async refs(tx, ids) {
33
+ const out = /* @__PURE__ */ new Map();
34
+ if (ids.length === 0) return out;
35
+ const rows = await tx.query(`SELECT id, path, block_id FROM ${table} WHERE id IN (SELECT value FROM json_each(?))`, [JSON.stringify(ids)]);
36
+ for (const r of rows) out.set(Number(r.id), {
37
+ path: r.path,
38
+ blockId: r.block_id
39
+ });
40
+ return out;
41
+ }
42
+ };
43
+ }
44
+ //#endregion
45
+ export { openBlockIds };
46
+
47
+ //# sourceMappingURL=block-ids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-ids.js","names":[],"sources":["../src/block-ids.ts"],"sourcesContent":["import type { Db } from \"@statewalker/db-api\";\nimport type { BlockReference, DocumentPath, PathSelector } from \"@statewalker/indexer-api\";\nimport { compositeKey } from \"@statewalker/indexer-core\";\nimport { pathPrefixSql } from \"./path-sql.js\";\n\n/**\n * The Index-wide `{path, blockId} ↔ integer id` table shared by every sub-index.\n * Every method takes the `Db` to run on, so it works on `tx` inside `inTransaction`.\n * Rows are never deleted before `deleteIndex`: an id stays stable for its block.\n */\nexport interface BlockIds {\n readonly table: string;\n /** Ids for `refs`, in order; missing rows are created. */\n ensure(db: Db, refs: readonly BlockReference[]): Promise<number[]>;\n /** Ids of every known block matched by any selector (path prefix, or exact block). */\n select(db: Db, selectors: readonly PathSelector[]): Promise<number[]>;\n /** References of `ids`; unknown ids are absent. */\n refs(db: Db, ids: readonly number[]): Promise<Map<number, BlockReference>>;\n}\n\nexport async function openBlockIds(db: Db, table: string): Promise<BlockIds> {\n await db.exec(\n `CREATE TABLE IF NOT EXISTS ${table} (id INTEGER PRIMARY KEY, path TEXT NOT NULL, block_id TEXT NOT NULL, UNIQUE (path, block_id))`,\n );\n return {\n table,\n async ensure(tx, refs) {\n if (refs.length === 0) return [];\n const json = JSON.stringify(refs.map((r) => [r.path, r.blockId]));\n // `WHERE true` disambiguates `SELECT … ON CONFLICT` for the SQLite parser.\n await tx.query(\n `INSERT INTO ${table} (path, block_id) SELECT json_extract(value, '$[0]'), json_extract(value, '$[1]') FROM json_each(?) WHERE true ON CONFLICT (path, block_id) DO NOTHING`,\n [json],\n );\n const rows = await tx.query<{ id: number; path: string; block_id: string }>(\n `SELECT b.id, b.path, b.block_id FROM ${table} b JOIN json_each(?) j ON b.path = json_extract(j.value, '$[0]') AND b.block_id = json_extract(j.value, '$[1]')`,\n [json],\n );\n const byKey = new Map(\n rows.map((r) => [compositeKey(r.path as DocumentPath, r.block_id), Number(r.id)]),\n );\n return refs.map((r) => {\n const id = byKey.get(compositeKey(r.path, r.blockId));\n if (id === undefined) throw new Error(`block id missing for ${r.path}#${r.blockId}`);\n return id;\n });\n },\n async select(tx, selectors) {\n const ids = new Set<number>();\n for (const sel of selectors) {\n const where =\n sel.blockId !== undefined\n ? { sql: \"b.path = ? AND b.block_id = ?\", params: [sel.path, sel.blockId] }\n : pathPrefixSql(\"b.path\", [sel.path]);\n const rows = await tx.query<{ id: number }>(\n `SELECT b.id FROM ${table} b WHERE ${where.sql}`,\n where.params,\n );\n for (const r of rows) ids.add(Number(r.id));\n }\n return [...ids];\n },\n async refs(tx, ids) {\n const out = new Map<number, BlockReference>();\n if (ids.length === 0) return out;\n const rows = await tx.query<{ id: number; path: string; block_id: string }>(\n `SELECT id, path, block_id FROM ${table} WHERE id IN (SELECT value FROM json_each(?))`,\n [JSON.stringify(ids)],\n );\n for (const r of rows)\n out.set(Number(r.id), { path: r.path as DocumentPath, blockId: r.block_id });\n return out;\n },\n };\n}\n"],"mappings":";;;AAoBA,eAAsB,aAAa,IAAQ,OAAkC;CAC3E,MAAM,GAAG,KACP,8BAA8B,MAAM,+FACtC;CACA,OAAO;EACL;EACA,MAAM,OAAO,IAAI,MAAM;GACrB,IAAI,KAAK,WAAW,GAAG,OAAO,CAAC;GAC/B,MAAM,OAAO,KAAK,UAAU,KAAK,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;GAEhE,MAAM,GAAG,MACP,eAAe,MAAM,yJACrB,CAAC,IAAI,CACP;GACA,MAAM,OAAO,MAAM,GAAG,MACpB,wCAAwC,MAAM,kHAC9C,CAAC,IAAI,CACP;GACA,MAAM,QAAQ,IAAI,IAChB,KAAK,KAAK,MAAM,CAAC,aAAa,EAAE,MAAsB,EAAE,QAAQ,GAAG,OAAO,EAAE,EAAE,CAAC,CAAC,CAClF;GACA,OAAO,KAAK,KAAK,MAAM;IACrB,MAAM,KAAK,MAAM,IAAI,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC;IACpD,IAAI,OAAO,KAAA,GAAW,MAAM,IAAI,MAAM,wBAAwB,EAAE,KAAK,GAAG,EAAE,SAAS;IACnF,OAAO;GACT,CAAC;EACH;EACA,MAAM,OAAO,IAAI,WAAW;GAC1B,MAAM,sBAAM,IAAI,IAAY;GAC5B,KAAK,MAAM,OAAO,WAAW;IAC3B,MAAM,QACJ,IAAI,YAAY,KAAA,IACZ;KAAE,KAAK;KAAiC,QAAQ,CAAC,IAAI,MAAM,IAAI,OAAO;IAAE,IACxE,cAAc,UAAU,CAAC,IAAI,IAAI,CAAC;IACxC,MAAM,OAAO,MAAM,GAAG,MACpB,oBAAoB,MAAM,WAAW,MAAM,OAC3C,MAAM,MACR;IACA,KAAK,MAAM,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,EAAE,CAAC;GAC5C;GACA,OAAO,CAAC,GAAG,GAAG;EAChB;EACA,MAAM,KAAK,IAAI,KAAK;GAClB,MAAM,sBAAM,IAAI,IAA4B;GAC5C,IAAI,IAAI,WAAW,GAAG,OAAO;GAC7B,MAAM,OAAO,MAAM,GAAG,MACpB,kCAAkC,MAAM,gDACxC,CAAC,KAAK,UAAU,GAAG,CAAC,CACtB;GACA,KAAK,MAAM,KAAK,MACd,IAAI,IAAI,OAAO,EAAE,EAAE,GAAG;IAAE,MAAM,EAAE;IAAsB,SAAS,EAAE;GAAS,CAAC;GAC7E,OAAO;EACT;CACF;AACF"}
@@ -0,0 +1,18 @@
1
+ import { SqliteModalityProvider } from "./types.js";
2
+ import { Db } from "@statewalker/db-api";
3
+ import { Indexer } from "@statewalker/indexer-api";
4
+ //#region src/create-sqlite-indexer.d.ts
5
+ export interface SqliteIndexerOptions {
6
+ /** The database. The indexer never closes it. */
7
+ db: Db;
8
+ /** One provider per modality type, e.g. FTS5, vector, n-gram. */
9
+ providers: SqliteModalityProvider[];
10
+ }
11
+ /**
12
+ * `Indexer` over one SQLite `Db`. Keeps a manifest row per Index, one shared
13
+ * `{indexPrefix}blocks` id table per Index, and opens each sub-index through
14
+ * its provider. Search fuses sub-indexes by RRF (`createCompositeIndex`).
15
+ */
16
+ export declare function createSqliteIndexer(options: SqliteIndexerOptions): Promise<Indexer>;
17
+ //#endregion
18
+ //# sourceMappingURL=create-sqlite-indexer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-sqlite-indexer.d.ts","names":[],"sources":["../src/create-sqlite-indexer.ts"],"mappings":";;;;iBAaiB;;EAEf,IAAI;;EAEJ,WAAW;;;;;;;wBAqBS,oBAAoB,SAAS,uBAAuB,QAAQ"}
@@ -0,0 +1,152 @@
1
+ import { openBlockIds } from "./block-ids.js";
2
+ import { createCompositeIndex, createSerialiser } from "@statewalker/indexer-core";
3
+ //#region src/create-sqlite-indexer.ts
4
+ const MANIFEST_TABLE = "__indexer_manifest";
5
+ /** Escapes everything but lower-case letters and digits: SQLite table names fold case, so "Docs" and "docs" must not collide. */
6
+ const sanitizePrefix = (name) => name.replace(/[^a-z0-9]/g, (ch) => `_${ch.charCodeAt(0)}_`);
7
+ const indexPrefix = (name) => `idx_${sanitizePrefix(name)}__`;
8
+ /**
9
+ * `Indexer` over one SQLite `Db`. Keeps a manifest row per Index, one shared
10
+ * `{indexPrefix}blocks` id table per Index, and opens each sub-index through
11
+ * its provider. Search fuses sub-indexes by RRF (`createCompositeIndex`).
12
+ */
13
+ async function createSqliteIndexer(options) {
14
+ const { db } = options;
15
+ const providers = new Map(options.providers.map((p) => [p.type, p]));
16
+ const indexes = /* @__PURE__ */ new Map();
17
+ const manifest = /* @__PURE__ */ new Map();
18
+ const runExclusive = createSerialiser();
19
+ let closed = false;
20
+ await db.exec(`CREATE TABLE IF NOT EXISTS ${MANIFEST_TABLE} (name TEXT PRIMARY KEY, config TEXT NOT NULL)`);
21
+ for (const row of await db.query(`SELECT name, config FROM ${MANIFEST_TABLE}`)) manifest.set(row.name, JSON.parse(row.config));
22
+ const ensureOpen = () => {
23
+ if (closed) throw new Error("Indexer is closed");
24
+ };
25
+ const contextFor = (name, sub, blockIds) => ({
26
+ db,
27
+ prefix: `${indexPrefix(name)}${sanitizePrefix(sub)}_`,
28
+ blockIds
29
+ });
30
+ async function writeManifest(name, entry) {
31
+ await db.query(`INSERT INTO ${MANIFEST_TABLE} (name, config) VALUES (?, ?) ON CONFLICT (name) DO UPDATE SET config = excluded.config`, [name, JSON.stringify(entry)]);
32
+ manifest.set(name, entry);
33
+ }
34
+ async function dropIndexTables(name, subIndexes) {
35
+ const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);
36
+ for (const [sub, spec] of Object.entries(subIndexes)) await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));
37
+ await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);
38
+ await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);
39
+ manifest.delete(name);
40
+ }
41
+ async function openIndex(name, subIndexes, options) {
42
+ const onMissing = options?.onMissingProvider ?? "throw";
43
+ const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);
44
+ const bindings = [];
45
+ const effective = {};
46
+ for (const [sub, saved] of Object.entries(subIndexes)) {
47
+ const action = options?.subIndexes?.[sub];
48
+ if (action && "skip" in action && action.skip === true) {
49
+ effective[sub] = saved;
50
+ continue;
51
+ }
52
+ const ctx = contextFor(name, sub, blockIds);
53
+ const spec = action && !("skip" in action) ? action : saved;
54
+ const provider = providers.get(spec.type);
55
+ if (provider && spec !== saved) await providers.get(saved.type)?.drop(ctx);
56
+ if (!provider) {
57
+ if (onMissing === "throw") throw new Error(`Cannot load index: sub-index "${sub}" requires modality "${spec.type}" but only [${[...providers.keys()].map((t) => `"${t}"`).join(", ")}] are registered.`);
58
+ globalThis.console?.warn(`Sub-index "${sub}" (type "${spec.type}") has no provider; skipping.`);
59
+ effective[sub] = saved;
60
+ continue;
61
+ }
62
+ const { type, ...config } = spec;
63
+ bindings.push({
64
+ name: sub,
65
+ type,
66
+ config,
67
+ index: await provider.open(ctx, config)
68
+ });
69
+ effective[sub] = spec;
70
+ }
71
+ return {
72
+ index: createCompositeIndex({
73
+ name,
74
+ bindings,
75
+ onDeleteIndex: async () => {
76
+ for (const [sub, spec] of Object.entries(manifest.get(name)?.subIndexes ?? {})) await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));
77
+ await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);
78
+ await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);
79
+ manifest.delete(name);
80
+ indexes.delete(name);
81
+ }
82
+ }),
83
+ effective
84
+ };
85
+ }
86
+ return {
87
+ async getIndexNames() {
88
+ ensureOpen();
89
+ return [...manifest.keys()].map((name) => ({ name }));
90
+ },
91
+ createIndex(params) {
92
+ return runExclusive(async () => {
93
+ ensureOpen();
94
+ const { name, subIndexes, overwrite, metadata } = params;
95
+ if (!subIndexes || Object.keys(subIndexes).length === 0) throw new Error("Cannot create index without any subIndexes");
96
+ const existing = manifest.get(name);
97
+ if (existing || indexes.has(name)) {
98
+ if (!overwrite) throw new Error(`Index "${name}" already exists`);
99
+ await indexes.get(name)?.close().catch(() => void 0);
100
+ indexes.delete(name);
101
+ await dropIndexTables(name, existing?.subIndexes ?? {});
102
+ }
103
+ const { index, effective } = await openIndex(name, subIndexes);
104
+ await writeManifest(name, { subIndexes: effective });
105
+ if (metadata !== void 0) index.metadata = metadata;
106
+ indexes.set(name, index);
107
+ return index;
108
+ });
109
+ },
110
+ getIndex(name, options) {
111
+ return runExclusive(async () => {
112
+ ensureOpen();
113
+ const cached = indexes.get(name);
114
+ if (cached) return cached;
115
+ const entry = manifest.get(name);
116
+ if (!entry) return null;
117
+ const { index, effective } = await openIndex(name, entry.subIndexes, options);
118
+ if (JSON.stringify(effective) !== JSON.stringify(entry.subIndexes)) await writeManifest(name, { subIndexes: effective });
119
+ indexes.set(name, index);
120
+ return index;
121
+ });
122
+ },
123
+ async hasIndex(name) {
124
+ ensureOpen();
125
+ return manifest.has(name);
126
+ },
127
+ async deleteIndex(name) {
128
+ ensureOpen();
129
+ const live = indexes.get(name);
130
+ if (live) {
131
+ await live.deleteIndex();
132
+ return;
133
+ }
134
+ const entry = manifest.get(name);
135
+ if (entry) await dropIndexTables(name, entry.subIndexes);
136
+ },
137
+ async flush() {
138
+ ensureOpen();
139
+ for (const index of indexes.values()) await index.flush();
140
+ },
141
+ async close() {
142
+ if (closed) return;
143
+ closed = true;
144
+ for (const index of indexes.values()) await index.close().catch(() => void 0);
145
+ indexes.clear();
146
+ }
147
+ };
148
+ }
149
+ //#endregion
150
+ export { createSqliteIndexer };
151
+
152
+ //# sourceMappingURL=create-sqlite-indexer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-sqlite-indexer.js","names":[],"sources":["../src/create-sqlite-indexer.ts"],"sourcesContent":["import type { Db } from \"@statewalker/db-api\";\nimport type {\n AnySubIndexBinding,\n CreateIndexParams,\n GetIndexOptions,\n Index,\n Indexer,\n IndexInfo,\n} from \"@statewalker/indexer-api\";\nimport { createCompositeIndex, createSerialiser } from \"@statewalker/indexer-core\";\nimport { type BlockIds, openBlockIds } from \"./block-ids.js\";\nimport type { SqliteModalityContext, SqliteModalityProvider } from \"./types.js\";\n\nexport interface SqliteIndexerOptions {\n /** The database. The indexer never closes it. */\n db: Db;\n /** One provider per modality type, e.g. FTS5, vector, n-gram. */\n providers: SqliteModalityProvider[];\n}\n\ntype SubSpec = { type: string } & Record<string, unknown>;\ninterface IndexManifest {\n subIndexes: Record<string, SubSpec>;\n}\n\nconst MANIFEST_TABLE = \"__indexer_manifest\";\n\n/** Escapes everything but lower-case letters and digits: SQLite table names fold case, so \"Docs\" and \"docs\" must not collide. */\nconst sanitizePrefix = (name: string) =>\n name.replace(/[^a-z0-9]/g, (ch) => `_${ch.charCodeAt(0)}_`);\n\nconst indexPrefix = (name: string) => `idx_${sanitizePrefix(name)}__`;\n\n/**\n * `Indexer` over one SQLite `Db`. Keeps a manifest row per Index, one shared\n * `{indexPrefix}blocks` id table per Index, and opens each sub-index through\n * its provider. Search fuses sub-indexes by RRF (`createCompositeIndex`).\n */\nexport async function createSqliteIndexer(options: SqliteIndexerOptions): Promise<Indexer> {\n const { db } = options;\n const providers = new Map(options.providers.map((p) => [p.type, p]));\n const indexes = new Map<string, Index>();\n const manifest = new Map<string, IndexManifest>();\n const runExclusive = createSerialiser();\n let closed = false;\n\n await db.exec(\n `CREATE TABLE IF NOT EXISTS ${MANIFEST_TABLE} (name TEXT PRIMARY KEY, config TEXT NOT NULL)`,\n );\n for (const row of await db.query<{ name: string; config: string }>(\n `SELECT name, config FROM ${MANIFEST_TABLE}`,\n )) {\n manifest.set(row.name, JSON.parse(row.config) as IndexManifest);\n }\n\n const ensureOpen = () => {\n if (closed) throw new Error(\"Indexer is closed\");\n };\n\n const contextFor = (name: string, sub: string, blockIds: BlockIds): SqliteModalityContext => ({\n db,\n prefix: `${indexPrefix(name)}${sanitizePrefix(sub)}_`,\n blockIds,\n });\n\n async function writeManifest(name: string, entry: IndexManifest): Promise<void> {\n await db.query(\n `INSERT INTO ${MANIFEST_TABLE} (name, config) VALUES (?, ?) ON CONFLICT (name) DO UPDATE SET config = excluded.config`,\n [name, JSON.stringify(entry)],\n );\n manifest.set(name, entry);\n }\n\n async function dropIndexTables(name: string, subIndexes: Record<string, SubSpec>): Promise<void> {\n const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);\n for (const [sub, spec] of Object.entries(subIndexes)) {\n await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));\n }\n await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);\n await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);\n manifest.delete(name);\n }\n\n async function openIndex(\n name: string,\n subIndexes: Record<string, SubSpec>,\n options?: GetIndexOptions,\n ): Promise<{ index: Index; effective: Record<string, SubSpec> }> {\n const onMissing = options?.onMissingProvider ?? \"throw\";\n const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);\n const bindings: AnySubIndexBinding[] = [];\n const effective: Record<string, SubSpec> = {};\n\n for (const [sub, saved] of Object.entries(subIndexes)) {\n const action = options?.subIndexes?.[sub];\n if (action && \"skip\" in action && action.skip === true) {\n effective[sub] = saved;\n continue;\n }\n const ctx = contextFor(name, sub, blockIds);\n const spec = action && !(\"skip\" in action) ? (action as SubSpec) : saved;\n const provider = providers.get(spec.type);\n if (provider && spec !== saved) await providers.get(saved.type)?.drop(ctx);\n if (!provider) {\n if (onMissing === \"throw\") {\n throw new Error(\n `Cannot load index: sub-index \"${sub}\" requires modality \"${spec.type}\" but only [${[\n ...providers.keys(),\n ]\n .map((t) => `\"${t}\"`)\n .join(\", \")}] are registered.`,\n );\n }\n const g = globalThis as { console?: { warn(...args: unknown[]): void } };\n g.console?.warn(`Sub-index \"${sub}\" (type \"${spec.type}\") has no provider; skipping.`);\n effective[sub] = saved;\n continue;\n }\n const { type, ...config } = spec;\n bindings.push({ name: sub, type, config, index: await provider.open(ctx, config) });\n effective[sub] = spec;\n }\n\n const index = createCompositeIndex({\n name,\n bindings,\n onDeleteIndex: async () => {\n // Bound sub-indexes dropped themselves; skipped ones still own tables.\n for (const [sub, spec] of Object.entries(manifest.get(name)?.subIndexes ?? {})) {\n await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));\n }\n await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);\n await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);\n manifest.delete(name);\n indexes.delete(name);\n },\n });\n return { index, effective };\n }\n\n return {\n async getIndexNames(): Promise<IndexInfo[]> {\n ensureOpen();\n return [...manifest.keys()].map((name) => ({ name }));\n },\n\n createIndex(params: CreateIndexParams): Promise<Index> {\n return runExclusive(async () => {\n ensureOpen();\n const { name, subIndexes, overwrite, metadata } = params;\n if (!subIndexes || Object.keys(subIndexes).length === 0) {\n throw new Error(\"Cannot create index without any subIndexes\");\n }\n const existing = manifest.get(name);\n if (existing || indexes.has(name)) {\n if (!overwrite) throw new Error(`Index \"${name}\" already exists`);\n await indexes\n .get(name)\n ?.close()\n .catch(() => undefined);\n indexes.delete(name);\n await dropIndexTables(name, existing?.subIndexes ?? {});\n }\n const { index, effective } = await openIndex(name, subIndexes);\n await writeManifest(name, { subIndexes: effective });\n if (metadata !== undefined) (index as { metadata?: unknown }).metadata = metadata;\n indexes.set(name, index);\n return index;\n });\n },\n\n getIndex(name: string, options?: GetIndexOptions): Promise<Index | null> {\n // Serialised with createIndex so concurrent first opens open once\n // (a second reinit would otherwise drop tables the first just wrote).\n return runExclusive(async () => {\n ensureOpen();\n const cached = indexes.get(name);\n if (cached) return cached;\n const entry = manifest.get(name);\n if (!entry) return null;\n const { index, effective } = await openIndex(name, entry.subIndexes, options);\n if (JSON.stringify(effective) !== JSON.stringify(entry.subIndexes)) {\n await writeManifest(name, { subIndexes: effective });\n }\n indexes.set(name, index);\n return index;\n });\n },\n\n async hasIndex(name: string): Promise<boolean> {\n ensureOpen();\n return manifest.has(name);\n },\n\n async deleteIndex(name: string): Promise<void> {\n ensureOpen();\n const live = indexes.get(name);\n if (live) {\n await live.deleteIndex();\n return;\n }\n const entry = manifest.get(name);\n if (entry) await dropIndexTables(name, entry.subIndexes);\n },\n\n async flush(): Promise<void> {\n ensureOpen();\n for (const index of indexes.values()) await index.flush();\n },\n\n async close(): Promise<void> {\n if (closed) return;\n closed = true;\n for (const index of indexes.values()) await index.close().catch(() => undefined);\n indexes.clear();\n },\n } satisfies Indexer;\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { BlockIds } from "./block-ids.js";
2
+ import { Db } from "@statewalker/db-api";
3
+ import { BlockReference, DocumentPath } from "@statewalker/indexer-api";
4
+ //#region src/enumeration.d.ts
5
+ export interface BlockEntry extends BlockReference {
6
+ id: number;
7
+ }
8
+ /** `SearchIndex` enumeration over a sub-index's member ids joined with the blocks table. */
9
+ export interface Enumeration {
10
+ size(prefix?: DocumentPath): Promise<number>;
11
+ entries(prefix?: DocumentPath): Promise<BlockEntry[]>;
12
+ paths(prefix?: DocumentPath): AsyncGenerator<DocumentPath>;
13
+ refs(prefix?: DocumentPath): AsyncGenerator<BlockReference>;
14
+ }
15
+ /** `memberIdsSql` is a `SELECT … AS id` listing the sub-index's ids, e.g. `SELECT rowid AS id FROM t`. */
16
+ export declare function newEnumeration(db: Db, blockIds: BlockIds, memberIdsSql: string): Enumeration;
17
+ //#endregion
18
+ //# sourceMappingURL=enumeration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enumeration.d.ts","names":[],"sources":["../src/enumeration.ts"],"mappings":";;;;iBAKiB,mBAAmB;EAClC;;;iBAIe;EACf,KAAK,SAAS,eAAe;EAC7B,QAAQ,SAAS,eAAe,QAAQ;EACxC,MAAM,SAAS,eAAe,eAAe;EAC7C,KAAK,SAAS,eAAe,eAAe;;;wBAI9B,eAAe,IAAI,IAAI,UAAU,UAAU,uBAAuB"}
@@ -0,0 +1,42 @@
1
+ import { pathPrefixSql } from "./path-sql.js";
2
+ //#region src/enumeration.ts
3
+ /** `memberIdsSql` is a `SELECT … AS id` listing the sub-index's ids, e.g. `SELECT rowid AS id FROM t`. */
4
+ function newEnumeration(db, blockIds, memberIdsSql) {
5
+ const from = (prefix) => {
6
+ const scope = pathPrefixSql("b.path", prefix ? [prefix] : []);
7
+ return {
8
+ sql: `FROM (${memberIdsSql}) m JOIN ${blockIds.table} b ON b.id = m.id WHERE ${scope.sql}`,
9
+ params: scope.params
10
+ };
11
+ };
12
+ return {
13
+ async size(prefix) {
14
+ const f = from(prefix);
15
+ const rows = await db.query(`SELECT COUNT(*) AS n ${f.sql}`, f.params);
16
+ return Number(rows[0]?.n ?? 0);
17
+ },
18
+ async entries(prefix) {
19
+ const f = from(prefix);
20
+ return (await db.query(`SELECT b.id, b.path, b.block_id ${f.sql} ORDER BY b.id`, f.params)).map((r) => ({
21
+ id: Number(r.id),
22
+ path: r.path,
23
+ blockId: r.block_id
24
+ }));
25
+ },
26
+ async *paths(prefix) {
27
+ const f = from(prefix);
28
+ const rows = await db.query(`SELECT DISTINCT b.path ${f.sql}`, f.params);
29
+ for (const r of rows) yield r.path;
30
+ },
31
+ async *refs(prefix) {
32
+ for (const e of await this.entries(prefix)) yield {
33
+ path: e.path,
34
+ blockId: e.blockId
35
+ };
36
+ }
37
+ };
38
+ }
39
+ //#endregion
40
+ export { newEnumeration };
41
+
42
+ //# sourceMappingURL=enumeration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enumeration.js","names":[],"sources":["../src/enumeration.ts"],"sourcesContent":["import type { Db } from \"@statewalker/db-api\";\nimport type { BlockReference, DocumentPath } from \"@statewalker/indexer-api\";\nimport type { BlockIds } from \"./block-ids.js\";\nimport { pathPrefixSql } from \"./path-sql.js\";\n\nexport interface BlockEntry extends BlockReference {\n id: number;\n}\n\n/** `SearchIndex` enumeration over a sub-index's member ids joined with the blocks table. */\nexport interface Enumeration {\n size(prefix?: DocumentPath): Promise<number>;\n entries(prefix?: DocumentPath): Promise<BlockEntry[]>;\n paths(prefix?: DocumentPath): AsyncGenerator<DocumentPath>;\n refs(prefix?: DocumentPath): AsyncGenerator<BlockReference>;\n}\n\n/** `memberIdsSql` is a `SELECT … AS id` listing the sub-index's ids, e.g. `SELECT rowid AS id FROM t`. */\nexport function newEnumeration(db: Db, blockIds: BlockIds, memberIdsSql: string): Enumeration {\n const from = (prefix?: DocumentPath) => {\n const scope = pathPrefixSql(\"b.path\", prefix ? [prefix] : []);\n return {\n sql: `FROM (${memberIdsSql}) m JOIN ${blockIds.table} b ON b.id = m.id WHERE ${scope.sql}`,\n params: scope.params,\n };\n };\n return {\n async size(prefix) {\n const f = from(prefix);\n const rows = await db.query<{ n: number }>(`SELECT COUNT(*) AS n ${f.sql}`, f.params);\n return Number(rows[0]?.n ?? 0);\n },\n async entries(prefix) {\n const f = from(prefix);\n const rows = await db.query<{ id: number; path: string; block_id: string }>(\n `SELECT b.id, b.path, b.block_id ${f.sql} ORDER BY b.id`,\n f.params,\n );\n return rows.map((r) => ({\n id: Number(r.id),\n path: r.path as DocumentPath,\n blockId: r.block_id,\n }));\n },\n async *paths(prefix) {\n const f = from(prefix);\n const rows = await db.query<{ path: string }>(`SELECT DISTINCT b.path ${f.sql}`, f.params);\n for (const r of rows) yield r.path as DocumentPath;\n },\n async *refs(prefix) {\n for (const e of await this.entries(prefix)) yield { path: e.path, blockId: e.blockId };\n },\n };\n}\n"],"mappings":";;;AAkBA,SAAgB,eAAe,IAAQ,UAAoB,cAAmC;CAC5F,MAAM,QAAQ,WAA0B;EACtC,MAAM,QAAQ,cAAc,UAAU,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC;EAC5D,OAAO;GACL,KAAK,SAAS,aAAa,WAAW,SAAS,MAAM,0BAA0B,MAAM;GACrF,QAAQ,MAAM;EAChB;CACF;CACA,OAAO;EACL,MAAM,KAAK,QAAQ;GACjB,MAAM,IAAI,KAAK,MAAM;GACrB,MAAM,OAAO,MAAM,GAAG,MAAqB,wBAAwB,EAAE,OAAO,EAAE,MAAM;GACpF,OAAO,OAAO,KAAK,EAAE,EAAE,KAAK,CAAC;EAC/B;EACA,MAAM,QAAQ,QAAQ;GACpB,MAAM,IAAI,KAAK,MAAM;GAKrB,QAAO,MAJY,GAAG,MACpB,mCAAmC,EAAE,IAAI,iBACzC,EAAE,MACJ,EAAA,CACY,KAAK,OAAO;IACtB,IAAI,OAAO,EAAE,EAAE;IACf,MAAM,EAAE;IACR,SAAS,EAAE;GACb,EAAE;EACJ;EACA,OAAO,MAAM,QAAQ;GACnB,MAAM,IAAI,KAAK,MAAM;GACrB,MAAM,OAAO,MAAM,GAAG,MAAwB,0BAA0B,EAAE,OAAO,EAAE,MAAM;GACzF,KAAK,MAAM,KAAK,MAAM,MAAM,EAAE;EAChC;EACA,OAAO,KAAK,QAAQ;GAClB,KAAK,MAAM,KAAK,MAAM,KAAK,QAAQ,MAAM,GAAG,MAAM;IAAE,MAAM,EAAE;IAAM,SAAS,EAAE;GAAQ;EACvF;CACF;AACF"}
@@ -0,0 +1,6 @@
1
+ import { SqliteModalityProvider } from "./types.js";
2
+ import { FullTextConfig, FullTextIndex } from "@statewalker/indexer-fulltext";
3
+ //#region src/fts-provider.d.ts
4
+ export declare function newSqliteFullTextProvider(): SqliteModalityProvider<FullTextConfig, FullTextIndex>;
5
+ //#endregion
6
+ //# sourceMappingURL=fts-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fts-provider.d.ts","names":[],"sources":["../src/fts-provider.ts"],"mappings":";;;wBAmCgB,6BAA6B,uBAAuB,gBAAgB"}