fix: edge indexes become invalid when deleting and recreating same edge (#3097)#3482
fix: edge indexes become invalid when deleting and recreating same edge (#3097)#3482
Conversation
…ge (#3097) In TransactionIndexContext.addIndexKeyLock(), when a unique index entry is deleted (REMOVE) and recreated (ADD) with the same key within the same transaction on the same bucket-level index, the REMOVE entry was silently replaced by a REPLACE entry — losing the old RID. At commit time the old persisted index entry was never removed, causing stale entries to accumulate. After 2+ iterations, checkUniqueIndexKeys detected >2 entries for the same unique key and threw DuplicatedKeyException. Fix: store the old RID in the REPLACE entry (IndexKey.oldRid). At commit time, call index.remove(key, oldRid) for REPLACE entries with a non-null oldRid to properly clean up the old persisted entry. Also fix getTxDeletedEntries() to use oldRid as the deleted RID for correctness in checkUniqueIndexKeys. Update TxForwardRequest serialization to include oldRid for HA replication. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary of ChangesHello @robfrank, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses a critical bug where unique edge indexes could become invalid when an edge was deleted and then immediately recreated with the same key within the same transaction. Previously, this sequence of operations would lead to stale index entries and eventually "DuplicatedKeyException". The fix introduces a mechanism to correctly track and remove the old Record ID during such "REPLACE" operations, ensuring index integrity and proper cleanup, and extends this correction to high-availability replication. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request fixes an issue where deleting and recreating an edge with a unique index within the same transaction could lead to stale index entries and DuplicatedKeyException in subsequent operations. The fix introduces an oldRid field in IndexKey to track the RID of a replaced entry, ensuring it's properly removed at commit time. The changes also update HA transaction forwarding to include this new field.
The core logic seems correct, and the accompanying test has been improved to be more robust. I've found one potential issue in TransactionIndexContext where the oldRid is not propagated correctly in all cases, which could still lead to stale entries in more complex transaction scenarios. Please see my detailed comment.
engine/src/main/java/com/arcadedb/database/TransactionIndexContext.java
Outdated
Show resolved
Hide resolved
Code ReviewThis PR fixes a real and subtle bug: when an edge (or any record with a unique index) is deleted and recreated within the same transaction, the in-memory index context merges the REMOVE+ADD into a REPLACE, but the old persisted RID was never cleaned up at commit time. After the second such round-trip the stale entry causes a false Correctness
In the updated
The HA SerializationThe Backward compatibility concern: The comment in Code StyleThe Test CoverageLooping 10 times rather than just 2 is a meaningful improvement — it validates the fix across multiple iterations, not just the first recurrence. Using One gap: there is no assertion that the edge is still correctly queryable after each recreate cycle. The test only checks that no exception is thrown. Adding a There is also no HA-level test verifying that the SummaryThe fix is correct and well-targeted. Items to consider before merging:
Overall, this is a solid fix for a tricky transactional index bug. Good work. |
🧪 CI InsightsHere's what we observed from your CI run for 585c393. 🟢 All jobs passed!But CI Insights is watching 👀 |
When the same unique key undergoes REMOVE → ADD → ADD in the same transaction on the same bucket, the second ADD finds an existing REPLACE entry. The oldRid from that REPLACE must be propagated to the new REPLACE so the original persisted index entry is still properly removed at commit time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code ReviewThis PR fixes a real and subtle bug in Correctness / Potential Issues1.
// Current:
public RID oldRid; // for REPLACE created from same-bucket REMOVE→ADD: the old RID being replaced
public IndexKeyOperation operation;
2. Before the fix, only if (existent == null || entry.getValue().operation == IndexKey.IndexKeyOperation.REMOVE) {
final RID deletedRid = (entry.getValue().operation == IndexKey.IndexKeyOperation.REPLACE && entry.getValue().oldRid != null)
? entry.getValue().oldRid
: entry.getKey().rid;
entries.put(key, deletedRid);
}A REPLACE with a non-null 3. HA serialisation — no version guard or backward compatibility handling
Consider bumping the serialization format version (if one exists) or adding a note in the PR description about the requirement for a full-cluster restart when upgrading. Design / Style4. The new field is set immediately after construction: final TransactionIndexContext.IndexKey v = new TransactionIndexContext.IndexKey(index.isUnique(), operation, keyValues, rid);
if (operation == TransactionIndexContext.IndexKey.IndexKeyOperation.REPLACE) {
final byte hasOldRidFlag = uniqueKeysBuffer.getByte();
if (hasOldRidFlag == 1)
v.oldRid = new RID(database, ...);
}Adding a constructor that accepts 5. Comment style — inline uppercase explanatory comments are non-standard // REMOVE THE OLD RID THAT WAS REPLACED BY A NEW ONE IN THE SAME BUCKET
// SAVE THE OLD RID SO IT CAN BE PROPERLY REMOVED FROM THE PERSISTED INDEX AT COMMIT TIMEThe rest of the file uses mixed-case comments. Shouting-case is typically reserved for 6. final byte hasOldRidFlag = uniqueKeysBuffer.getByte();
if (hasOldRidFlag == 1)A local Test Coverage7. Test improvements are good — a few suggestions for completeness The loop-10 approach nicely exercises the repeated delete/recreate scenario. Two additional cases worth considering:
8. Test assertion message assertThat(result.<Long>getProperty("edgeCount")).isEqualTo(1);Adding SummaryThe core fix is correct and the logic in
Overall this is a well-reasoned fix for a tricky transactional index bug. Addressing the HA versioning concern before merge would be prudent. |
Coverage summary from CodacySee diff coverage on Codacy
Coverage variation details
Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: Diff coverage details
Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: See your quality gate settings Change summary preferences |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3482 +/- ##
==========================================
+ Coverage 63.15% 63.88% +0.72%
==========================================
Files 1299 1299
Lines 86378 86427 +49
Branches 17767 17789 +22
==========================================
+ Hits 54552 55212 +660
+ Misses 24087 23419 -668
- Partials 7739 7796 +57 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…ge (#3097) (#3482) * fix: edge indexes become invalid when deleting and recreating same edge (#3097) In TransactionIndexContext.addIndexKeyLock(), when a unique index entry is deleted (REMOVE) and recreated (ADD) with the same key within the same transaction on the same bucket-level index, the REMOVE entry was silently replaced by a REPLACE entry — losing the old RID. At commit time the old persisted index entry was never removed, causing stale entries to accumulate. After 2+ iterations, checkUniqueIndexKeys detected >2 entries for the same unique key and threw DuplicatedKeyException. Fix: store the old RID in the REPLACE entry (IndexKey.oldRid). At commit time, call index.remove(key, oldRid) for REPLACE entries with a non-null oldRid to properly clean up the old persisted entry. Also fix getTxDeletedEntries() to use oldRid as the deleted RID for correctness in checkUniqueIndexKeys. Update TxForwardRequest serialization to include oldRid for HA replication. * fix: propagate oldRid through chained REPLACE operations (#3097) When the same unique key undergoes REMOVE → ADD → ADD in the same transaction on the same bucket, the second ADD finds an existing REPLACE entry. The oldRid from that REPLACE must be propagated to the new REPLACE so the original persisted index entry is still properly removed at commit time. * test: assert edge count equals 1 after delete-recreate loop (#3097) (cherry picked from commit dffb415)
…ge (#3097) (#3482) * fix: edge indexes become invalid when deleting and recreating same edge (#3097) In TransactionIndexContext.addIndexKeyLock(), when a unique index entry is deleted (REMOVE) and recreated (ADD) with the same key within the same transaction on the same bucket-level index, the REMOVE entry was silently replaced by a REPLACE entry — losing the old RID. At commit time the old persisted index entry was never removed, causing stale entries to accumulate. After 2+ iterations, checkUniqueIndexKeys detected >2 entries for the same unique key and threw DuplicatedKeyException. Fix: store the old RID in the REPLACE entry (IndexKey.oldRid). At commit time, call index.remove(key, oldRid) for REPLACE entries with a non-null oldRid to properly clean up the old persisted entry. Also fix getTxDeletedEntries() to use oldRid as the deleted RID for correctness in checkUniqueIndexKeys. Update TxForwardRequest serialization to include oldRid for HA replication. * fix: propagate oldRid through chained REPLACE operations (#3097) When the same unique key undergoes REMOVE → ADD → ADD in the same transaction on the same bucket, the second ADD finds an existing REPLACE entry. The oldRid from that REPLACE must be propagated to the new REPLACE so the original persisted index entry is still properly removed at commit time. * test: assert edge count equals 1 after delete-recreate loop (#3097) (cherry picked from commit dffb415)
In TransactionIndexContext.addIndexKeyLock(), when a unique index entry is deleted (REMOVE) and recreated (ADD) with the same key within the same transaction on the same bucket-level index, the REMOVE entry was silently replaced by a REPLACE entry — losing the old RID. At commit time the old persisted index entry was never removed, causing stale entries to accumulate. After 2+ iterations, checkUniqueIndexKeys detected >2 entries for the same unique key and threw DuplicatedKeyException.
Fix: store the old RID in the REPLACE entry (IndexKey.oldRid). At commit time, call index.remove(key, oldRid) for REPLACE entries with a non-null oldRid to properly clean up the old persisted entry. Also fix getTxDeletedEntries() to use oldRid as the deleted RID for correctness in checkUniqueIndexKeys. Update TxForwardRequest serialization to include oldRid for HA replication.