Skip to content

fix(memory): prevent unique index violation in insert_or_supersede#3639

Merged
bug-ops merged 2 commits intomainfrom
3635-memory-supersede-unique-index
May 6, 2026
Merged

fix(memory): prevent unique index violation in insert_or_supersede#3639
bug-ops merged 2 commits intomainfrom
3635-memory-supersede-unique-index

Conversation

@bug-ops
Copy link
Copy Markdown
Owner

@bug-ops bug-ops commented May 6, 2026

Summary

  • insert_or_supersede_with_metrics violated uq_graph_edges_active_head (partial unique index on (source_entity_id, target_entity_id, canonical_relation, edge_type) WHERE valid_to IS NULL AND expired_at IS NULL) because insert_new_edge ran before the prior head was deactivated
  • SQLite enforces unique indexes at statement level, not at transaction commit — a DEFERRED transaction does not help
  • Fix: split invalidate_prior_head into expire_prior_head (sets valid_to/expired_at before INSERT) and set_superseded_by (back-fills superseded_by after INSERT), both within the same transaction

Root cause

// Before (buggy):
insert_new_edge(...)          -- unique index violated if old head still active
invalidate_prior_head(...)    -- too late

// After (correct):
expire_prior_head(...)        -- deactivates old head; index now clear
insert_new_edge(...)          -- no conflict
set_superseded_by(...)        -- back-fills superseded_by with new row ID

Test plan

  • cargo nextest run -p zeph-memory --lib — 1272 passed, regression test insert_or_supersede_same_tuple_no_unique_index_violation covers the exact bug scenario
  • cargo +nightly fmt --check — CLEAN
  • cargo clippy -p zeph-memory --tests -- -D warnings — CLEAN

Closes #3635

SQLite enforces unique indexes at statement level, not at transaction
commit. When insert_or_supersede_with_metrics had a prior active head,
insert_new_edge ran before invalidate_prior_head, momentarily leaving
two active rows for the same (source_entity_id, target_entity_id,
canonical_relation, edge_type) tuple and triggering
SQLITE_CONSTRAINT_UNIQUE on uq_graph_edges_active_head.

Split invalidate_prior_head into two helpers:
- expire_prior_head: sets valid_to and expired_at before the INSERT
- set_superseded_by: back-fills superseded_by after the INSERT returns new_id

Both run inside the same transaction, preserving atomicity.

Closes #3635
@github-actions github-actions Bot added memory zeph-memory crate (SQLite) rust Rust code changes labels May 6, 2026
@github-actions github-actions Bot added bug Something isn't working size/M Medium PR (51-200 lines) documentation Improvements or additions to documentation labels May 6, 2026
@bug-ops bug-ops merged commit ca078b2 into main May 6, 2026
32 checks passed
@bug-ops bug-ops deleted the 3635-memory-supersede-unique-index branch May 6, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation memory zeph-memory crate (SQLite) rust Rust code changes size/M Medium PR (51-200 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(memory): insert_or_supersede_with_metrics violates unique index on supersede

1 participant