From d4308e13c7477ef757e5af54216fdc4e8dd784c2 Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 19 Feb 2026 13:12:49 +0100 Subject: [PATCH 1/2] fix: edge indexes become invalid when deleting and recreating same edge (#3097) (#3482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 dffb415297e5584b69f9a2a9ead4113a8497fa97) --- bolt/pom.xml | 2 +- console/pom.xml | 2 +- coverage/pom.xml | 2 +- e2e-perf/pom.xml | 2 +- e2e/pom.xml | 2 +- engine/pom.xml | 2 +- .../database/TransactionIndexContext.java | 23 +++++- .../graph/EdgeIndexDuplicateKeyTest.java | 81 +++++++++++-------- graphql/pom.xml | 2 +- gremlin/pom.xml | 2 +- grpc-client/pom.xml | 2 +- grpc/pom.xml | 2 +- grpcw/pom.xml | 2 +- integration/pom.xml | 2 +- metrics/pom.xml | 2 +- mongodbw/pom.xml | 2 +- network/pom.xml | 2 +- package/pom.xml | 2 +- pom.xml | 2 +- postgresw/pom.xml | 2 +- redisw/pom.xml | 2 +- server/pom.xml | 2 +- .../server/ha/message/TxForwardRequest.java | 14 ++++ studio/package.json | 2 +- studio/pom.xml | 22 ++--- test-utils/pom.xml | 2 +- 26 files changed, 116 insertions(+), 68 deletions(-) diff --git a/bolt/pom.xml b/bolt/pom.xml index 7617a876c1..12598380ba 100644 --- a/bolt/pom.xml +++ b/bolt/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/console/pom.xml b/console/pom.xml index e4d126e723..4b6cf61a25 100644 --- a/console/pom.xml +++ b/console/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/coverage/pom.xml b/coverage/pom.xml index 5b0604d82c..ce32f9e756 100644 --- a/coverage/pom.xml +++ b/coverage/pom.xml @@ -26,7 +26,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/e2e-perf/pom.xml b/e2e-perf/pom.xml index 321aad240a..aed2468a33 100644 --- a/e2e-perf/pom.xml +++ b/e2e-perf/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/e2e/pom.xml b/e2e/pom.xml index 0e42fe442f..db58400937 100644 --- a/e2e/pom.xml +++ b/e2e/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/engine/pom.xml b/engine/pom.xml index 55e0a504e3..39387a46ad 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/engine/src/main/java/com/arcadedb/database/TransactionIndexContext.java b/engine/src/main/java/com/arcadedb/database/TransactionIndexContext.java index 279cebcc29..26419eac0a 100644 --- a/engine/src/main/java/com/arcadedb/database/TransactionIndexContext.java +++ b/engine/src/main/java/com/arcadedb/database/TransactionIndexContext.java @@ -42,6 +42,7 @@ public static class IndexKey { public final boolean unique; public final Object[] keyValues; public final RID rid; + public RID oldRid; // for REPLACE created from same-bucket REMOVE→ADD: the old RID being replaced public IndexKeyOperation operation; public enum IndexKeyOperation { @@ -174,6 +175,9 @@ public void commit() { for (final IndexKey key : values) { if (key.operation == IndexKey.IndexKeyOperation.REMOVE) index.remove(key.keyValues, key.rid); + else if (key.operation == IndexKey.IndexKeyOperation.REPLACE && key.oldRid != null) + // REMOVE THE OLD RID THAT WAS REPLACED BY A NEW ONE IN THE SAME BUCKET + index.remove(key.keyValues, key.oldRid); } } } @@ -290,6 +294,14 @@ public void addIndexKeyLock(final IndexInternal index, IndexKey.IndexKeyOperatio // REPLACE EXISTENT WITH THIS v.operation = IndexKey.IndexKeyOperation.REPLACE; + if (entry != null) { + if (entry.operation == IndexKey.IndexKeyOperation.REMOVE) + // SAVE THE OLD RID SO IT CAN BE PROPERLY REMOVED FROM THE PERSISTED INDEX AT COMMIT TIME + v.oldRid = entry.rid; + else if (entry.operation == IndexKey.IndexKeyOperation.REPLACE) + // PROPAGATE THE OLD RID FROM THE PREVIOUS REPLACE OPERATION (e.g. REMOVE → ADD → ADD) + v.oldRid = entry.oldRid; + } } } } @@ -418,9 +430,14 @@ private Map> getTxDeletedEntries() { final ComparableKey key = new ComparableKey(entry.getValue().keyValues); final RID existent = entries.get(key); - if (existent == null || entry.getValue().operation == IndexKey.IndexKeyOperation.REMOVE) - // MULTIPLE OPERATIONS ON THE SAME KEY (DIFFERENT BUCKETS), PREFER THE REMOVE ONE - entries.put(key, entry.getKey().rid); + if (existent == null || entry.getValue().operation == IndexKey.IndexKeyOperation.REMOVE) { + // MULTIPLE OPERATIONS ON THE SAME KEY (DIFFERENT BUCKETS), PREFER THE REMOVE ONE. + // For REPLACE entries that originated from a same-bucket REMOVE→ADD merge, use the oldRid (the actual deleted RID). + final RID deletedRid = (entry.getValue().operation == IndexKey.IndexKeyOperation.REPLACE && entry.getValue().oldRid != null) + ? entry.getValue().oldRid + : entry.getKey().rid; + entries.put(key, deletedRid); + } } } } diff --git a/engine/src/test/java/com/arcadedb/graph/EdgeIndexDuplicateKeyTest.java b/engine/src/test/java/com/arcadedb/graph/EdgeIndexDuplicateKeyTest.java index 0e85165f43..a383ce9830 100644 --- a/engine/src/test/java/com/arcadedb/graph/EdgeIndexDuplicateKeyTest.java +++ b/engine/src/test/java/com/arcadedb/graph/EdgeIndexDuplicateKeyTest.java @@ -19,8 +19,11 @@ package com.arcadedb.graph; import com.arcadedb.TestHelper; +import com.arcadedb.query.sql.executor.Result; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * Test for issue #3097: Edge indexes become invalid in certain scenario #2 * Reproduces DuplicatedKeyException when deleting and recreating the same edge multiple times. @@ -33,49 +36,63 @@ class EdgeIndexDuplicateKeyTest extends TestHelper { void edgeDeleteAndRecreateMultipleTimes() { // Transaction #1: Create schema database.transaction(() -> { - database.command("sql", "CREATE VERTEX TYPE duct"); - database.command("sql", "CREATE VERTEX TYPE trs"); - database.command("sql", "CREATE PROPERTY duct.id STRING"); - database.command("sql", "CREATE INDEX ON duct (id) UNIQUE"); - database.command("sql", "CREATE PROPERTY trs.id STRING"); - database.command("sql", "CREATE INDEX ON trs (id) UNIQUE"); - database.command("sql", "CREATE EDGE TYPE trs_duct"); - database.command("sql", "CREATE PROPERTY trs_duct.from_id STRING"); - database.command("sql", "CREATE INDEX ON trs_duct (from_id) NOTUNIQUE"); - database.command("sql", "CREATE PROPERTY trs_duct.to_id STRING"); - database.command("sql", "CREATE INDEX ON trs_duct (to_id) NOTUNIQUE"); - database.command("sql", "CREATE PROPERTY trs_duct.swap STRING"); - database.command("sql", "CREATE PROPERTY trs_duct.order_number INTEGER"); - database.command("sql", "CREATE INDEX ON trs_duct (from_id,to_id,swap,order_number) UNIQUE"); + database.command("sqlscript", """ + CREATE VERTEX TYPE duct; + CREATE VERTEX TYPE trs; + CREATE PROPERTY duct.id STRING; + CREATE INDEX ON duct (id) UNIQUE; + CREATE PROPERTY trs.id STRING; + CREATE INDEX ON trs (id) UNIQUE; + CREATE EDGE TYPE trs_duct; + CREATE PROPERTY trs_duct.from_id STRING; + CREATE INDEX ON trs_duct (from_id) NOTUNIQUE; + CREATE PROPERTY trs_duct.to_id STRING; + CREATE INDEX ON trs_duct (to_id) NOTUNIQUE; + CREATE PROPERTY trs_duct.swap STRING; + CREATE PROPERTY trs_duct.order_number INTEGER; + CREATE INDEX ON trs_duct (from_id,to_id,swap,order_number) UNIQUE; + """); }); // Transaction #2: Insert vertices and create edge database.transaction(() -> { - database.command("sql", "INSERT INTO duct (id) VALUES ('duct_1')"); - database.command("sql", "INSERT INTO trs (id) VALUES ('trs_1')"); - database.command("sql", - "CREATE EDGE trs_duct from (SELECT FROM trs WHERE id='trs_1') to (SELECT FROM duct WHERE id='duct_1') " + - "SET from_id='trs_1', to_id='duct_1', swap='N', order_number=1"); + database.command("sqlscript", """ + INSERT INTO duct (id) VALUES ('duct_1'); + INSERT INTO trs (id) VALUES ('trs_1'); + + CREATE EDGE trs_duct + from (SELECT FROM trs WHERE id='trs_1') + to (SELECT FROM duct WHERE id='duct_1') + SET from_id='trs_1', to_id='duct_1', swap='N', order_number=1"""); }); // Transaction #3: Delete and recreate edge (first time - should work) database.transaction(() -> { - database.command("sql", - "DELETE FROM trs_duct WHERE (from_id='trs_1') AND (to_id='duct_1') AND (swap='N') AND (order_number=1)"); - database.command("sql", - "CREATE EDGE trs_duct from (SELECT FROM trs WHERE id='trs_1') to (SELECT FROM duct WHERE id='duct_1') " + - "SET from_id='trs_1', to_id='duct_1', swap='N', order_number=1"); + database.command("sqlscript", """ + DELETE FROM trs_duct WHERE (from_id='trs_1') AND (to_id='duct_1') AND (swap='N') AND (order_number=1); + + CREATE EDGE trs_duct + from (SELECT FROM trs WHERE id='trs_1') + to (SELECT FROM duct WHERE id='duct_1') + SET from_id='trs_1', to_id='duct_1', swap='N', order_number=1"""); }); // Transaction #4: Delete and recreate edge (second time - this should NOT throw DuplicatedKeyException) - database.transaction(() -> { - database.command("sql", - "DELETE FROM trs_duct WHERE (from_id='trs_1') AND (to_id='duct_1') AND (swap='N') AND (order_number=1)"); - database.command("sql", - "CREATE EDGE trs_duct from (SELECT FROM trs WHERE id='trs_1') to (SELECT FROM duct WHERE id='duct_1') " + - "SET from_id='trs_1', to_id='duct_1', swap='N', order_number=1"); - }); + for (int i = 0; i < 10; i++) { + database.transaction(() -> { + database.command("sqlscript", """ + DELETE FROM trs_duct WHERE (from_id='trs_1') AND (to_id='duct_1') AND (swap='N') AND (order_number=1); + + CREATE EDGE trs_duct + from (SELECT FROM trs WHERE id='trs_1') + to (SELECT FROM duct WHERE id='duct_1') + SET from_id='trs_1', to_id='duct_1', swap='N', order_number=1"""); + }); + } - // If we got here without exception, the test passes + Result result = database.query("sql", + "SELECT COUNT(*) AS edgeCount FROM trs_duct WHERE from_id='trs_1' AND to_id='duct_1' AND swap='N' AND order_number=1") + .next(); + assertThat(result.getProperty("edgeCount")).isEqualTo(1); } } diff --git a/graphql/pom.xml b/graphql/pom.xml index e04dbea28a..94fe85e417 100644 --- a/graphql/pom.xml +++ b/graphql/pom.xml @@ -24,7 +24,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/gremlin/pom.xml b/gremlin/pom.xml index 37303e4239..7a74edbcdc 100644 --- a/gremlin/pom.xml +++ b/gremlin/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/grpc-client/pom.xml b/grpc-client/pom.xml index 8a5facbeab..65fcba2779 100644 --- a/grpc-client/pom.xml +++ b/grpc-client/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/grpc/pom.xml b/grpc/pom.xml index 73bf9dd5c5..635db76086 100644 --- a/grpc/pom.xml +++ b/grpc/pom.xml @@ -22,7 +22,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/grpcw/pom.xml b/grpcw/pom.xml index c49ad13bdc..2665a9ca41 100644 --- a/grpcw/pom.xml +++ b/grpcw/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/integration/pom.xml b/integration/pom.xml index 4dee2c03d2..0d85b5e0e1 100644 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/metrics/pom.xml b/metrics/pom.xml index ee793259e0..ce6bf5a7bd 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/mongodbw/pom.xml b/mongodbw/pom.xml index 8948aff94b..eca6b837e3 100644 --- a/mongodbw/pom.xml +++ b/mongodbw/pom.xml @@ -24,7 +24,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/network/pom.xml b/network/pom.xml index b8cd550b2d..e328a086d6 100644 --- a/network/pom.xml +++ b/network/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/package/pom.xml b/package/pom.xml index f4118504fd..5bd5633dbf 100644 --- a/package/pom.xml +++ b/package/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index e8a9599dd9..eccdd889ba 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent pom - 26.2.1 + 26.2.2-SNAPSHOT ArcadeDB https://arcadedata.com/ diff --git a/postgresw/pom.xml b/postgresw/pom.xml index 8176c823a1..4f3ced9bea 100644 --- a/postgresw/pom.xml +++ b/postgresw/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/redisw/pom.xml b/redisw/pom.xml index 4421514b11..4c81102f6d 100644 --- a/redisw/pom.xml +++ b/redisw/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/server/pom.xml b/server/pom.xml index 81e84c038a..e4de74d455 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml diff --git a/server/src/main/java/com/arcadedb/server/ha/message/TxForwardRequest.java b/server/src/main/java/com/arcadedb/server/ha/message/TxForwardRequest.java index 2d639c2b20..ccbf56b6f8 100755 --- a/server/src/main/java/com/arcadedb/server/ha/message/TxForwardRequest.java +++ b/server/src/main/java/com/arcadedb/server/ha/message/TxForwardRequest.java @@ -160,6 +160,15 @@ protected void writeIndexKeysToBuffer(final DatabaseInternal database, uniqueKeysBuffer.putByte((byte) key.operation.ordinal()); uniqueKeysBuffer.putUnsignedNumber(key.rid.getBucketId()); uniqueKeysBuffer.putUnsignedNumber(key.rid.getPosition()); + if (key.operation == TransactionIndexContext.IndexKey.IndexKeyOperation.REPLACE) { + // Serialize oldRid for REPLACE entries (introduced to fix same-bucket REMOVE→ADD merge) + final boolean hasOldRid = key.oldRid != null; + uniqueKeysBuffer.putByte((byte) (hasOldRid ? 1 : 0)); + if (hasOldRid) { + uniqueKeysBuffer.putUnsignedNumber(key.oldRid.getBucketId()); + uniqueKeysBuffer.putUnsignedNumber(key.oldRid.getPosition()); + } + } } } } @@ -211,6 +220,11 @@ protected Map com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml @@ -72,16 +72,16 @@ - - npm audit - - npm - - compile - - audit --audit-level=moderate - - + + + + + + + + + + diff --git a/test-utils/pom.xml b/test-utils/pom.xml index 8c99d5a255..1ae3983b80 100644 --- a/test-utils/pom.xml +++ b/test-utils/pom.xml @@ -25,7 +25,7 @@ com.arcadedb arcadedb-parent - 26.2.1 + 26.2.2-SNAPSHOT ../pom.xml From 484414de73869e727c91c8b54ad68d0796a0adeb Mon Sep 17 00:00:00 2001 From: robfrank Date: Tue, 24 Feb 2026 09:00:58 +0100 Subject: [PATCH 2/2] remove upgrade of version --- .github/workflows/mvn-release.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/mvn-release.yml b/.github/workflows/mvn-release.yml index 593a647a9d..3c904d3659 100644 --- a/.github/workflows/mvn-release.yml +++ b/.github/workflows/mvn-release.yml @@ -81,22 +81,3 @@ jobs: - name: Set next development version if: success() run: mvn versions:set "-DnewVersion=${{ github.event.inputs.nextversion }}" --no-transfer-progress - - - name: Update studio package.json to next development version - if: success() - run: | - cd studio - jq --arg version "${{ github.event.inputs.nextversion }}" '.version = $version' package.json > package.json.tmp - jq --arg version "${{ github.event.inputs.nextversion }}" '.version = $version' package-lock.json > package-lock.json.tmp - mv package.json.tmp package.json - mv package-lock.json.tmp package-lock.json - - - name: Commit next development version - if: success() - run: | - git config user.email "actions@github.com" - git config user.name "GitHub Actions" - git commit -am "Set next development version to ${{ github.event.inputs.nextversion }}" - git push origin main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}