Filing this at @lexasub's request from #50 — the follow-up to the :EDGE mismatch that PR fixed for the call path only.
Summary
batch_upsert_edges writes every edge as a single untyped :EDGE relationship and stores the kind as a property. A number of read sites instead match on typed relationships (:INHERITS, :OVERRIDES, :TYPES, :CAPTURES, :EXTENDS, :IMPLEMENTS) that nothing ever creates, so those queries can only ever return empty — silently, with no error.
The writer
# ast_rag/repositories/queries.py:58
MERGE (a)-[r:EDGE {id: e.id}]->(b)
SET r += e
Everything lands on :EDGE; kind arrives as a property via SET r += e. The only other relationship types created anywhere are :RELATES (neo4j_repository.py:329,360) and :CONTAINS_BLOCK (graph_updater_service.py:857) — both of which do have matching readers and are fine.
There is no dynamic/APOC relationship-type creation, so nothing else can be emitting these types.
The broken read sites
Eight query sites match relationship types that no writer emits:
| file:line |
pattern |
result |
api/ast_rag_api.py:470 |
[:INHERITS|EXTENDS|IMPLEMENTS*1..N] |
always empty |
api/ast_rag_api.py:486 |
[:INHERITS|EXTENDS|IMPLEMENTS*1..N] |
always empty |
api/ast_rag_api.py:520 |
[:OVERRIDES*1..N] |
always empty |
api/ast_rag_api.py:908 |
[r:TYPES] |
always 0 |
api/ast_rag_api.py:913 |
[r:INHERITS|EXTENDS|IMPLEMENTS] |
always 0 |
api/ast_rag_api.py:981 |
[r:TYPES] |
always 0 |
api/ast_rag_api.py:1005 |
[r:INHERITS|EXTENDS|IMPLEMENTS] |
always 0 |
services/search_service.py:499 |
[:CAPTURES] |
always empty |
This affects get_inheritance_tree / ancestor–descendant traversal, find_overrides, and the type-usage and inheritance counts in the node-detail/impact paths.
Clearest illustration
Both shapes sit in the same method, a few lines apart — the first works, the next two cannot:
-- ast_rag_api.py:903 WORKS (the shape #50 moved the call path to)
MATCH (caller)-[r:EDGE]->(target {id: $node_id})
WHERE caller.valid_to IS NULL AND r.valid_to IS NULL AND r.kind IN $call_kinds
RETURN count(*) as count
-- ast_rag_api.py:908 ALWAYS 0
MATCH (user)-[r:TYPES]->(target {id: $node_id})
WHERE user.valid_to IS NULL AND r.valid_to IS NULL
RETURN count(*) as count
-- ast_rag_api.py:913 ALWAYS 0
MATCH (child)-[r:INHERITS|EXTENDS|IMPLEMENTS]->(parent {id: $node_id})
WHERE child.valid_to IS NULL AND r.valid_to IS NULL
RETURN count(*) as count
Two ways to fix it — your call
Option A — align the readers to the writer. Rewrite the eight sites as [r:EDGE] + WHERE r.kind IN [...], matching what #50 did for calls. Small, contained, no re-index required, and consistent with the majority of existing queries. The cost is that variable-length traversals get more awkward: [:INHERITS*1..N] becomes [r:EDGE*1..N] with an ALL(rel IN r WHERE rel.kind IN [...]) predicate, which Neo4j cannot index as well.
Option B — make the writer emit typed relationships. Keeps the traversal queries natural and lets Neo4j use relationship-type indexes, which matters for the *1..N inheritance walks. Costs a dynamic write (APOC or a per-kind MERGE branch), and it is a breaking graph-schema change — existing indexes need re-indexing.
I lean A for now: it is the smaller change, it makes the queries correct today, and it does not force a re-index on anyone. B is the better long-term shape if inheritance traversal depth becomes a performance concern — worth doing deliberately rather than as a bug fix.
Happy to send a PR for whichever you prefer. If A, I'd do it as one PR across the eight sites with a test per query path asserting a non-empty result on a fixture that actually has inheritance/override/type edges — the current tests don't catch this because they never assert these paths return anything.
Filing this at @lexasub's request from #50 — the follow-up to the
:EDGEmismatch that PR fixed for the call path only.Summary
batch_upsert_edgeswrites every edge as a single untyped:EDGErelationship and stores the kind as a property. A number of read sites instead match on typed relationships (:INHERITS,:OVERRIDES,:TYPES,:CAPTURES,:EXTENDS,:IMPLEMENTS) that nothing ever creates, so those queries can only ever return empty — silently, with no error.The writer
Everything lands on
:EDGE;kindarrives as a property viaSET r += e. The only other relationship types created anywhere are:RELATES(neo4j_repository.py:329,360) and:CONTAINS_BLOCK(graph_updater_service.py:857) — both of which do have matching readers and are fine.There is no dynamic/APOC relationship-type creation, so nothing else can be emitting these types.
The broken read sites
Eight query sites match relationship types that no writer emits:
api/ast_rag_api.py:470[:INHERITS|EXTENDS|IMPLEMENTS*1..N]api/ast_rag_api.py:486[:INHERITS|EXTENDS|IMPLEMENTS*1..N]api/ast_rag_api.py:520[:OVERRIDES*1..N]api/ast_rag_api.py:908[r:TYPES]api/ast_rag_api.py:913[r:INHERITS|EXTENDS|IMPLEMENTS]api/ast_rag_api.py:981[r:TYPES]api/ast_rag_api.py:1005[r:INHERITS|EXTENDS|IMPLEMENTS]services/search_service.py:499[:CAPTURES]This affects
get_inheritance_tree/ ancestor–descendant traversal,find_overrides, and the type-usage and inheritance counts in the node-detail/impact paths.Clearest illustration
Both shapes sit in the same method, a few lines apart — the first works, the next two cannot:
Two ways to fix it — your call
Option A — align the readers to the writer. Rewrite the eight sites as
[r:EDGE]+WHERE r.kind IN [...], matching what #50 did for calls. Small, contained, no re-index required, and consistent with the majority of existing queries. The cost is that variable-length traversals get more awkward:[:INHERITS*1..N]becomes[r:EDGE*1..N]with anALL(rel IN r WHERE rel.kind IN [...])predicate, which Neo4j cannot index as well.Option B — make the writer emit typed relationships. Keeps the traversal queries natural and lets Neo4j use relationship-type indexes, which matters for the
*1..Ninheritance walks. Costs a dynamic write (APOC or a per-kindMERGEbranch), and it is a breaking graph-schema change — existing indexes need re-indexing.I lean A for now: it is the smaller change, it makes the queries correct today, and it does not force a re-index on anyone. B is the better long-term shape if inheritance traversal depth becomes a performance concern — worth doing deliberately rather than as a bug fix.
Happy to send a PR for whichever you prefer. If A, I'd do it as one PR across the eight sites with a test per query path asserting a non-empty result on a fixture that actually has inheritance/override/type edges — the current tests don't catch this because they never assert these paths return anything.