Skip to content

fix(long-term): stop silent data loss in the entity write path - #256

Open
frigidom1024 wants to merge 1 commit into
neo4j-labs:mainfrom
frigidom1024:fix/long-term-silent-data-loss
Open

fix(long-term): stop silent data loss in the entity write path#256
frigidom1024 wants to merge 1 commit into
neo4j-labs:mainfrom
frigidom1024:fix/long-term-silent-data-loss

Conversation

@frigidom1024

Copy link
Copy Markdown

Four defects where a call reported success while the graph did not hold what the caller was told it held. They compound: add_entity hands out an id that addresses no node, and add_relationship then acknowledges the write that id was used for, so the failure only surfaces as missing data much later.

Fixes #79.

  • aliases are written where the alias lookup does not read. add_entity stored them inside the JSON metadata blob while GET_ENTITY_BY_NAME reads a top-level aliases property — which is what MERGE_ENTITIES already wrote to target.aliases — so an entity was never findable by an alias passed to add_entity. aliases is now a top-level list property everywhere; ON MATCH appends only aliases not already present, in one statement, and _add_alias_to_entity drops its read-modify-write of the whole metadata blob. Rows written earlier still read back through a metadata fallback in _parse_entity.

    build_create_entity_query is public and callers do drive it with their own parameter dicts, so the new $aliases reference is opt-in (include_aliases=False, following the include_location precedent) — an unconditional one broke six existing tests with ParameterMissing.

  • add_entity returned an id that addressed no node. The query MERGEs on (name, type), so a repeat add hits ON MATCH, keeps the pre-existing node's id and discards the freshly minted one — which was returned anyway. The returned entity now adopts what the query stored.

    This is issue # Bug: add_relationship silently creates no edges when target entity already exists #79's root cause. The report describes the returned id as a "ghost entity"; no second node is created, which is why the fix belongs in the return value and not in a caller-side name lookup. With it, the reporting bridge's existing add_entity/add_relationship sequence works unmodified.

  • add_relationship acknowledged a write that matched nothing. CREATE_ENTITY_RELATIONSHIP MATCHes both endpoints before MERGEing the edge, so ids addressing no node write zero rows and the method still returned a Relationship. It now raises NotFoundError naming both ids. Re-adding an existing edge also returned a freshly minted uuid and the arguments rather than the stored values; the query now projects r.id/r.description/r.confidence, because Result.data() flattens a bare RETURN r to a (start_props, type, end_props) tuple and drops the relationship's own properties.

  • merge_duplicate_entities orphaned most of the entity's edges. The docstring said relationships were transferred; the query carried subqueries for MENTIONS and SAME_AS only, so a merge silently dropped RELATED_TO (both directions), both provenance edges (EXTRACTED_FROM, EXTRACTED_BY) and the v0.2 APPLIES_TO / TOUCHED audit edges. All are now copied onto the surviving entity, each tagged migrated_from. Edges are copied rather than moved so the merge stays reversible.

Bolt backend only. NAMS is unaffected: its add_relationship raises NotSupportedError, and its add_entity already follows merged_into and asserts the canonical id, so #79's defect does not exist there.

