Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions crates/codegraph-core/src/native_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,22 @@ impl NativeDatabase {
)
.map_err(|e| napi::Error::from_reason(format!("cfg_edges prepare failed: {e}")))?;

let mut del_edges = tx.prepare(
"DELETE FROM cfg_edges WHERE function_node_id = ?1",
)
.map_err(|e| napi::Error::from_reason(format!("cfg_edges del prepare failed: {e}")))?;
let mut del_blocks = tx.prepare(
"DELETE FROM cfg_blocks WHERE function_node_id = ?1",
)
.map_err(|e| napi::Error::from_reason(format!("cfg_blocks del prepare failed: {e}")))?;

for entry in &entries {
// Delete existing CFG data for this node so the caller doesn't
// need to perform deletes on a separate (JS) connection, which
// would cause a WAL conflict with the native connection.
let _ = del_edges.execute(params![entry.node_id]);
let _ = del_blocks.execute(params![entry.node_id]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent delete errors may mask partial state

Both DELETE statements discard their Result with let _ = ..., silently swallowing any error. If either delete fails inside the transaction (e.g., a locked auxiliary index or a statement constraint), the subsequent INSERTs still proceed, potentially leaving duplicate rows for the same function_node_id.

The existing INSERT code uses the same pattern (if let Ok(_) = block_stmt.execute(...)) so this is consistent, but the deletes are "load-bearing" here — a silent failure means old rows survive alongside new ones. Consider propagating delete errors the same way the tx.commit() error is propagated:

Suggested change
let _ = del_edges.execute(params![entry.node_id]);
let _ = del_blocks.execute(params![entry.node_id]);
del_edges.execute(params![entry.node_id])
.map_err(|e| napi::Error::from_reason(format!("cfg_edges delete failed: {e}")))?;
del_blocks.execute(params![entry.node_id])
.map_err(|e| napi::Error::from_reason(format!("cfg_blocks delete failed: {e}")))?;

This keeps the whole function consistent with how tx.commit() failures are surfaced, and ensures the caller gets a clear error rather than silently storing duplicate blocks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Changed both del_edges.execute() and del_blocks.execute() to propagate errors via .map_err(|e| napi::Error::from_reason(...))?; — consistent with how tx.commit() errors are surfaced. A failed delete now aborts the transaction instead of silently proceeding with inserts.


let mut block_db_ids: std::collections::HashMap<u32, i64> =
std::collections::HashMap::new();
for block in &entry.blocks {
Expand Down
9 changes: 7 additions & 2 deletions src/features/cfg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,13 @@ export async function buildCFGData(
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
if (!nodeId) continue;

deleteCfgForNode(db, nodeId);
if (!def.cfg?.blocks?.length) continue;
// Deletion is handled inside nativeDb.bulkInsertCfg to avoid
// dual-connection WAL conflicts between JS and native connections.
if (!def.cfg?.blocks?.length) {
// Still send an entry so the native side deletes stale CFG data.
entries.push({ nodeId, blocks: [], edges: [] });
continue;
}

const cfg = def.cfg as unknown as { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] };
entries.push({
Expand Down
Loading