Skip to content

fix(queries): match :EDGE and filter on kind in typed traversals - #70

Merged
r0h1tb merged 1 commit into
mainfrom
fix/typed-relationship-queries
Aug 24, 2026
Merged

fix(queries): match :EDGE and filter on kind in typed traversals#70
r0h1tb merged 1 commit into
mainfrom
fix/typed-relationship-queries

Conversation

@r0h1tb

@r0h1tb r0h1tb commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Description

Eight read sites matched relationship types that no writer ever creates, so
they could only ever return empty — silently, with no error.

batch_upsert_edges writes every edge as a single untyped :EDGE
relationship and keeps the semantic kind as a property:

MERGE (a)-[r:EDGE {id: e.id}]->(b)
SET r += e

The only other relationship types created anywhere are :RELATES and
:CONTAINS_BLOCK, both of which have matching readers and are fine.

This is option A, as you chose on the issue — align the readers to the
writer. Each site now matches [r:EDGE] and filters with WHERE r.kind IN $...,
the shape #50 already moved the call path to. Variable-length traversals use
all(rel IN rels WHERE rel.kind IN $...), matching find_callers/find_callees.

Related Issue

Fixes #61

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature
  • Breaking change
  • Documentation update

What was broken

file:line was now
api/ast_rag_api.py:470 [:INHERITS|EXTENDS|IMPLEMENTS*1..N] [rels:EDGE*1..N] + all(rel IN rels …)
api/ast_rag_api.py:486 same same
api/ast_rag_api.py:520 [:OVERRIDES*1..N] [rels:EDGE*1..N] + all(rel IN rels …)
api/ast_rag_api.py:916 [r:TYPES] [r:EDGE] + r.kind IN $type_kinds
api/ast_rag_api.py:921 [r:INHERITS|EXTENDS|IMPLEMENTS] [r:EDGE] + r.kind IN $inheritance_kinds
api/ast_rag_api.py:995 [r:TYPES] [r:EDGE] + r.kind IN $type_kinds
api/ast_rag_api.py:1019 [r:INHERITS|EXTENDS|IMPLEMENTS] [r:EDGE] + r.kind IN $inheritance_kinds
services/search_service.py:499 [:CAPTURES] [r:EDGE] + r.kind IN $capture_kinds

User-visible effect: find_subclasses, find_superclasses, find_overrides
and the type-usage / inheritance counts in the node-detail and impact paths
returned nothing regardless of the graph.

Two details worth flagging yourself

1. type(r) would have silently changed meaning. The paginated inheritance
query returned type(r) as rel_type, which after this change is always "EDGE".
It returns r.kind instead, so the reported reference type still says
INHERITS/EXTENDS/IMPLEMENTS rather than degrading to EDGE. A naive
rewrite of the eight sites would have broken that field.

2. :CAPTURES is corrected but still returns nothing — for a different
reason, which I want to be explicit about rather than imply it now works. The
parsing layer emits no CAPTURES edges and no Variable nodes at all, so
find_lambdas(with_captured_vars=True) reports captured_vars: [] either way.
The query shape is fixed so it starts working the moment the extractor lands,
and EdgeKind.CAPTURES is declared so the kind is no longer a bare undeclared
string. Happy to drop that hunk if you would rather keep this PR strictly to
the sites where edges actually exist.

Option B

Left undone deliberately, and documented in a comment next to the new kind
groups. Emitting real typed relationships keeps traversals natural and lets
Neo4j use relationship-type indexes for the *1..N inheritance walks, but it
needs a dynamic write (APOC or a per-kind MERGE branch) and is a breaking
graph-schema change requiring a re-index. Worth doing deliberately rather than
as a bug fix.

Checklist

  • My code follows the code style of this project
  • I have added tests that prove my fix works
  • All new and existing tests passed (pytest tests/ -v)
  • I have updated the documentation accordingly (comments; no doc pages affected)
  • I have run ast-rag evaluate --allnot run, see Testing below
  • My changes generate no new warnings

Testing

The regression test is static, for the same reason test_query_parameters_bound
is: proving a traversal returns rows needs a populated graph with real
inheritance and override edges. It collects the relationship types the writers
create and the ones the readers match, and asserts the second is a subset of the
first. It is derived from the source rather than hard-coded, so if a writer is
later changed to emit typed relationships (option B), the matching readers stop
failing it on their own. A second test guards the guard — if the scan finds no
:EDGE writer, the subset check proves nothing and it says so.

