Skip to content

fix(graph): a row without a site leaves the located edges alone - #3529

Open
vitali87 wants to merge 1 commit into
mainfrom
pr-split/fix-2879-siteless-rows-leave-located-edges/pr-1
Open

vitali87 wants to merge 1 commit into
mainfrom
pr-split/fix-2879-siteless-rows-leave-located-edges/pr-1

Conversation

@vitali87

@vitali87 vitali87 commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • CALLS, REFERENCES, INSTANTIATES, IMPORTS, LINKS_TO and FLOWS_TO keep one edge per site. A row with none of its type's site keys used to MERGE on its endpoints alone. That matched every located edge of the pair and set the row's properties, resolution among them, on each one. Such rows come from returned and factory closures, the callable-parameter fallback, libclang macro uses, Roslyn facts and inferred C# namespace imports. A row like that now lands on the pair's site-less edge, created when there is none, which is how index.bin already keeps it.
  • What changes for users: a guessed call keeps its own resolution rather than reading as exact, so cgr rename no longer rewrites a call on an unrelated object. The graph and index.bin also hold the same CALLS edges.
  • Trace write-back relied on the old merge. _confirm_static set only resolution through its own query, and the rest of the observation (dynamic, dynamic_call_count, dynamic_stale, ...) rode on a site-less row that the merge stamped onto every static site. The confirm query now sets the whole observation on every static site itself, and a confirmed pair writes no site-less row. Runtime-only edges are written as before. What [Bug]: any incremental re-parse of a file silently discards its trace-derived (dynamic) CALLS edges; dead-code flips back after a comment edit #2429 specifies is unchanged: a confirmed pair carries the observation on each of its sites, with no site-less duplicate.

Demo

CALLS edges and cgr rename on main and on this branch

The fixture from the issue: make(obj) calls obj.inner() on an untyped object and returns its own inner. It is indexed into an embedded graph, first importing codebase_rag from main, then from this branch. On main the guessed line-4 call reads as exact and rename would rewrite it. Here it stays heuristic, and the returned-closure edge stands alone with no site.

That edge is why rename now refuses this fixture: "1 graph-known site(s) carry no rewrite location". Rename already refused a closure that is only returned for the same reason. A closure that is both called and returned went through on main only because its site-less edge was merged into the located call, which is this bug. It is now refused the same way. The refusal comes from rename counting a reachability edge as an occurrence of the name, which #3531 describes.

How

  • graph_service: site-less rows of a site-keyed type take a two-step write. The first step updates the pair's site-less edge (WHERE every site key IS NULL). The second creates one for the pairs that had none, one edge per pair however many rows, with later rows' properties winning as repeated writes to one edge do. Rows still count once each in the flush summary. The Rust extension does not take this case, so these rows run Cypher; every other write is unchanged.
  • build_trace_confirm_calls_query sets each property in TRACE_CONFIRMED_PROPS with coalesce($key, r.key), so a key the observation lacks keeps the edge's value. A test pins the keys both trace writers produce to that list. write_trace_edge returns after confirming.
  • Comments and docs/architecture/graph-schema.md no longer say site-less rows collapse onto located edges.

Type of Change

  • Bug fix
  • New feature
  • Performance improvement
  • Refactoring (no functional changes)
  • Documentation
  • CI/CD or tooling
  • Dependencies

Related Issues

Closes #2879. The embedded-engine tests are in #3530, which is stacked on this PR.

Test Plan

  • Unit tests pass (make test-parallel or uv run pytest -n auto -m "not integration")
  • New tests added
  • Integration tests pass (make test-integration, requires Docker)
  • Manual testing (describe below)

New tests here:

  • integration/test_siteless_rel_rows_memgraph.py (Memgraph 3.3.0): fails with the routing to the new path disabled (the located edges read exact), passes with it.
  • test_node_relationship_coverage.py::test_a_row_without_a_site_flushes_in_two_queries: every site-keyed type writes a site-less row as an update, then a create, and never as a MERGE.
  • test_dynamic_trace_ingest.py::test_the_confirm_query_names_every_key_an_observation_carries: fails when a key is dropped from TRACE_CONFIRMED_PROPS.

Negative tests:

  • The Memgraph test: a located heuristic edge keeps heuristic after a site-less exact row for its pair, and two site-less rows make one edge, not two.
  • The trace tests assert that a confirmed pair writes no relationship row.

