fix: UPDATE EDGE SET @in/@out correctly rewires vertex edge lists#4074
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Coverage variation | ✅ -7.25% coverage variation |
| Diff coverage | ✅ 84.78% diff coverage |
Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (76f212a) 121624 88477 72.75% Head commit (a1692c4) 153017 (+31393) 100228 (+11751) 65.50% (-7.25%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>
Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#4074) 46 39 84.78% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This pull request introduces the ability to move edges between vertices by updating the @in and @out properties in MutableEdge. It adds a moveEdge method to GraphEngine to handle the necessary updates to vertex edge lists to maintain graph consistency. Feedback suggests addressing potential graph inconsistencies when setting endpoints to null and optimizing moveEdge by reducing redundant database lookups and adding direction validation.
| return this; | ||
| if ("@in".equals(name)) { | ||
| final RID newIn = toRID(value); | ||
| if (rid != null && newIn != null && !newIn.equals(this.in) && database instanceof DatabaseInternal dbInt) |
There was a problem hiding this comment.
When newIn (or newOut) is null, the moveEdge logic is skipped, but the field is still updated. This results in the edge being disconnected from its current vertex without updating that vertex's edge list, leading to graph inconsistency (stale pointers). Since ArcadeDB edges require both endpoints, setting @in or @out to null should either be prohibited or handled by properly removing the edge from the old vertex's list.
There was a problem hiding this comment.
Fixed in 6678316. Setting @in or @out to null now throws IllegalArgumentException immediately, before any field mutation. The newIn != null guard was also removed from the main condition since we now reject null upfront.
| public void moveEdge(final MutableEdge edge, final Vertex.DIRECTION direction, final RID newVertexRID) { | ||
| if (direction == Vertex.DIRECTION.IN) { | ||
| final RID oldIn = edge.getIn(); | ||
| final RID out = edge.getOut(); | ||
| // Remove from old in-vertex's IN list | ||
| if (oldIn != null) { | ||
| try { | ||
| final VertexInternal oldVIn = (VertexInternal) database.lookupByRID(oldIn, false); | ||
| final EdgeLinkedList inEdges = getEdgeHeadChunk(oldVIn, Vertex.DIRECTION.IN); | ||
| if (inEdges != null) | ||
| inEdges.removeEdge(edge); | ||
| } catch (final RecordNotFoundException ignored) { | ||
| } | ||
| } | ||
| // Remove from out-vertex's OUT list (its stored destination is stale after the move) | ||
| if (out != null) { | ||
| try { | ||
| final VertexInternal vOut = (VertexInternal) database.lookupByRID(out, false); | ||
| final EdgeLinkedList outEdges = getEdgeHeadChunk(vOut, Vertex.DIRECTION.OUT); | ||
| if (outEdges != null) | ||
| outEdges.removeEdge(edge); | ||
| } catch (final RecordNotFoundException ignored) { | ||
| } | ||
| } | ||
| edge.setIn(newVertexRID); | ||
| // Re-add to out-vertex's OUT list pointing to the new in-vertex | ||
| if (out != null) | ||
| connectOutgoingEdge((VertexInternal) database.lookupByRID(out, false), database.lookupByRID(newVertexRID, false), edge); | ||
| // Add to new in-vertex's IN list | ||
| connectIncomingEdge(database.lookupByRID(newVertexRID, false), out, edge.getIdentity()); | ||
| } else { | ||
| final RID oldOut = edge.getOut(); | ||
| final RID in = edge.getIn(); | ||
| // Remove from old out-vertex's OUT list | ||
| if (oldOut != null) { | ||
| try { | ||
| final VertexInternal oldVOut = (VertexInternal) database.lookupByRID(oldOut, false); | ||
| final EdgeLinkedList outEdges = getEdgeHeadChunk(oldVOut, Vertex.DIRECTION.OUT); | ||
| if (outEdges != null) | ||
| outEdges.removeEdge(edge); | ||
| } catch (final RecordNotFoundException ignored) { | ||
| } | ||
| } | ||
| // Remove from in-vertex's IN list (its stored source is stale after the move) | ||
| if (in != null) { | ||
| try { | ||
| final VertexInternal vIn = (VertexInternal) database.lookupByRID(in, false); | ||
| final EdgeLinkedList inEdges = getEdgeHeadChunk(vIn, Vertex.DIRECTION.IN); | ||
| if (inEdges != null) | ||
| inEdges.removeEdge(edge); | ||
| } catch (final RecordNotFoundException ignored) { | ||
| } | ||
| } | ||
| edge.setOut(newVertexRID); | ||
| // Add to new out-vertex's OUT list | ||
| connectOutgoingEdge((VertexInternal) database.lookupByRID(newVertexRID, false), database.lookupByRID(in, false), edge); | ||
| // Re-add to in-vertex's IN list with the new source | ||
| if (in != null) | ||
| connectIncomingEdge(database.lookupByRID(in, false), newVertexRID, edge.getIdentity()); | ||
| } | ||
| } |
There was a problem hiding this comment.
The moveEdge method contains redundant database lookups for the same RIDs and duplicated logic between the IN and OUT direction handling. Specifically, newVertexRID and the other end of the edge are looked up multiple times. Reusing the vertex objects from the initial lookups and adding explicit validation for the direction parameter (to avoid issues if BOTH is passed) would improve performance and code quality.
public void moveEdge(final MutableEdge edge, final Vertex.DIRECTION direction, final RID newVertexRID) {
final Identifiable vNew = database.lookupByRID(newVertexRID, false);
if (direction == Vertex.DIRECTION.IN) {
final RID oldIn = edge.getIn();
final RID out = edge.getOut();
if (oldIn != null) {
try {
final VertexInternal oldVIn = (VertexInternal) database.lookupByRID(oldIn, false);
final EdgeLinkedList inEdges = getEdgeHeadChunk(oldVIn, Vertex.DIRECTION.IN);
if (inEdges != null)
inEdges.removeEdge(edge);
} catch (final RecordNotFoundException ignored) {
}
}
VertexInternal vOut = null;
if (out != null) {
try {
vOut = (VertexInternal) database.lookupByRID(out, false);
final EdgeLinkedList outEdges = getEdgeHeadChunk(vOut, Vertex.DIRECTION.OUT);
if (outEdges != null)
outEdges.removeEdge(edge);
} catch (final RecordNotFoundException ignored) {
}
}
edge.setIn(newVertexRID);
if (vOut != null)
connectOutgoingEdge(vOut, vNew, edge);
connectIncomingEdge(vNew, out, edge.getIdentity());
} else if (direction == Vertex.DIRECTION.OUT) {
final RID oldOut = edge.getOut();
final RID in = edge.getIn();
if (oldOut != null) {
try {
final VertexInternal oldVOut = (VertexInternal) database.lookupByRID(oldOut, false);
final EdgeLinkedList outEdges = getEdgeHeadChunk(oldVOut, Vertex.DIRECTION.OUT);
if (outEdges != null)
outEdges.removeEdge(edge);
} catch (final RecordNotFoundException ignored) {
}
}
VertexInternal vIn = null;
if (in != null) {
try {
vIn = (VertexInternal) database.lookupByRID(in, false);
final EdgeLinkedList inEdges = getEdgeHeadChunk(vIn, Vertex.DIRECTION.IN);
if (inEdges != null)
inEdges.removeEdge(edge);
} catch (final RecordNotFoundException ignored) {
}
}
edge.setOut(newVertexRID);
connectOutgoingEdge((VertexInternal) vNew, vIn != null ? vIn : (in != null ? database.lookupByRID(in, false) : null), edge);
if (in != null)
connectIncomingEdge(vIn != null ? vIn : database.lookupByRID(in, false), newVertexRID, edge.getIdentity());
} else {
throw new IllegalArgumentException("Invalid direction for moveEdge: " + direction);
}
}There was a problem hiding this comment.
Fixed in 6678316. vOut and vIn are now declared before their respective try blocks and reused for the connect calls, eliminating the duplicate lookupByRID invocations. The null guards remain on the connect calls rather than adopting the ternary form in the suggestion - connectOutgoingEdge NPEs on a null toVertex, so guarding on the cached variable (which is null if the lookup threw RecordNotFoundException) is cleaner.
Code Review - Edge Movement FeatureOverviewThis PR adds the ability to move an edge's source or target vertex via The feature idea is sound, but there are a few bugs and test gaps that should be addressed before merging. Bugs1.
The simplest fix is to keep the // moveEdge already knows it is mutating - mark dirty explicitly there
edge.in = newVertexRID;
edge.dirty = true;2. Null connectIncomingEdge(database.lookupByRID(newVertexRID, false), out, edge.getIdentity());
3. Unguarded connectOutgoingEdge(..., database.lookupByRID(in, false), edge);If 4. The Minor / Design Concerns5. If a caller writes 6. ClassCastException risk on non-vertex RIDs
7. Redundant vertex lookup (minor performance) In the 8. Comments describe WHAT, not WHY Per the project's CLAUDE.md convention, comments should only explain non-obvious why, not restate what the code does. Lines like Test CoverageThe single test covers only
Checklist & PR descriptionThe PR description still contains the template placeholder text and the checklist boxes are unchecked. Please fill these in before merging. SummaryThe core idea and code structure are solid. The main blocker is the |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4074 +/- ##
==========================================
+ Coverage 63.80% 64.41% +0.60%
==========================================
Files 1603 1603
Lines 121624 121668 +44
Branches 25940 25950 +10
==========================================
+ Hits 77605 78368 +763
+ Misses 33178 32441 -737
- Partials 10841 10859 +18 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Code Review:
|
Code Review - UPDATE EDGE @in/@out RewiringNote: a previous automated comment on this PR was posted with some inaccurate analysis; this supersedes it. OverviewGood fix for a real correctness gap - Bugs1. Partial mutation if the new-vertex lookup throws (medium severity) In both branches of // IN branch
edge.setIn(newVertexRID); // 1. field already changed
final Identifiable newInVertex = database.lookupByRID(newVertexRID, false); // 2. can throw RNFE
if (vOut != null) {
connectOutgoingEdge(vOut, newInVertex, edge); // 3. not reached on throw
connectIncomingEdge(newInVertex, out, edge.getIdentity());
}If Fix: do all lookups first, then perform mutations: // look up everything before touching any structure
final Identifiable newInVertex = database.lookupByRID(newVertexRID, false); // throws early if missing
final VertexInternal oldVIn = ...
final VertexInternal vOut = ...
// now mutate
inEdges.removeEdge(edge);
outEdges.removeEdge(edge);
edge.setIn(newVertexRID);
connectOutgoingEdge(...);
connectIncomingEdge(...);2. Unchecked final VertexInternal newOutVertex = (VertexInternal) database.lookupByRID(newVertexRID, false);If 3. Wasted lookup when the opposite endpoint is absent In both branches the PR Description vs. Diff DiscrepancyThe description says "Fixes Test Gaps
The traversal test for Minor IssuesMisleading error message in throw new IllegalArgumentException(
"Cannot set @in to a non-RID value (" + value.getClass().getSimpleName() + "): use a RID or Identifiable");Test assertions use // instead of:
assertThat(edge.getOut().toString()).isEqualTo(rids[0].toString());
// prefer:
assertThat(edge.getOut()).isEqualTo(rids[0]);SummaryThe core design is sound and the three root causes are correctly identified. The main ask before merging is:
|
Code Review - UPDATE EDGE @in/@out RewiringOverviewThis PR fixes a real bug: The root-cause analysis is correct and the basic test coverage is solid. A few concerns follow. Significant: Edge RID changes after move
A cleaner alternative is an in-place update:
This preserves the RID and matches the semantics of an UPDATE. If delete+create is intentional, please add a comment explaining why. Potential double-write after
|
| Concern | Severity |
|---|---|
| Edge RID changes after move - breaks external references | Significant |
Potential double-write due to dirty = true after moveEdge() |
Minor/bug |
| NPE if target vertex RID does not exist | Minor/bug |
Missing traversal test for @out, no-op, and multi-edge cases |
Test gap |
439c04b to
3aa211c
Compare
Code ReviewSummary: This PR fixes three real bugs with Critical: RID changes after
|
Code Review: UPDATE EDGE SET @in/@out rewiringGood fix for a real bug - the three-bug breakdown in the PR description is clear and accurate. The test coverage is solid for the happy path. Here are my observations: Critical: Edge RID changes after
|
| Severity | Issue |
|---|---|
| High | moveEdge changes edge RID - breaks external references |
| Medium | setIn/setOut dirty flag not actually fixed despite PR description |
| Medium | toRID does not handle String RID input |
| Medium | Missing null + type check before VertexInternal cast in moveEdge |
| Low | Silent fallback when database is not DatabaseInternal |
| Low | No CHECK DATABASE regression test |
| Low | Unrelated Bolt changes mixed into the PR |
The core idea is sound and the test structure is good - the main concern is the delete+recreate approach changing the edge RID, which is the riskiest part of the implementation.
- Remove dirty=true from setIn/setOut to avoid marking edges dirty during deserialization (JavaBinarySerializer) - Move dirty=true unconditionally after moveEdge/direct-assign in MutableEdge.set() - Guard connectOutgoingEdge/connectIncomingEdge with null checks in both IN and OUT branches - Cache newInVertex and newOutVertex lookups to avoid redundant database.lookupByRID calls - Replace else with else-if + throw for unsupported DIRECTION values - Add tests for @out direction, property preservation, and adjacency list consistency Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…veEdge - Setting @in or @out to null now throws IllegalArgumentException since edges require both endpoints; previously this silently set a null field leaving old vertex edge lists stale - Cache vOut/vIn from the removal try blocks in moveEdge to reuse them for the connect calls, removing duplicate lookupByRID invocations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace manual edge-list manipulation with deleteEdge() + standard edge creation. deleteEdge() already handles removal from both vertex lists; newEdge creation handles connecting the new endpoints. This also fixes a latent bug where connectIncomingEdge was called regardless of whether the edge type is bidirectional. Add updateIdentity() on MutableEdge (package-private) so SaveElementStep's subsequent save() writes to the new record RID rather than the deleted one. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code ReviewThis PR fixes a real and important bug - Critical: Edge RID changes after UPDATE EDGE
A safer implementation would:
This avoids changing the RID and avoids the double-write described below. Major: Double-save after moveEdge()
Major: Silent fallback when database is not DatabaseInternalif (rid != null && !newIn.equals(this.in) && database instanceof DatabaseInternal dbInt)
dbInt.getGraphEngine().moveEdge(this, Vertex.DIRECTION.IN, newIn);
else
this.in = newIn; // <-- silently skips vertex list updateIf Major:
|
What does this PR do?
Implements correct behaviour for
UPDATE EDGE SET @in = <rid>andUPDATE EDGE SET @out = <rid>, which allows an existing edge to be re-pointed to a different vertex.Three bugs were fixed:
1. Wrong SQL syntax in the test - The original test used
`#1:2`(backtick-quoted identifier), which the ANTLR grammar resolves as a property-name lookup on the current record (returning null). The correct RID literal syntax is#1:2without backticks.2.
MutableEdge.set("@in"/"@out", ...)silently discarded the value - The previousset(String, Object)override delegated toMutableDocument.set(), storing the value in the document property map. HoweverBinarySerializer.serializeEdge()readsedge.getIn()/edge.getOut()(the dedicatedin/outfields), never the map, so the update was lost on serialisation.setIn()/setOut()also failed to mark the record dirty.3. Vertex edge-linked-lists not updated - Even with the field corrected, the
EdgeLinkedListstructures inside the adjacent vertices remained stale (Alice's OUT list still pointed to Bob; Bob's IN list still referenced the edge). This causedCHECK DATABASEto report a corrupted record.Changes
MutableEdge- overridesset(String, Object)to intercept@in/@outand route throughGraphEngine.moveEdge(). FixessetIn()/setOut()to setdirty = true.GraphEngine- newmoveEdge(edge, direction, newVertexRID)that atomically:edge.inoredge.outUpdateEdgeTest- rewrites the test with correct RID literal SQL, uses capturedRIDvariables instead of hardcoded strings in both the SQL command and assertions, removes debug prints.Checklist
mvn clean packagecommand