Tests: tests/unit/test_long_term_write_integrity.py (34) and tests/integration/test_long_term_write_integrity.py (22, including TestIssue79 reproducing the report's own sequence). Every defect here is invisible to a mocked client — the mechanism is a real MERGE/ON MATCH branch — and the integration suite caught two bugs in this patch, including the Result.data() flattening above. 12 of the 22 fail against the pre-fix code (8c29880) and all 22 pass against this commit; the other 10 assert behaviour that did not change.

Four defects where a call reported success while the graph did not hold
what the caller was told it held. They compound: add_entity hands out an
id that addresses no node, and add_relationship then acknowledges the
write that id was used for, so the failure only surfaces as missing data
much later.

Fixes neo4j-labs#79.

* aliases are written where the alias lookup does not read. add_entity
  stored them inside the JSON metadata blob while GET_ENTITY_BY_NAME
  reads a top-level `aliases` property — which is what MERGE_ENTITIES
  already wrote to target.aliases — so an entity was never findable by
  an alias passed to add_entity. `aliases` is now a top-level list
  property everywhere; ON MATCH appends only aliases not already
  present, in one statement, and _add_alias_to_entity drops its
  read-modify-write of the whole metadata blob. Rows written earlier
  still read back through a metadata fallback in _parse_entity.

  build_create_entity_query is public and callers do drive it with their
  own parameter dicts, so the new $aliases reference is opt-in
  (include_aliases=False, following the include_location precedent) —
  an unconditional one broke six existing tests with ParameterMissing.

* add_entity returned an id that addressed no node. The query MERGEs on
  (name, type), so a repeat add hits ON MATCH, keeps the pre-existing
  node's id and discards the freshly minted one — which was returned
  anyway. The returned entity now adopts what the query stored.

  This is issue neo4j-labs#79's root cause. The report describes the returned id
  as a "ghost entity"; no second node is created, which is why the fix
  belongs in the return value and not in a caller-side name lookup. With
  it, the reporting bridge's existing add_entity/add_relationship
  sequence works unmodified.

* add_relationship acknowledged a write that matched nothing.
  CREATE_ENTITY_RELATIONSHIP MATCHes both endpoints before MERGEing the
  edge, so ids addressing no node write zero rows and the method still
  returned a Relationship. It now raises NotFoundError naming both ids.
  Re-adding an existing edge also returned a freshly minted uuid and the
  arguments rather than the stored values; the query now projects
  r.id/r.description/r.confidence, because Result.data() flattens a bare
  RETURN r to a (start_props, type, end_props) tuple and drops the
  relationship's own properties.

* merge_duplicate_entities orphaned most of the entity's edges. The
  docstring said relationships were transferred; the query carried
  subqueries for MENTIONS and SAME_AS only, so a merge silently dropped
  RELATED_TO (both directions), both provenance edges (EXTRACTED_FROM,
  EXTRACTED_BY) and the v0.2 APPLIES_TO / TOUCHED audit edges. All are
  now copied onto the surviving entity, each tagged migrated_from. Edges
  are copied rather than moved so the merge stays reversible.

Bolt backend only. NAMS is unaffected: its add_relationship raises
NotSupportedError, and its add_entity already follows `merged_into` and
asserts the canonical id, so neo4j-labs#79's defect does not exist there.

Tests: tests/unit/test_long_term_write_integrity.py (34) and
tests/integration/test_long_term_write_integrity.py (22, including
TestIssue79 reproducing the report's own sequence). Every defect here is
invisible to a mocked client — the mechanism is a real MERGE/ON MATCH
branch — and the integration suite caught two bugs in this patch,
including the Result.data() flattening above. 12 of the 22 fail against
the pre-fix code (8c29880) and all 22 pass against this commit; the
other 10 assert behaviour that did not change.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@frigidom1024
frigidom1024 force-pushed the fix/long-term-silent-data-loss branch from d808c23 to dbf5e21 Compare September 12, 2026 13:09
@codecov-commenter

codecov-commenter commented Sep 13, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@0303dc0). Learn more about missing BASE report.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #256   +/-   ##
=======================================
  Coverage        ?   68.22%           
=======================================
  Files           ?      140           
  Lines           ?    13745           
  Branches        ?     2024           
=======================================
  Hits            ?     9377           
  Misses          ?     3796           
  Partials        ?      572           
Flag Coverage Δ
integration 45.58% <90.90%> (?)
merged 68.22% <100.00%> (?)
unit 53.74% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/neo4j_agent_memory/graph/queries.py 100.00% <ø> (ø)
src/neo4j_agent_memory/graph/query_builder.py 91.30% <100.00%> (ø)
src/neo4j_agent_memory/memory/long_term.py 79.80% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

# Bug: add_relationship silently creates no edges when target entity already exists

2 participants