Existing tests changed:

  • test_graph_service_calls_failure_logging.py, test_failed_rel_rows_logged.py, test_links_after_targets.py, test_graph_service_node_retry.py, test_graph_service.py::TestCreateMode::test_flush_relationships_uses_merge_query_by_default and test_node_relationship_coverage.py::test_each_relationship_type_can_be_flushed: their rows now carry a site, as real rows of those types do. Their fakes stand in for the MERGE query's count, and rows without a site have their own path, covered by the new tests.
  • test_dynamic_trace_ingest.py, test_trace_carry.py, test_edge_resolution.py and test_trace_confirm_labelled.py: a confirmed pair's observation is now read from the confirm write's parameters, and each asserts that no relationship row was written for that pair.

Runs, on this stack's top before the split:

Mutations, each failing the test meant to catch it:

  • Without the routing to the new path.
  • Without the per-pair dedupe.
  • Counting one row per created edge.
  • Leaving the trace row in for confirmed pairs: 4 unit tests and 2 tests in test_trace_edges_survive_reparse_e2e.py.
  • Dropping a key from TRACE_CONFIRMED_PROPS.

Checklist

  • PR title follows Conventional Commits format
  • A breaking change has ! in the title and its release notes in changes/breaking/<PR number>.md (see Breaking changes)
  • All pre-commit checks pass (make pre-commit)
  • No hardcoded strings in non-config/non-constants files
  • No # type: ignore, cast(), Any, or object type hints
  • New comments and docstrings explain why, not what (see Comment Policy)

@greptile-apps

greptile-apps Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerTREX TREX

Tested 2 flows, found no issues.

What we tested

  • Pass · Keep calls without locations on separate edges · View ↗
  • Pass · Save trace details on every static call · View ↗

@coderabbitai

coderabbitai Bot commented Oct 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 35 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 8 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 0c97225 and 673ece0.

Files selected for processing: 19
  • codebase_rag/constants/graph.py
  • codebase_rag/constants/trace.py
  • codebase_rag/cypher_queries.py
  • codebase_rag/parsers/call_processor.py
  • codebase_rag/services/graph_service.py
  • codebase_rag/services/protobuf_service.py
  • codebase_rag/tests/integration/test_siteless_rel_rows_memgraph.py
  • codebase_rag/tests/test_dynamic_trace_ingest.py
  • codebase_rag/tests/test_edge_resolution.py
  • codebase_rag/tests/test_failed_rel_rows_logged.py
  • codebase_rag/tests/test_graph_service.py
  • codebase_rag/tests/test_graph_service_calls_failure_logging.py
  • codebase_rag/tests/test_graph_service_node_retry.py
  • codebase_rag/tests/test_links_after_targets.py
  • codebase_rag/tests/test_node_relationship_coverage.py
  • codebase_rag/tests/test_trace_carry.py
  • codebase_rag/tests/test_trace_confirm_labelled.py
  • codebase_rag/trace/ingest.py
  • docs/architecture/graph-schema.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T18:59:03.583299Z 673ece0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 673ece0a1a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return (
f"MATCH (a:{from_label} {{{from_key}: row.from_val}}), "
f"(b:{to_label} {{{to_key}: row.to_val}})\n"
f"CREATE (a)-[r:{rel_type}]->(b)\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make site-less edge creation atomic

When two ingestors concurrently write the same previously absent site-less endpoint pair, both update statements can observe no matching edge and then both execute this unconditional CREATE, leaving duplicate site-less relationships. The two statements run as separate auto-commit transactions, and _conn_lock only serializes one ingestor instance, so it does not prevent this race against another writer to the shared graph; preserve the prior idempotence with an atomic database operation or cross-ingestor serialization.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; no concrete blocking bugs were found.

Summary

This PR keeps relationship rows without a site separate from located edges. Trace confirmation now writes the full runtime observation directly onto existing static edges.

  • Site-less relationship rows keep their own edge between two nodes.
  • Runtime traces update every static call edge they confirm.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Relationship row] --> B{Has site keys?}
  B -->|Yes| C[Merge by endpoints and site]
  B -->|No| D[Update edge without site keys]
  D --> E{Edge found?}
  E -->|No| F[Create one edge for the pair]
  E -->|Yes| G[Keep located edges unchanged]
  H[Runtime observation] --> I{Static pair exists?}
  I -->|Yes| J[Write observation onto static edges]
  I -->|No| A
Loading

Reviews (1) · Last reviewed commit: "Update codebase_rag/services/graph_servi..." · Reviewed by Greptile

@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

1 participant