fix(cfg): avoid dual-connection WAL conflict in native bulkInsertCfg#719
fix(cfg): avoid dual-connection WAL conflict in native bulkInsertCfg#719carlos-alm merged 4 commits intomainfrom
Conversation
…d 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.
|
Claude finished @carlos-alm's task —— View job PR Review CompletedAnalyzing PR #719: fix(cfg): avoid dual-connection WAL conflict in native bulkInsertCfg Review Checklist
Root Cause AnalysisProblem Being Solved: WAL (Write-Ahead Logging) conflict introduced in v3.6.0 when Why This Approach: The fix correctly moves the DELETE operations into the native Rust side so all CFG write operations happen on a single connection. This eliminates the WAL conflict by ensuring atomicity of the delete-then-insert operation. Risk Assessment: ✅ Low risk - This is a genuine bug fix that restores correct behavior. The approach maintains the same functional behavior (delete existing CFG data before inserting new data) but does it through a single connection. Backlog Compliance
Code Quality AssessmentPositive aspects:
Technical correctness:
Test coverage: The existing test Critical ConcernsNone. This is a well-executed bug fix. Final Recommendation
|
Greptile SummaryThis PR fixes a dual-connection WAL conflict introduced when Changes:
Minor note: The comment at line 382 of Confidence Score: 5/5Safe to merge — the WAL conflict is fully resolved and the prior P1 (silent delete errors) has been addressed. All previously flagged concerns are resolved. The only remaining finding is a P2 comment-accuracy issue: the stale explanation in cfg.ts describes a WAL conflict that the Rust change in this same PR fixes. No logic bugs, data-integrity risks, or correctness issues remain. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant TS as buildCFGData (cfg.ts)
participant JS as better-sqlite3 (JS conn)
participant Rust as bulk_insert_cfg (rusqlite)
Note over TS,Rust: BEFORE this PR — dual-connection WAL conflict
TS->>JS: deleteCfgForNode(db, nodeId)
JS-->>TS: OK
TS->>Rust: nativeDb.bulkInsertCfg(entries)
Note right of Rust: WAL conflict! Both connections writing to same WAL file
Rust-->>TS: cfg_blocks empty ❌
Note over TS,Rust: AFTER this PR — single-connection JS path
TS->>JS: tx() — deleteCfgForNode + insertBlock + insertEdge
JS-->>TS: All writes committed on one connection ✅
Note over Rust: bulk_insert_cfg (Rust) also fixed: DELETE edges → DELETE blocks → INSERT all within one rusqlite transaction (ready for future re-enablement)
Reviews (2): Last reviewed commit: "fix: propagate CFG delete errors instead..." | Re-trigger Greptile |
…onflict 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.
| let _ = del_edges.execute(params![entry.node_id]); | ||
| let _ = del_blocks.execute(params![entry.node_id]); |
There was a problem hiding this comment.
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:
| 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.
There was a problem hiding this comment.
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.
|
@greptileai review |
Summary
bulkInsertCfgonNativeDatabase, causingbuildCFGDatato take the native bulk insert path for the first timedeleteCfgForNodevia the JS better-sqlite3 connection, thennativeDb.bulkInsertCfgvia the native rusqlite connection — a dual-connection WAL conflictcfg_blockstable ended up empty after builds, failing the incremental-parity test on all PRsDELETEstatements into the Rustbulk_insert_cfgfunction so all writes happen on a single (native) connection, eliminating the WAL conflictTest plan
tests/integration/incremental-parity.test.tspasses (specifically "preserves CFG blocks for changed file")