Skip to content

merge_nodes_in_tx bypasses cardinality/reverse_cardinality enforcement #2908

Closed

Description

@mstomar125

SqliteStore::merge_nodes_in_tx (the node-ID-conflict/dedup merge path, packages/core/src/db/sqlite_store/conflicts.rs around line 461) re-points relationship edges when merging a duplicate node into a survivor via a raw UPDATE relationship SET in_node = ?, out_node = ? — it never checks cardinality or reverse_cardinality, and bypasses create_relationship/create_relationship_in_tx entirely.

Why it matters

core#2804 added enforcement of cardinality: One and reverse_cardinality: One at create_relationship/create_relationship_in_tx — the only two write paths that create a relationship edge through the normal API. merge_nodes_in_tx is a third path that also produces (repoints) relationship edges, and it has no equivalent check.

Concretely: if node A and node B are both sources of a cardinality: One relationship (or both point at the same reverse_cardinality: One target from different declaring nodes), and a dedup merge re-points one of A's or B's edges onto the survivor of a conflict resolution, the survivor can end up with two live edges into (or out of) a cardinality-one end — silently violating the exact invariant core#2804 exists to enforce, through a code path that was never touched by that fix.

This is pre-existing behavior, not something core#2804 introduced — but core#2804 is what makes this invariant load-bearing for the first time (declared relationships now assume create_relationship is the only door in). Found during review of core#2804's implementation.

Suggested approach

  • Either route merge_nodes_in_tx's edge re-pointing through the same cardinality/reverse_cardinality checks create_relationship_in_tx uses (reusing that logic rather than duplicating it), or
  • After a repoint, detect and resolve any resulting cardinality violation (e.g. keep the survivor's edge, drop the loser's, the same way create_relationship's replace semantics would have).

Acceptance criteria

  • merge_nodes_in_tx cannot leave a cardinality: One source or reverse_cardinality: One target with more than one live edge after a merge
  • Test: two nodes each hold a cardinality-one-conflicting edge before a merge; after merging one into the other, exactly one edge remains
  • Existing merge_nodes tests (merge_nodes_test.rs) still pass unchanged

Activity

  1. self-assigned this
    on Sep 25, 2026
  2. mstomar125 commented on Sep 25, 2026

    @mstomar125
    ContributorAuthor

    Frontend baseline: 5268 passed, 1 failed (pre-existing flaky perf test architecture-benchmarks.test.ts cold-cache timing, unrelated to this Rust conflict-resolution fix). Test Files: 261 passed, 1 failed (262 total).

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions