Skip to content

Commit 724bddc

Browse files
HumanBean17claude
andcommitted
fix: restructure _delete_file_scope to handle cross-file edge deletion
Process edge deletions across all scope files before deleting any nodes. The previous per-file loop could fail when file B has an EXTENDS edge to file A — deleting A's nodes first left dangling edges that Kuzu refused to drop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent aa541e2 commit 724bddc

1 file changed

Lines changed: 49 additions & 58 deletions

File tree

build_ast_graph.py

Lines changed: 49 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -729,70 +729,61 @@ def _delete_file_scope(conn: kuzu.Connection, filenames: set[str]) -> None:
729729
Skip phantom nodes (filename=""). For Symbol-to-Symbol and UNRESOLVED_AT
730730
edge tables only. Client/Producer/Route edges are handled separately in
731731
pass 5-6 global rebuild.
732-
"""
733-
for filename in filenames:
734-
# Delete Symbol-to-Symbol edges by source_file
735-
edge_tables = ["EXTENDS", "IMPLEMENTS", "INJECTS", "CALLS", "DECLARES", "OVERRIDES", "UNRESOLVED_AT"]
736-
for edge_type in edge_tables:
737-
query = f"""
738-
MATCH (src)-[e:{edge_type}]->(dst)
739-
WHERE e.source_file = $filename
740-
DELETE e
741-
"""
742-
conn.execute(query, {"filename": filename})
743732
744-
# Collect Symbol node IDs for this file (for UnresolvedCallSite cleanup)
745-
symbol_ids_query = """
746-
MATCH (s:Symbol)
747-
WHERE s.filename = $filename
748-
RETURN s.id
749-
"""
750-
symbol_ids = []
751-
result = conn.execute(symbol_ids_query, {"filename": filename})
752-
while result.has_next():
753-
row = result.get_next()
754-
symbol_ids.append(row[0]) # Kuzu returns list, not dict
755-
756-
# Delete UnresolvedCallSite nodes whose caller_id is in the collected set
757-
if symbol_ids:
758-
unresolved_query = """
759-
MATCH (u:UnresolvedCallSite)
760-
WHERE u.caller_id IN $symbol_ids
761-
DELETE u
762-
"""
763-
conn.execute(unresolved_query, {"symbol_ids": symbol_ids})
764-
765-
# Delete Symbol nodes
766-
delete_symbols_query = """
767-
MATCH (s:Symbol)
768-
WHERE s.filename = $filename
769-
DELETE s
733+
Edges are deleted in batch across all filenames first to avoid Kuzu
734+
"has connected edges" errors when edges from one file point to nodes
735+
in another file within the same scope.
736+
"""
737+
filename_list = list(filenames)
738+
739+
# Phase 1: Delete ALL edges from ALL scope files at once.
740+
# This avoids ordering issues where file A has an edge from file B
741+
# pointing into it; if we delete A's nodes before B's edges, Kuzu
742+
# raises "has connected edges" errors.
743+
edge_tables = ["EXTENDS", "IMPLEMENTS", "INJECTS", "CALLS", "DECLARES", "OVERRIDES", "UNRESOLVED_AT"]
744+
for edge_type in edge_tables:
745+
query = f"""
746+
MATCH (src)-[e:{edge_type}]->(dst)
747+
WHERE e.source_file IN $filenames
748+
DELETE e
770749
"""
771-
conn.execute(delete_symbols_query, {"filename": filename})
750+
conn.execute(query, {"filenames": filename_list})
772751

773-
# Delete Route nodes (note: EXPOSES edges are deleted in pass 5-6 global rebuild)
774-
delete_routes_query = """
775-
MATCH (r:Route)
776-
WHERE r.filename = $filename
777-
DELETE r
752+
# Phase 2: Collect all Symbol node IDs for UnresolvedCallSite cleanup.
753+
symbol_ids: list[str] = []
754+
symbol_ids_query = """
755+
MATCH (s:Symbol)
756+
WHERE s.filename IN $filenames
757+
RETURN s.id
758+
"""
759+
result = conn.execute(symbol_ids_query, {"filenames": filename_list})
760+
while result.has_next():
761+
row = result.get_next()
762+
symbol_ids.append(row[0])
763+
764+
# Delete UnresolvedCallSite nodes whose caller_id is in the collected set
765+
if symbol_ids:
766+
unresolved_query = """
767+
MATCH (u:UnresolvedCallSite)
768+
WHERE u.caller_id IN $symbol_ids
769+
DELETE u
778770
"""
779-
conn.execute(delete_routes_query, {"filename": filename})
771+
conn.execute(unresolved_query, {"symbol_ids": symbol_ids})
780772

781-
# Delete Client nodes
782-
delete_clients_query = """
783-
MATCH (c:Client)
784-
WHERE c.filename = $filename
785-
DELETE c
786-
"""
787-
conn.execute(delete_clients_query, {"filename": filename})
773+
# Phase 3: Delete Symbol nodes.
774+
delete_symbols_query = """
775+
MATCH (s:Symbol)
776+
WHERE s.filename IN $filenames
777+
DELETE s
778+
"""
779+
conn.execute(delete_symbols_query, {"filenames": filename_list})
788780

789-
# Delete Producer nodes
790-
delete_producers_query = """
791-
MATCH (p:Producer)
792-
WHERE p.filename = $filename
793-
DELETE p
794-
"""
795-
conn.execute(delete_producers_query, {"filename": filename})
781+
# Phase 4: Delete Route, Client, Producer nodes.
782+
for label in ["Route", "Client", "Producer"]:
783+
conn.execute(
784+
f"MATCH (n:{label}) WHERE n.filename IN $filenames DELETE n",
785+
{"filenames": filename_list},
786+
)
796787

797788

798789
def _scoped_write(conn: kuzu.Connection, tables: GraphTables, *, project_root: Path, meta_chain: dict[str, frozenset[str]] | None) -> None:

0 commit comments

Comments
 (0)