From a158a696337ecbfb0da73ee079a9a117876887e7 Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Wed, 1 Jul 2026 03:46:37 +0000 Subject: [PATCH] =?UTF-8?q?fix(graph):=20dedupe=20node=5Frows=20+=20use=20?= =?UTF-8?q?INSERT=20OR=20REPLACE=20to=20avoid=20UNIQUE=20constraint=20fail?= =?UTF-8?q?ures=20=E2=80=94=20closes=20#114?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: populate_graph_tables() in scripts/graph_model.py built node_rows from flat_nodes, which can contain duplicate node_id values (file:line:fn) when e.g. a class and its __init__ method share the same line, or when two declarations collide on the same line number. The graph_nodes.node_id column has a UNIQUE constraint, so a bare INSERT raised sqlite3.IntegrityError and rolled back the entire BEGIN EXCLUSIVE transaction — leaving graph_nodes empty. Fix applies two layers of defense: 1. Dedupe node_rows by node_id (last occurrence wins, matching the 'last match wins' semantics of the flat registry) before the batch insert. Applied to both populate_graph_tables() and incremental_graph_update(). 2. Use INSERT OR REPLACE instead of bare INSERT so even if a duplicate slips through (e.g. from a prior partial write), the batch succeeds instead of failing the whole transaction. Verified: scan on clean_app fixture now consistently populates graph_nodes (31 nodes) and graph_edges (113 edges) without IntegrityError. Test suite: 1183 passed, 1 pre-existing failure (#118, unrelated). --- scripts/graph_model.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/scripts/graph_model.py b/scripts/graph_model.py index 638f268e..105d5627 100644 --- a/scripts/graph_model.py +++ b/scripts/graph_model.py @@ -325,6 +325,22 @@ def populate_graph_tables(workspace: str, db_path: Optional[str] = None) -> Dict src_file, src_line, confidence, extra_json, )) + # ── Issue #114: dedupe node_rows by node_id. + # The flat registry can produce multiple nodes with the same + # ``node_id`` (file:line:fn) when e.g. a class and its __init__ + # method share the same line, or when two declarations collide on + # the same line number. The graph_nodes.node_id column has a UNIQUE + # constraint, so a bare INSERT would raise sqlite3.IntegrityError + # and roll back the entire batch — leaving the graph empty. + # Dedupe by node_id (last occurrence wins, matching the "last match + # wins" semantics of the flat registry) before the batch insert. + if node_rows: + seen: Dict[str, Tuple[Any, ...]] = {} + for row in node_rows: + # row[0] is node_id + seen[row[0]] = row + node_rows = list(seen.values()) + # ── Issue #10: RAM-first indexing — batch write in a single # ``BEGIN EXCLUSIVE`` transaction. The node_rows + edge_rows are # collected fully in memory (above) BEFORE opening the transaction; @@ -351,8 +367,10 @@ def populate_graph_tables(workspace: str, db_path: Optional[str] = None) -> Dict ) conn.execute("DELETE FROM {}".format(GRAPH_NODES_TABLE)) if node_rows: + # Issue #114: use INSERT OR REPLACE as defense-in-depth + # against UNIQUE constraint violations even after dedup. conn.executemany( - "INSERT INTO {t} (node_id, node_type, name, file, line, extra_json) " + "INSERT OR REPLACE INTO {t} (node_id, node_type, name, file, line, extra_json) " "VALUES (?, ?, ?, ?, ?, ?)".format(t=GRAPH_NODES_TABLE), node_rows, ) @@ -669,9 +687,15 @@ def incremental_graph_update( ) # ── Step 4 (insert): Re-insert nodes from changed files ── + # Issue #114: dedupe + INSERT OR REPLACE to avoid UNIQUE + # constraint violations on duplicate node_ids. if node_rows: + _seen_inc: Dict[str, Tuple[Any, ...]] = {} + for _row in node_rows: + _seen_inc[_row[0]] = _row + node_rows = list(_seen_inc.values()) conn.executemany( - "INSERT INTO {t} (node_id, node_type, name, file, line, extra_json) " + "INSERT OR REPLACE INTO {t} (node_id, node_type, name, file, line, extra_json) " "VALUES (?, ?, ?, ?, ?, ?)".format(t=GRAPH_NODES_TABLE), node_rows, )