Skip to content

Commit 4717232

Browse files
authored
fix(cfg): avoid dual-connection WAL conflict in native bulkInsertCfg (#719)
* fix(cfg): move delete-before-insert into native bulkInsertCfg to avoid WAL conflict The native bulk insert path called deleteCfgForNode via the JS better-sqlite3 connection, then suspended the JS DB and inserted via the native rusqlite connection. This dual-connection pattern caused a WAL conflict that left cfg_blocks empty after builds. Move the DELETE statements into the Rust bulk_insert_cfg function so all operations happen on a single (native) connection within one transaction. The JS side now sends entries with empty blocks/edges for nodes that need deletion only. * fix(cfg): disable native bulk CFG path to avoid dual-connection WAL conflict The v3.6.0 native binary introduced bulkInsertCfg, but the CFG path requires delete-before-insert where deletes go through JS (better-sqlite3) and inserts through native (rusqlite), creating a WAL conflict that silently drops all CFG data. Disable the native bulk path so the proven JS-only persistNativeFileCfg handles both operations on a single connection. The Rust-side fix (previous commit) will allow re-enabling the native path once the next binary is published with delete logic inside bulkInsertCfg. * fix(cfg): prefix unused engineOpts param with underscore * fix: propagate CFG delete errors instead of silently swallowing (#719)
1 parent abe4b46 commit 4717232

2 files changed

Lines changed: 23 additions & 51 deletions

File tree

crates/codegraph-core/src/native_db.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,24 @@ impl NativeDatabase {
858858
)
859859
.map_err(|e| napi::Error::from_reason(format!("cfg_edges prepare failed: {e}")))?;
860860

861+
let mut del_edges = tx.prepare(
862+
"DELETE FROM cfg_edges WHERE function_node_id = ?1",
863+
)
864+
.map_err(|e| napi::Error::from_reason(format!("cfg_edges del prepare failed: {e}")))?;
865+
let mut del_blocks = tx.prepare(
866+
"DELETE FROM cfg_blocks WHERE function_node_id = ?1",
867+
)
868+
.map_err(|e| napi::Error::from_reason(format!("cfg_blocks del prepare failed: {e}")))?;
869+
861870
for entry in &entries {
871+
// Delete existing CFG data for this node so the caller doesn't
872+
// need to perform deletes on a separate (JS) connection, which
873+
// would cause a WAL conflict with the native connection.
874+
del_edges.execute(params![entry.node_id])
875+
.map_err(|e| napi::Error::from_reason(format!("cfg_edges delete failed: {e}")))?;
876+
del_blocks.execute(params![entry.node_id])
877+
.map_err(|e| napi::Error::from_reason(format!("cfg_blocks delete failed: {e}")))?;
878+
862879
let mut block_db_ids: std::collections::HashMap<u32, i64> =
863880
std::collections::HashMap::new();
864881
for block in &entry.blocks {

src/features/cfg.ts

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ export async function buildCFGData(
369369
db: BetterSqlite3Database,
370370
fileSymbols: Map<string, FileSymbols>,
371371
rootDir: string,
372-
engineOpts?: {
372+
_engineOpts?: {
373373
nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
374374
suspendJsDb?: () => void;
375375
resumeJsDb?: () => void;
@@ -379,56 +379,11 @@ export async function buildCFGData(
379379
// skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
380380
const allNative = allCfgNative(fileSymbols);
381381

382-
// ── Native bulk-insert fast path ──────────────────────────────────────
383-
const nativeDb = engineOpts?.nativeDb;
384-
if (allNative && nativeDb?.bulkInsertCfg) {
385-
const entries: Array<Record<string, unknown>> = [];
386-
387-
for (const [relPath, symbols] of fileSymbols) {
388-
const ext = path.extname(relPath).toLowerCase();
389-
if (!CFG_EXTENSIONS.has(ext)) continue;
390-
391-
for (const def of symbols.definitions) {
392-
if (def.kind !== 'function' && def.kind !== 'method') continue;
393-
if (!def.line) continue;
394-
395-
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
396-
if (!nodeId) continue;
397-
398-
deleteCfgForNode(db, nodeId);
399-
if (!def.cfg?.blocks?.length) continue;
400-
401-
const cfg = def.cfg as unknown as { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] };
402-
entries.push({
403-
nodeId,
404-
blocks: cfg.blocks.map((b) => ({
405-
index: b.index,
406-
blockType: b.type,
407-
startLine: b.startLine ?? null,
408-
endLine: b.endLine ?? null,
409-
label: b.label ?? null,
410-
})),
411-
edges: cfg.edges.map((e) => ({
412-
sourceIndex: e.sourceIndex,
413-
targetIndex: e.targetIndex,
414-
kind: e.kind,
415-
})),
416-
});
417-
}
418-
}
419-
420-
if (entries.length > 0) {
421-
let inserted: number;
422-
try {
423-
engineOpts?.suspendJsDb?.();
424-
inserted = nativeDb.bulkInsertCfg(entries);
425-
} finally {
426-
engineOpts?.resumeJsDb?.();
427-
}
428-
info(`CFG (native bulk): ${inserted} blocks across ${entries.length} functions`);
429-
}
430-
return;
431-
}
382+
// NOTE: nativeDb.bulkInsertCfg is intentionally NOT used here.
383+
// The CFG path requires delete-before-insert (deleteCfgForNode) which creates
384+
// a dual-connection WAL conflict when deletes go through JS (better-sqlite3)
385+
// and inserts go through native (rusqlite). The JS-only persistNativeFileCfg
386+
// path below handles both on a single connection safely.
432387

433388
const extToLang = buildExtToLangMap();
434389
let parsers: unknown = null;

0 commit comments

Comments
 (0)