@remnic/coding-graph 9.3.759
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/dist/chunk-5I2DBHOQ.js +1042 -0
- package/dist/chunk-5I2DBHOQ.js.map +1 -0
- package/dist/chunk-CPYJACC5.js +1838 -0
- package/dist/chunk-CPYJACC5.js.map +1 -0
- package/dist/chunk-ZVCMIM4T.js +216 -0
- package/dist/chunk-ZVCMIM4T.js.map +1 -0
- package/dist/cypher/query-parser.d.ts +253 -0
- package/dist/cypher/query-parser.js +17 -0
- package/dist/cypher/query-parser.js.map +1 -0
- package/dist/graph-schema.d.ts +84 -0
- package/dist/graph-schema.js +17 -0
- package/dist/graph-schema.js.map +1 -0
- package/dist/graph-store.d.ts +938 -0
- package/dist/graph-store.js +16 -0
- package/dist/graph-store.js.map +1 -0
- package/dist/index.d.ts +1953 -0
- package/dist/index.js +3509 -0
- package/dist/index.js.map +1 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +79 -0
- package/src/co-change.test.ts +175 -0
- package/src/co-change.ts +167 -0
- package/src/cypher/query-parser.test.ts +1107 -0
- package/src/cypher/query-parser.ts +1692 -0
- package/src/detect-changes.test.ts +533 -0
- package/src/detect-changes.ts +367 -0
- package/src/engine/emit.ts +556 -0
- package/src/engine/engine.test.ts +1417 -0
- package/src/engine/engine.ts +182 -0
- package/src/engine/extractors.ts +486 -0
- package/src/engine/fixtures.ts +364 -0
- package/src/engine/language-sniff.ts +56 -0
- package/src/engine/parser-backend.ts +206 -0
- package/src/engine/utf16-offsets.ts +68 -0
- package/src/git-invoker.test.ts +116 -0
- package/src/git-invoker.ts +426 -0
- package/src/graph-schema.test.ts +541 -0
- package/src/graph-schema.ts +383 -0
- package/src/graph-store-pr2.test.ts +1879 -0
- package/src/graph-store.test.ts +1420 -0
- package/src/graph-store.ts +3489 -0
- package/src/index-status.test.ts +303 -0
- package/src/index-status.ts +135 -0
- package/src/index.ts +384 -0
- package/src/lsp/byte-position.ts +173 -0
- package/src/lsp/characterization.test.ts +174 -0
- package/src/lsp/client.test.ts +275 -0
- package/src/lsp/client.ts +484 -0
- package/src/lsp/config.ts +219 -0
- package/src/lsp/degradation.ts +86 -0
- package/src/lsp/fixtures/fake-server.mjs +198 -0
- package/src/lsp/framing.test.ts +180 -0
- package/src/lsp/framing.ts +177 -0
- package/src/lsp/resolution.test.ts +497 -0
- package/src/lsp/resolution.ts +483 -0
- package/src/lsp/status.ts +140 -0
- package/src/lsp/types.ts +167 -0
- package/src/reindex.test.ts +1038 -0
- package/src/reindex.ts +908 -0
- package/src/row-types.ts +45 -0
- package/src/semantic/canonical-text.test.ts +150 -0
- package/src/semantic/canonical-text.ts +219 -0
- package/src/semantic/config.ts +235 -0
- package/src/semantic/index.ts +78 -0
- package/src/semantic/minhash.test.ts +197 -0
- package/src/semantic/minhash.ts +261 -0
- package/src/semantic/semantic-query.ts +173 -0
- package/src/semantic/semantic.test.ts +1315 -0
- package/src/semantic/similarity.ts +268 -0
- package/src/semantic/types.ts +145 -0
- package/src/semantic/vectors.ts +235 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding-graph SQLite schema — versioned meta + tables + FTS5 virtual table.
|
|
3
|
+
*
|
|
4
|
+
* Issue #1552 PR1 (Track B, Phase 1). The schema + write pipeline only;
|
|
5
|
+
* traversal, search, dead-code and the openCypher subset land in PR2/PR3.
|
|
6
|
+
*
|
|
7
|
+
* PR2 additive table (`node_attributes`): tracks per-node exclusion flags
|
|
8
|
+
* consumed by `deadCode()` — `is_exported`, `is_route_handler`. Added in
|
|
9
|
+
* PR2 (issue #1552 step 5) as a SEPARATE table rather than ALTER TABLE on
|
|
10
|
+
* `nodes`, so existing v1 databases gain the table via the same
|
|
11
|
+
* `CREATE TABLE IF NOT EXISTS` pass without a schema-version bump or a
|
|
12
|
+
* migration (rule 23 — additive, characterized before moving).
|
|
13
|
+
*
|
|
14
|
+
* Design anchors:
|
|
15
|
+
* - `packages/remnic-core/src/lcm/schema.ts` (versioning + pragmas) —
|
|
16
|
+
* copied VERBATIM for the WAL / busy_timeout / synchronous pragmas and
|
|
17
|
+
* the meta-table version row. Do not invent a new pattern (rule 23/38).
|
|
18
|
+
* - `packages/remnic-core/src/runtime/better-sqlite.ts` (`openBetterSqlite3`)
|
|
19
|
+
* — the shared opener; native-binding lifecycle is paid for once there.
|
|
20
|
+
* - issue://1552 "Design" — node ids hash sorted key material; file
|
|
21
|
+
* contents are NEVER stored (only spans + content hashes); FTS5 covers
|
|
22
|
+
* node names; provenance CHECK enforces the four-value enum.
|
|
23
|
+
*
|
|
24
|
+
* Schema versioning:
|
|
25
|
+
* The schema_version row lives in `meta` (a generic key/value table, not
|
|
26
|
+
* lcm_meta — independent store, separate version namespace). Fresh DB →
|
|
27
|
+
* schema_version=1. Upgrade stub: meta v0 → v1 (rule 23, characterize
|
|
28
|
+
* before moving).
|
|
29
|
+
*
|
|
30
|
+
* Dangling-edge policy (PR1 decision):
|
|
31
|
+
* When `upsertFileBatch` deletes a file's prior nodes + edges, cross-file
|
|
32
|
+
* edges whose `dst` was a node owned by the deleted file become dangling.
|
|
33
|
+
* We DROP them. They are counted in `UpsertResult.droppedDanglingEdges`
|
|
34
|
+
* so callers can surface the loss. Keeping them with a `dst_unresolved`
|
|
35
|
+
* marker would leak orphans and bias `traverse()` results — drop is the
|
|
36
|
+
* conservative choice for a write pipeline whose caller knows the
|
|
37
|
+
* canonical file set on each batch (rule 11, 40).
|
|
38
|
+
*/
|
|
39
|
+
import type { BetterSqlite3Database } from "@remnic/core/runtime/better-sqlite";
|
|
40
|
+
|
|
41
|
+
import { expectRow, expectRows } from "./row-types.js";
|
|
42
|
+
|
|
43
|
+
export const CODING_GRAPH_SCHEMA_VERSION = 1;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* FTS5 rowid derived from a deterministic node id. FTS5 rowids are
|
|
47
|
+
* signed 64-bit integers; we slice the leading 16 hex chars (= 64 bits)
|
|
48
|
+
* of the sha256 id and mask to the int64 positive range so SQLite
|
|
49
|
+
* accepts it. The full 64-bit space is large enough that collisions
|
|
50
|
+
* across distinct node ids are negligible. Contentless FTS5
|
|
51
|
+
* (`content=''`) does NOT store UNINDEXED column values, so the only
|
|
52
|
+
* reliable key into the virtual table is the rowid.
|
|
53
|
+
*
|
|
54
|
+
* Lives in graph-schema (not graph-store) so the schema migration path
|
|
55
|
+
* can rebuild FTS rows from existing `nodes` without importing the
|
|
56
|
+
* store module (which would create a circular dependency — graph-store
|
|
57
|
+
* imports graph-schema).
|
|
58
|
+
*/
|
|
59
|
+
export function ftsRowidForNodeId(nodeId: string): bigint {
|
|
60
|
+
return BigInt(`0x${nodeId.slice(0, 16)}`) & BigInt("0x7fffffffffffffff");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Provenance enum for edges — mirrors #1552's `heuristic|lsp|trace|semantic`
|
|
65
|
+
* whitelist. The CHECK constraint rejects writes outside this set so a
|
|
66
|
+
* buggy resolver can't sneak in unknown values (rule 23).
|
|
67
|
+
*/
|
|
68
|
+
export const EDGE_PROVENANCE_VALUES = [
|
|
69
|
+
"heuristic",
|
|
70
|
+
"lsp",
|
|
71
|
+
"trace",
|
|
72
|
+
"semantic",
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
export type EdgeProvenance = (typeof EDGE_PROVENANCE_VALUES)[number];
|
|
76
|
+
|
|
77
|
+
export function isEdgeProvenance(value: unknown): value is EdgeProvenance {
|
|
78
|
+
return (
|
|
79
|
+
typeof value === "string" &&
|
|
80
|
+
(EDGE_PROVENANCE_VALUES as readonly string[]).includes(value)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply (or upgrade) the coding-graph schema on an already-open SQLite
|
|
86
|
+
* handle. Public so test seams and migration tools can bootstrap an
|
|
87
|
+
* in-memory database without going through {@link openCodingGraphDatabase}.
|
|
88
|
+
*
|
|
89
|
+
* Mirrors `applyLcmSchema` in `packages/remnic-core/src/lcm/schema.ts` —
|
|
90
|
+
* distinct function, same shape, separate version namespace.
|
|
91
|
+
*/
|
|
92
|
+
export function applyCodingGraphSchema(db: BetterSqlite3Database): void {
|
|
93
|
+
const versionRow = expectRow<{ name: string }>(
|
|
94
|
+
db
|
|
95
|
+
.prepare(
|
|
96
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='meta'",
|
|
97
|
+
)
|
|
98
|
+
.get(),
|
|
99
|
+
["name"],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!versionRow) {
|
|
103
|
+
// Fresh DB — create every table and stamp the current version.
|
|
104
|
+
createTables(db);
|
|
105
|
+
writeSchemaVersion(db, CODING_GRAPH_SCHEMA_VERSION);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const meta = expectRow<{ value: string }>(
|
|
110
|
+
db
|
|
111
|
+
.prepare("SELECT value FROM meta WHERE key = 'schema_version'")
|
|
112
|
+
.get(),
|
|
113
|
+
["value"],
|
|
114
|
+
);
|
|
115
|
+
const currentVersion = meta ? parseInt(meta.value, 10) : 0;
|
|
116
|
+
|
|
117
|
+
// Only run createTables when the on-disk version is at or below this
|
|
118
|
+
// code's version. For an AT-OR-BELOW DB (fresh-ish v0/v1) the pass is
|
|
119
|
+
// additive: every core statement is CREATE TABLE IF NOT EXISTS, so the
|
|
120
|
+
// PR2 `node_attributes` table appears on existing v1 databases without
|
|
121
|
+
// a version bump (chatgpt-codex-connector P1: 'Create node_attributes
|
|
122
|
+
// for existing v1 stores'), and a v0 DB is upgraded.
|
|
123
|
+
//
|
|
124
|
+
// A NEWER DB (currentVersion > CODING_GRAPH_SCHEMA_VERSION — older
|
|
125
|
+
// code opening a future-version DB after a downgrade, or a parallel
|
|
126
|
+
// install) must be left UNTOUCHED: createTables is NOT purely
|
|
127
|
+
// additive because its FTS migration drops + recreates `nodes_fts`
|
|
128
|
+
// when the stored CREATE SQL lacks `contentless_delete=1`. Running
|
|
129
|
+
// that against a future schema that legitimately changed or removed
|
|
130
|
+
// that table would mutate the newer schema while preserving its
|
|
131
|
+
// version marker — silent corruption. Skip createTables AND the
|
|
132
|
+
// version write for newer DBs (chatgpt-codex-connector P2: 'Skip
|
|
133
|
+
// destructive DDL for future schema versions').
|
|
134
|
+
if (currentVersion > CODING_GRAPH_SCHEMA_VERSION) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
createTables(db);
|
|
138
|
+
writeSchemaVersion(db, CODING_GRAPH_SCHEMA_VERSION);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createTables(db: BetterSqlite3Database): void {
|
|
142
|
+
// Provenance whitelist must match EDGE_PROVENANCE_VALUES. SQLite CHECK
|
|
143
|
+
// constraints are re-checked against every INSERT/UPDATE; bypassing this
|
|
144
|
+
// gate would require raw exec, which we never do (rule 51).
|
|
145
|
+
const provenanceList = EDGE_PROVENANCE_VALUES.map((v) => `'${v}'`).join(", ");
|
|
146
|
+
|
|
147
|
+
db.exec(`
|
|
148
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
149
|
+
key TEXT PRIMARY KEY,
|
|
150
|
+
value TEXT NOT NULL
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
154
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
155
|
+
path TEXT NOT NULL UNIQUE,
|
|
156
|
+
lang TEXT NOT NULL,
|
|
157
|
+
content_hash TEXT NOT NULL
|
|
158
|
+
);
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
|
|
160
|
+
|
|
161
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
162
|
+
id TEXT PRIMARY KEY,
|
|
163
|
+
label TEXT NOT NULL,
|
|
164
|
+
name TEXT NOT NULL,
|
|
165
|
+
qualified_name TEXT NOT NULL,
|
|
166
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
167
|
+
span_start INTEGER NOT NULL,
|
|
168
|
+
span_end INTEGER NOT NULL,
|
|
169
|
+
lang TEXT NOT NULL
|
|
170
|
+
);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file_id);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_qname ON nodes(qualified_name);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label);
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
176
|
+
src TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
177
|
+
dst TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
178
|
+
type TEXT NOT NULL,
|
|
179
|
+
confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0),
|
|
180
|
+
provenance TEXT NOT NULL CHECK (provenance IN (${provenanceList})),
|
|
181
|
+
UNIQUE (src, dst, type)
|
|
182
|
+
);
|
|
183
|
+
-- Destination-leading index. The UNIQUE(src,dst,type) key is
|
|
184
|
+
-- src-leading, so pruneFileNodes()'s 'WHERE dst IN (...)' count and
|
|
185
|
+
-- the ON DELETE CASCADE that follows a node delete (SQLite must
|
|
186
|
+
-- locate child edges by 'dst') would otherwise scan the whole edges
|
|
187
|
+
-- table. A dst-leading index turns ordinary symbol deletion into an
|
|
188
|
+
-- index lookup instead of a full scan
|
|
189
|
+
-- (chatgpt-codex-connector P2: 'Add an index for edge destination
|
|
190
|
+
-- lookups').
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst);
|
|
192
|
+
`);
|
|
193
|
+
// PR2 (issue #1552 step 5): per-node exclusion flags consumed by
|
|
194
|
+
// `GraphStore.deadCode()`. Kept in a SEPARATE table rather than an
|
|
195
|
+
// ALTER TABLE on `nodes` so existing v1 databases gain the table via
|
|
196
|
+
// the same `CREATE TABLE IF NOT EXISTS` pass without a schema-version
|
|
197
|
+
// bump or a data migration (rule 23 — additive, characterized before
|
|
198
|
+
// moving). ON DELETE CASCADE on `nodes(id)` keeps the table in lockstep
|
|
199
|
+
// with node lifetimes; the foreign_keys=ON pragma set in
|
|
200
|
+
// `GraphStore.open()` enforces it.
|
|
201
|
+
//
|
|
202
|
+
// `is_exported`: 1 when the symbol's `name` matches an entry in the
|
|
203
|
+
// FileIR's `exports` list (matched per-file at write time). The
|
|
204
|
+
// dead-code query treats exported symbols as not-dead even with
|
|
205
|
+
// zero inbound CALLS/USES_TYPE edges — they form the package's
|
|
206
|
+
// public surface and may be called by external consumers the graph
|
|
207
|
+
// cannot see.
|
|
208
|
+
// `is_route_handler`: 1 when the symbol's `qualified_name` matches a
|
|
209
|
+
// route's `handlerQualifiedName` in the FileIR's `routes` list.
|
|
210
|
+
// Route handlers are reachable from HTTP requests regardless of
|
|
211
|
+
// whether any other node CALLS them inside the indexed codebase.
|
|
212
|
+
//
|
|
213
|
+
// Both columns are NOT NULL with CHECK IN (0,1): a missing row means
|
|
214
|
+
// "neither flag set" (the LEFT JOIN in deadCode() COALESCEs to 0).
|
|
215
|
+
db.exec(`
|
|
216
|
+
CREATE TABLE IF NOT EXISTS node_attributes (
|
|
217
|
+
node_id TEXT PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
|
|
218
|
+
is_exported INTEGER NOT NULL CHECK (is_exported IN (0, 1)),
|
|
219
|
+
is_route_handler INTEGER NOT NULL CHECK (is_route_handler IN (0, 1))
|
|
220
|
+
);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_node_attributes_exported
|
|
222
|
+
ON node_attributes(is_exported) WHERE is_exported = 1;
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_node_attributes_route
|
|
224
|
+
ON node_attributes(is_route_handler) WHERE is_route_handler = 1;
|
|
225
|
+
`);
|
|
226
|
+
// PR3 (issue #1553): co-change edges — file-level relationships mined
|
|
227
|
+
// from git history. Stored separately from the symbol-level `edges`
|
|
228
|
+
// table because co-change is a file-to-file concern, not symbol-to-
|
|
229
|
+
// symbol. Additive to v1 (CREATE TABLE IF NOT EXISTS — same pattern
|
|
230
|
+
// as `node_attributes` in PR2, rule 23).
|
|
231
|
+
//
|
|
232
|
+
// UNIQUE(file_a, file_b) ensures idempotent upserts; the mining
|
|
233
|
+
// pipeline clears + repopulates each run so stale edges from history
|
|
234
|
+
// changes are pruned automatically.
|
|
235
|
+
db.exec(`
|
|
236
|
+
CREATE TABLE IF NOT EXISTS co_changes (
|
|
237
|
+
file_a TEXT NOT NULL,
|
|
238
|
+
file_b TEXT NOT NULL,
|
|
239
|
+
support INTEGER NOT NULL CHECK (support >= 0),
|
|
240
|
+
confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0),
|
|
241
|
+
UNIQUE (file_a, file_b)
|
|
242
|
+
);
|
|
243
|
+
CREATE INDEX IF NOT EXISTS idx_co_changes_a ON co_changes(file_a);
|
|
244
|
+
CREATE INDEX IF NOT EXISTS idx_co_changes_b ON co_changes(file_b);
|
|
245
|
+
`);
|
|
246
|
+
// Semantic layer (issue #1556): symbol embedding vectors. Additive to v1
|
|
247
|
+
// (CREATE TABLE IF NOT EXISTS — same rule-23 pattern as node_attributes).
|
|
248
|
+
// One row per (node_id, model_id): a node re-embedded under a different
|
|
249
|
+
// provider/model gets a distinct row so a provider swap does not
|
|
250
|
+
// overwrite the prior vectors (the cache invalidation test covers this).
|
|
251
|
+
// content_hash is the canonical-text hash (rule 37) — a re-index compares
|
|
252
|
+
// it to decide whether to re-embed. CASCADE on nodes(id) keeps the table
|
|
253
|
+
// in lockstep with node lifetimes (foreign_keys=ON is set in GraphStore.open).
|
|
254
|
+
db.exec(`
|
|
255
|
+
CREATE TABLE IF NOT EXISTS symbol_vectors (
|
|
256
|
+
node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
257
|
+
model_id TEXT NOT NULL,
|
|
258
|
+
content_hash TEXT NOT NULL,
|
|
259
|
+
dims INTEGER NOT NULL CHECK (dims > 0),
|
|
260
|
+
vector BLOB NOT NULL,
|
|
261
|
+
PRIMARY KEY (node_id, model_id)
|
|
262
|
+
);
|
|
263
|
+
CREATE INDEX IF NOT EXISTS idx_symbol_vectors_model
|
|
264
|
+
ON symbol_vectors(model_id);
|
|
265
|
+
`);
|
|
266
|
+
// repopulate both tables in lockstep without ordering concerns.
|
|
267
|
+
db.exec(`
|
|
268
|
+
CREATE TABLE IF NOT EXISTS fts_index (
|
|
269
|
+
fts_rowid INTEGER PRIMARY KEY,
|
|
270
|
+
node_id TEXT NOT NULL UNIQUE
|
|
271
|
+
);
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_fts_index_node ON fts_index(node_id);
|
|
273
|
+
`);
|
|
274
|
+
// FTS5 virtual table over node names + qualified_names for PR2's
|
|
275
|
+
// name search. Contentless-delete mode (`content=''` plus
|
|
276
|
+
// `contentless_delete=1`, SQLite 3.43+) lets us run standard
|
|
277
|
+
// `DELETE` and `INSERT OR REPLACE` statements against the virtual
|
|
278
|
+
// table — the write pipeline's only requirement. The `id UNINDEXED`
|
|
279
|
+
// column mirrors the deterministic node id for human-readable
|
|
280
|
+
// inspection; the write pipeline's actual key is the rowid derived
|
|
281
|
+
// from the node-id hash (see `ftsRowidForNodeId` below).
|
|
282
|
+
//
|
|
283
|
+
// Migration: databases created before the contentless-FTS5 fix have
|
|
284
|
+
// an OLD `nodes_fts` table whose CREATE SQL used `content=nodes`
|
|
285
|
+
// (external-content) and lacks `contentless_delete=1`. The write
|
|
286
|
+
// pipeline's `DELETE FROM nodes_fts WHERE rowid = ?` fails on such
|
|
287
|
+
// a table while the source `nodes` row still exists, throwing and
|
|
288
|
+
// aborting the whole batch (kilo WARNING: 'Contentless FTS5 assumes
|
|
289
|
+
// fresh schema — old databases are not migrated'). We detect this
|
|
290
|
+
// by inspecting sqlite_master for the `contentless_delete=1` token;
|
|
291
|
+
// if absent, we DROP + RECREATE the virtual table and rebuild its
|
|
292
|
+
// rows from the surviving `nodes` table so the index is immediately
|
|
293
|
+
// usable after migration (rule 23: characterize before moving).
|
|
294
|
+
const ftsCreateSql = expectRow<{ sql: string }>(
|
|
295
|
+
db
|
|
296
|
+
.prepare(
|
|
297
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='nodes_fts'",
|
|
298
|
+
)
|
|
299
|
+
.get(),
|
|
300
|
+
["sql"],
|
|
301
|
+
);
|
|
302
|
+
const needsFtsRecreate =
|
|
303
|
+
!ftsCreateSql || !ftsCreateSql.sql.includes("contentless_delete=1");
|
|
304
|
+
if (needsFtsRecreate) {
|
|
305
|
+
db.exec("DROP TABLE IF EXISTS nodes_fts;");
|
|
306
|
+
db.exec(`
|
|
307
|
+
CREATE VIRTUAL TABLE nodes_fts USING fts5(
|
|
308
|
+
name,
|
|
309
|
+
qualified_name,
|
|
310
|
+
id UNINDEXED,
|
|
311
|
+
content='',
|
|
312
|
+
contentless_delete=1,
|
|
313
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
314
|
+
);
|
|
315
|
+
`);
|
|
316
|
+
// Rebuild FTS + fts_index from surviving nodes so both are populated
|
|
317
|
+
// after migration. Fresh databases have zero rows so this is a no-op.
|
|
318
|
+
const survivingNodes = expectRows<{
|
|
319
|
+
id: string;
|
|
320
|
+
name: string;
|
|
321
|
+
qualified_name: string;
|
|
322
|
+
}>(
|
|
323
|
+
db.prepare("SELECT id, name, qualified_name FROM nodes").all(),
|
|
324
|
+
["id", "name", "qualified_name"],
|
|
325
|
+
);
|
|
326
|
+
if (survivingNodes.length > 0) {
|
|
327
|
+
const insertFts = db.prepare(
|
|
328
|
+
"INSERT INTO nodes_fts (rowid, name, qualified_name) VALUES (?, ?, ?)",
|
|
329
|
+
);
|
|
330
|
+
const upsertFtsIndex = db.prepare(
|
|
331
|
+
"INSERT OR REPLACE INTO fts_index (fts_rowid, node_id) VALUES (?, ?)",
|
|
332
|
+
);
|
|
333
|
+
for (const n of survivingNodes) {
|
|
334
|
+
const rowid = ftsRowidForNodeId(n.id);
|
|
335
|
+
insertFts.run(rowid, n.name, n.qualified_name);
|
|
336
|
+
upsertFtsIndex.run(rowid, n.id);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// NOTE: the schema_version marker is written by the caller
|
|
341
|
+
// (applyCodingGraphSchema / writeSchemaVersion), NOT here. createTables
|
|
342
|
+
// only owns additive DDL; the version-write concern (never downgrade a
|
|
343
|
+
// newer DB) lives at the apply layer where currentVersion is known.
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Stamp the schema_version meta row. INSERT OR REPLACE so the upgrade
|
|
348
|
+
* path can rewrite the row in place (rule 23 — one canonical form).
|
|
349
|
+
* Callers MUST gate this on `currentVersion <= CODING_GRAPH_SCHEMA_VERSION`
|
|
350
|
+
* to avoid downgrading a newer DB.
|
|
351
|
+
*/
|
|
352
|
+
function writeSchemaVersion(
|
|
353
|
+
db: BetterSqlite3Database,
|
|
354
|
+
version: number,
|
|
355
|
+
): void {
|
|
356
|
+
db.prepare(
|
|
357
|
+
"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)",
|
|
358
|
+
).run(String(version));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Read the schema_version row. Returns 0 when the table is missing or the
|
|
363
|
+
* row has not been written yet — used by the upgrade stub test.
|
|
364
|
+
*
|
|
365
|
+
* Mirrors the LCM helper's tolerance: a missing `meta` table is not an
|
|
366
|
+
* error condition here, it just means the schema has never been applied.
|
|
367
|
+
*/
|
|
368
|
+
export function readSchemaVersion(db: BetterSqlite3Database): number {
|
|
369
|
+
const metaTable = expectRow<{ name: string }>(
|
|
370
|
+
db
|
|
371
|
+
.prepare(
|
|
372
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='meta'",
|
|
373
|
+
)
|
|
374
|
+
.get(),
|
|
375
|
+
["name"],
|
|
376
|
+
);
|
|
377
|
+
if (!metaTable) return 0;
|
|
378
|
+
const meta = expectRow<{ value: string }>(
|
|
379
|
+
db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get(),
|
|
380
|
+
["value"],
|
|
381
|
+
);
|
|
382
|
+
return meta ? parseInt(meta.value, 10) : 0;
|
|
383
|
+
}
|