On this branch with the source changes reverted and the test kept:

$ git stash                       # revert source, keep the new test
$ pytest tests/test_relationship_types_emitted.py -q

E   AssertionError: These queries match relationship types no writer creates,
E   so they can only ever return empty:
E     :CAPTURES matched at ast_rag/services/search_service.py:499
E     :EXTENDS matched at ast_rag/api/ast_rag_api.py:470, :486, :921, :1019
E     :IMPLEMENTS matched at ast_rag/api/ast_rag_api.py:470, :486, :921, :1019
E     :INHERITS matched at ast_rag/api/ast_rag_api.py:470, :486, :921, :1019
E     :OVERRIDES matched at ast_rag/api/ast_rag_api.py:520
E     :TYPES matched at ast_rag/api/ast_rag_api.py:916, :995
E
E   Relationship types actually written: ['CONTAINS_BLOCK', 'EDGE', 'RELATES'].

1 failed, 1 passed

Full suite, Python 3.12 on macOS:

passed failed skipped xfailed
baseline (main @ 41e48af) 232 0 1 1
this branch 234 0 1 1

Exactly +2, the two new tests. No pre-existing failures on either side.

test_query_parameters_bound still passes, which matters here — it is what
would catch a rewritten query whose new $inheritance_kinds / $type_kinds /
$capture_kinds parameter was not actually bound at some session.run site.

$ ruff check ast_rag/          # All checks passed!
$ ruff format --check ast_rag/ tests/   # 88 files already formatted

ast-rag evaluate --all was not run. It needs a live Neo4j + Qdrant, and
the committed ast_rag_config.json points at a private LAN (#62), so I have no
way to run it from here. Nothing in this change touches indexing, embeddings or
scoring — it only rewrites read-side Cypher — but flagging it rather than
ticking the box.

Additional Notes

Ping me if you would prefer this split per query family, or if you want the
:CAPTURES hunk dropped.

batch_upsert_edges writes every edge as a single untyped :EDGE
relationship and keeps the semantic kind as a property:

    MERGE (a)-[r:EDGE {id: e.id}]->(b)
    SET r += e

Eight read sites instead matched relationship types that no writer
creates (:INHERITS, :EXTENDS, :IMPLEMENTS, :OVERRIDES, :TYPES,
:CAPTURES). Those queries are valid Cypher over an empty set: they
compile, they run, and they return nothing, for ever, with no error.

That silently emptied find_subclasses, find_superclasses, find_overrides
and the type-usage and inheritance counts in the node-detail and impact
paths.

This is option A from #61, as chosen on the issue: align the readers to
the writer. Each site now matches [r:EDGE] and filters with
`WHERE r.kind IN $...`, the shape #50 already moved the call path to.
Variable-length traversals use `all(rel IN rels WHERE rel.kind IN $...)`,
matching find_callers/find_callees.

Option B — emitting real typed relationships — keeps traversals natural
and lets Neo4j use relationship-type indexes for the *1..N inheritance
walks, but needs a dynamic write and a re-index. Documented in a comment
next to the new kind groups; worth doing deliberately rather than as a
bug fix.

Two details worth calling out:

- The paginated inheritance query returned `type(r) as rel_type`, which
  would now always be "EDGE". It returns `r.kind` instead, so the
  reported reference type still says INHERITS/EXTENDS/IMPLEMENTS.
- The :CAPTURES site is corrected for consistency but still returns
  nothing, for a different reason: the parsing layer emits no CAPTURES
  edges or Variable nodes yet. EdgeKind.CAPTURES is declared so the kind
  is no longer a bare undeclared string.

The regression test is static, for the same reason test_query_parameters_bound
is: proving a traversal returns rows needs a populated graph with real
inheritance and override edges. It collects the relationship types the
writers create and the ones the readers match, and asserts the second set
is a subset of the first — so a future typed writer makes its matching
readers pass on their own.

Fixes #61
@lexasub

lexasub commented Aug 8, 2026

Copy link
Copy Markdown
Owner

@good pr, you may merge

@r0h1tb
r0h1tb merged commit 83d8add into main Aug 24, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in raged kanban Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Typed-relationship queries match relationship types the writer never emits (:INHERITS, :OVERRIDES, :TYPES, :CAPTURES)

2 participants