fix: [cypher] batch fixes for #4184, #4185, #4186, #4188, #4189#4196
Conversation
- #4186 reduce() over a null list now returns null instead of the initial accumulator - #4184 map projection with a function-call computed field (e.g. {upperName: toUpper(p.name)}) no longer collapses to just the function value - #4189 RETURN DISTINCT * keeps distinct rows by hashing on the projected row properties instead of the unexpanded * return item - #4188 endpoint property maps in relationship patterns resolve Expression-typed entries against the current row (mirrors MatchNodeStep) - #4185 FOREACH DELETE: DeleteClause now stores parsed Expression list so function-call targets such as endNode(r) actually resolve; ForeachStep batches deletes per iteration via a CommandContext variable and flushes them edges-first to align with Cypher's deferred-delete semantics Adds regression tests in CypherReduceAndShortestPathTest, OpenCypherExpressionTest, and new Issue4185/Issue4188/Issue4189 test classes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| ErrorProne | 1 high |
| CodeStyle | 1 minor |
🟢 Metrics 0 complexity
Metric Results Complexity 0
🟢 Coverage 84.43% diff coverage · -7.47% coverage variation
Metric Results Coverage variation ✅ -7.47% coverage variation Diff coverage ✅ 84.43% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (7e9f984) 126684 93276 73.63% Head commit (1abf47c) 158456 (+31772) 104835 (+11559) 66.16% (-7.47%) 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 (#4196) 122 103 84.43% 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.
Code Review - Cypher Batch Fix PR #4196Overall this is a well-scoped batch of targeted correctness fixes with good test coverage. Each fix is minimal and addresses a real spec deviation. Below are issues worth addressing before merge. Blocker: Planning document committed to repo
Code style violation - fully qualified name
java.util.Collections.sort(propNames);The project's CLAUDE.md convention is to never use FQNs - always import the class. Either:
Fragile reflection in test code
private static String extractName(final Object node) {
try {
final var m = node.getClass().getMethod("getString", String.class);
return (String) m.invoke(node, "name");
} catch (final NoSuchMethodException ignored) {
try {
final var m = node.getClass().getMethod("getProperty", String.class);
return (String) m.invoke(node, "name");
} catch ...This try/catch reflection cascade is fragile and bypasses the type system. The Design concern - parallel lists in
|
There was a problem hiding this comment.
Code Review
This pull request addresses five Cypher-related issues (#4184, #4185, #4186, #4188, #4189) by refining expression evaluation, deduplication logic, and deletion behavior. Key improvements include fixing reduce() semantics for null lists, ensuring map projections correctly handle function calls, and implementing deferred deletion within FOREACH loops to maintain correct graph state visibility. Review feedback suggests enhancing type safety in DeleteStep by replacing the List<Object[]> used for deferred deletes with a dedicated static inner class.
| return; | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| final List<Object[]> deferredBatch = (List<Object[]>) context.getVariable(DEFERRED_DELETE_BATCH_VAR); |
There was a problem hiding this comment.
For better type safety and readability, consider creating a small static inner class to hold the deferred delete information instead of using a List<Object[]>. This avoids raw array indexing and casting, making the code more robust and easier to understand.
You could define a class like this within DeleteStep:
private static class DeferredDelete {
final Object target;
final boolean detach;
DeferredDelete(Object target, boolean detach) {
this.target = target;
this.detach = detach;
}
}Then, the deferredBatch would be of type List<DeferredDelete>, and you would use it like this:
In applyDeleteOperations:
// ...
final List<DeferredDelete> deferredBatch = (List<DeferredDelete>) context.getVariable(DEFERRED_DELETE_BATCH_VAR);
// ...
deferredBatch.add(new DeferredDelete(edge, deleteClause.isDetach()));
// ...
deferredBatch.add(new DeferredDelete(other, deleteClause.isDetach()));In flushDeferredDeletes:
public static void flushDeferredDeletes(final CommandContext context, final List<DeferredDelete> batch) {
// ...
final List<DeferredDelete> others = new ArrayList<>();
// ...
final Object target = entry.target;
final boolean detach = entry.detach;
// ...
}This change would also need to be applied in ForeachStep.java.
There was a problem hiding this comment.
Done in d7d0a1e - replaced List<Object[]> with a new public record DeleteStep.DeferredDeleteTarget(Object target, boolean detach). The deferred-batch consumer is also updated to use it, dropping the positional indexing and the redundant Boolean.valueOf.
…n scope, test polish
- Drop the planning doc that duplicated the PR description.
- ProjectReturnStep: replace FQN java.util.Collections.sort with List.sort(null).
- ForeachStep: hoist begin/commit outside the iteration loop so a FOREACH over N
elements no longer issues N begin/commit pairs (restores original transaction scope).
- DeleteStep: replace List<Object[]> deferred batch with a typed DeferredDeleteTarget
record (clearer than positional arrays); switch the connected-edges probe to a
try-with-resources wrapper that closes AutoCloseable iterators; drop the
CommandExecutionException FQN and unneeded Boolean.valueOf boxing.
- Issue4189ReturnDistinctStarTest: replace fragile reflection helper with a direct
Vertex cast plus Vertex.getString("name").
- Issue4185ForeachDeleteVisibilityTest: add coverage for DETACH DELETE inside FOREACH,
nested FOREACH with DELETE, and mixed SET+DELETE within a single iteration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Response to review (d7d0a1e)Addressed:
Pushing back:
Test results: 5919 OpenCypher engine tests pass, including the three new FOREACH+DELETE cases. |
Code Review - PR #4196: Batch Cypher fixes for #4184, #4185, #4186, #4188, #4189Good set of correctness fixes overall. Each change is logically sound and targets a real spec violation. The test coverage is solid. Below are my observations, ordered roughly by severity. Correctness / Logic
final Set<String> propNames = new TreeSet<>(projectedResult.getPropertyNames());
for (final String name : propNames) { ... }
Test coverageTests are good. A few additions would strengthen the suite:
Style / CLAUDE.md conventionsPer Several new methods have multi-paragraph Javadocs ( SummaryThe five fixes are all correct and well-targeted. The main things I'd address before merging:
Good work on the test coverage - regression tests for all five issues plus control cases is exactly the right approach. |
…tch, tighter tests - DeleteStep: rename hasOnlyDetachedEdges to hasNoEdges and replace iterator-with-wrapper with a single Vertex.countEdges(BOTH) call (drops CloseableIteratorRef helper). Narrow the catch to RecordNotFoundException. - DeleteStep / MatchRelationshipStep: trim multi-paragraph Javadocs to one-liners per CLAUDE.md style. - ForeachStep: widen the FOREACH catch from RuntimeException to Exception so a checked exception from an inner clause still triggers rollback; wraps in CommandExecutionException when needed. - ProjectReturnStep: use TreeSet over getPropertyNames() directly for DISTINCT * keying; avoids the intermediate ArrayList copy. - Tests: tighten Issue4189ReturnDistinctStarTest#returnDistinctStarAfterTrivialWith to assert the actual names; add foreachDeleteInsideOuterTransaction (FOREACH inside an already-active outer transaction) and endpointPropertyBoundFromParameter ($param binding for #4188). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Response to review round 2 (1abf47c)Addressed:
Pushing back:
5919 OpenCypher engine tests still pass, including the three new tests. |
Code Review - PR #4196 (Batch Cypher Fixes)Good batch of targeted fixes from differential testing. Overall the approach is sound, but I have a few observations across the individual changes. #4186 -
|
| Fix | Assessment |
|---|---|
| #4186 reduce/null | ✅ Clean, correct |
| #4184 map projection | ✅ Clean, correct |
| #4189 DISTINCT * | ✅ Correct; minor GC note |
| #4188 endpoint variable property | ✅ Correct |
| #4185 FOREACH+DELETE | ✅ Correct; isFunctionLikeTarget heuristic can be simplified, ForeachStep transaction scope change worth a second look |
No blockers found. The isFunctionLikeTarget simplification and the ForeachStep transaction interaction are the two items worth addressing before merge or tracking as follow-up.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4196 +/- ##
==========================================
+ Coverage 64.56% 64.58% +0.01%
==========================================
Files 1645 1645
Lines 126684 126778 +94
Branches 27098 27123 +25
==========================================
+ Hits 81799 81882 +83
+ Misses 33438 33435 -3
- Partials 11447 11461 +14 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
Bundles five Cypher correctness fixes filed together on 2026-05-10 from Neo4j differential testing.
reduce()over a null list returned the initial accumulator; now returns null per Cypher spec (ReduceExpression).p {.name, upperName: toUpper(p.name)}) collapsed to just the function value. The parser's recursive function-invocation search now skipsMapProjectionContextjust like it already skippedMapContext.RETURN DISTINCT *collapsed all rows to one because the deduplication key was built from the unexpanded*return item.ProjectReturnStepnow keys off the sorted set of projected properties whenisReturnAll()is true.(:Person {name: fName})) matched zero rows becauseMatchRelationshipStep.matchesTargetPropertiescould not evaluateExpression-typed entries. It now accepts the current row and evaluates expressions, mirroringMatchNodeStep.matchesProperties.FOREACH (r IN rels | DELETE endNode(r) DELETE r)were never deleted becauseDeleteClauseonly kept the textual target form and the chained-access resolver did not understand function calls.DeleteClausenow also stores the parsedExpressionlist;DeleteStepevaluates it for function-like targets. To match Neo4j's deferred-delete semantics within a FOREACH iteration,ForeachStepenables per-iteration delete batching via a newCommandContextvariable (DeleteStep.DEFERRED_DELETE_BATCH_VAR) and flushes the batch edges-first at iteration end.Test plan
CypherReduceAndShortestPathTest#reduceWithNullList,reduceWithNullListFromOptionalMatchOpenCypherExpressionTest#mapProjectionWithFunctionCallField,mapProjectionWithSizeFunctionFieldIssue4189ReturnDistinctStarTest(4 cases)Issue4188EndpointVariablePropertyTest(4 cases)Issue4185ForeachDeleteVisibilityTest(2 cases)Closes #4184
Closes #4185
Closes #4186
Closes #4188
Closes #4189
🤖 Generated with Claude Code