#4095 fix(cypher): anonymous directed rel patterns must not reuse the same edge - #4115
Conversation
… same edge In the optimizer path, only named relationship variables were tracked in relVarsPerClause for same-clause isomorphism, so anonymous multi-hop patterns like (s)<-[:KNOWS]-(f)-[:KNOWS]->(d) could bind the same physical edge to both slots. ExpandAll gains an edgeTrackingVar field that, when set, stashes the edge in the row under a synthetic property name even with no user variable. CypherOptimizer.computeNeedsEdgeTracking flags anonymous hops whose edge types overlap with a sibling hop in the same MATCH clause, and buildExpansionChain assigns a synthetic name to each, sets it on the ExpandAll, and registers it in relVarsPerClause so subsequent hops apply the existing isomorphism check.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 17 |
🟢 Coverage 90.00% diff coverage · -7.76% coverage variation
Metric Results Coverage variation ✅ -7.76% coverage variation Diff coverage ✅ 90.00% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (c1814a7) 123556 90737 73.44% Head commit (fb92b57) 154945 (+31389) 101770 (+11033) 65.68% (-7.76%) 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 (#4115) 40 36 90.00% 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 addresses GitHub issue #4095 by enforcing Cypher relationship uniqueness for anonymous relationship variables within the same MATCH clause. It introduces a mechanism in CypherOptimizer to identify relationships requiring tracking and implements synthetic variable generation to store these edges in the ExpandAll operator. A comprehensive regression test suite is also added. Feedback was provided regarding the efficiency of the relationship type overlap detection logic in CypherOptimizer, suggesting a Map-based approach to replace the current nested loops for better performance and readability.
| outer: | ||
| for (int i = 0; i < clauseRels.size(); i++) { | ||
| for (int j = 0; j < clauseRels.size(); j++) { | ||
| if (i == j) | ||
| continue; | ||
| for (final String type : clauseRels.get(i).getTypes()) { | ||
| if (clauseRels.get(j).getTypes().contains(type)) { | ||
| needs.add(clauseRels.get(i)); | ||
| continue outer; | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The logic to find relationships with overlapping types can be implemented more efficiently and readably. The current implementation uses nested loops with a complexity that is roughly quadratic in the number of relationships per clause, which could be slow for complex queries with many relationships.
A more efficient and declarative approach would be to use a map to group relationships by their types. This avoids the nested loops and manual index management, improving both performance and readability.
final Map<String, List<LogicalRelationship>> relationshipsByType = new HashMap<>();
for (final LogicalRelationship rel : clauseRels) {
for (final String type : rel.getTypes()) {
relationshipsByType.computeIfAbsent(type, k -> new ArrayList<>()).add(rel);
}
}
for (final List<LogicalRelationship> rels : relationshipsByType.values()) {
if (rels.size() > 1) {
needs.addAll(rels);
}
}There was a problem hiding this comment.
Applied. Replaced the nested per-clause loop with a Map<String, List<LogicalRelationship>> type-bucket pass: each rel is added to the bucket of every type it declares, then any bucket with size > 1 contributes its rels to the tracking set. Linear in (rels x types) and clearly intent-revealing. The hasUntyped short-circuit stays — an empty getTypes() would otherwise never enter any bucket, but semantically an untyped hop overlaps with everything.
Code Review: #4095 fix - Anonymous directed rel patterns isomorphismOverall: Solid, well-scoped fix with good test coverage. A few edge cases worth discussing. What this PR doesFixes Cypher relationship isomorphism enforcement for anonymous (unnamed) relationship variables in The fix follows the same pattern as the existing named-variable fix (#4096): assign synthetic Correctness
One potential gap - In Minor code observationsSynthetic vars in
O(N^2) type intersection in Test coverageCoverage is good for the primary cases (diverging/converging with row count and aggregate assertions). Missing scenarios worth considering before merge:
SummaryThe fix is correct for the primary case, uses the established synthetic-variable convention consistently, and the tests clearly document the bug and expected behaviour. The |
Replace nested per-clause overlap loop with a Map<String, List<Rel>> type-bucket pass. Same semantics, linear in (rels x types) instead of quadratic in rels. Tests added: - 3-hop anonymous same-type chain (multiple synthetic counters) - mixed named + anonymous in same MATCH (both tracking paths coexist) - cross-clause anonymous edge reuse remains permitted (not blocked)
|
Addressed in fb92b57: Test additions (all 3 suggested scenarios):
Named rels in
|
Code Review: #4095 fix - Anonymous directed rel patterns isomorphismOverall: Well-implemented fix, correct approach, test coverage is stronger than the PR description suggests. A few follow-up items worth tracking. What this PR doesFixes Cypher relationship isomorphism enforcement for anonymous (unnamed) relationship variables in The fix follows the same approach as the named-variable fix (#4096): compute which hops need tracking at plan-build time ( CorrectnessThe core fix is correct. Specific points:
One gap:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4115 +/- ##
==========================================
+ Coverage 64.46% 64.48% +0.02%
==========================================
Files 1623 1623
Lines 123556 123596 +40
Branches 26303 26316 +13
==========================================
+ Hits 79652 79704 +52
+ Misses 32850 32826 -24
- Partials 11054 11066 +12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Fixed issues: - #4095 (consecutive directed relationship slots reuse the same edge): already fixed in current main by commit 7ce2cd0 #4095 fix(cypher): anonymous directed rel patterns must not reuse the same edge (#4115). Added 1 regression test as a guard. - #4094 (CALL db.labels() YIELD label nullifies carried variables): already fixed in current main. Added 1 regression test as a guard. - #4105 (node label-union pattern (n:A|B) matches no rows): fixed. The parser was extracting the labels into a list but losing the OR semantics, so the executor treated :A|B like :A:B. 2 regression tests pass. - #4093 (backslash dropped from string literals): fixed. The string-literal decoder treated unknown escape sequences as "remove the backslash". 4 regression tests pass.
… reuse the same edge (ArcadeData#4115)
Fixed issues: - ArcadeData#4095 (consecutive directed relationship slots reuse the same edge): already fixed in current main by commit 7ce2cd0 ArcadeData#4095 fix(cypher): anonymous directed rel patterns must not reuse the same edge (ArcadeData#4115). Added 1 regression test as a guard. - ArcadeData#4094 (CALL db.labels() YIELD label nullifies carried variables): already fixed in current main. Added 1 regression test as a guard. - ArcadeData#4105 (node label-union pattern (n:A|B) matches no rows): fixed. The parser was extracting the labels into a list but losing the OR semantics, so the executor treated :A|B like :A:B. 2 regression tests pass. - ArcadeData#4093 (backslash dropped from string literals): fixed. The string-literal decoder treated unknown escape sequences as "remove the backslash". 4 regression tests pass.
Fixed issues: - #4095 (consecutive directed relationship slots reuse the same edge): already fixed in current main by commit 7ce2cd0 #4095 fix(cypher): anonymous directed rel patterns must not reuse the same edge (#4115). Added 1 regression test as a guard. - #4094 (CALL db.labels() YIELD label nullifies carried variables): already fixed in current main. Added 1 regression test as a guard. - #4105 (node label-union pattern (n:A|B) matches no rows): fixed. The parser was extracting the labels into a list but losing the OR semantics, so the executor treated :A|B like :A:B. 2 regression tests pass. - #4093 (backslash dropped from string literals): fixed. The string-literal decoder treated unknown escape sequences as "remove the backslash". 4 regression tests pass. (cherry picked from commit 53e34e5)
… 2.67.0 to 2.68.0 [skip ci] Bumps [com.google.api.grpc:proto-google-common-protos](https://github.com/googleapis/sdk-platform-java) from 2.67.0 to 2.68.0. Release notes *Sourced from [com.google.api.grpc:proto-google-common-protos's releases](https://github.com/googleapis/sdk-platform-java/releases).* > v2.68.0 > ------- > > [2.68.0](googleapis/sdk-platform-java@v2.67.0...v2.68.0) (2026-03-17) > ------------------------------------------------------------------------------------------------ > > ### Features > > * Add client request duration metric. ([ArcadeData#4132](https://redirect.github.com/googleapis/sdk-platform-java/issues/4132)) ([6a76397](googleapis/sdk-platform-java@6a76397)) > * Add more attributes to golden signals metrics. ([ArcadeData#4135](https://redirect.github.com/googleapis/sdk-platform-java/issues/4135)) ([59d0624](googleapis/sdk-platform-java@59d0624)) > * **gax-httpjson:** add HttpJsonErrorParser utility ([ArcadeData#4137](https://redirect.github.com/googleapis/sdk-platform-java/issues/4137)) ([a1b7565](googleapis/sdk-platform-java@a1b7565)) > * **generator:** add extra allowed modules that will not be removed from the monorepo if they are present ([ArcadeData#4124](https://redirect.github.com/googleapis/sdk-platform-java/issues/4124)) ([774fe6e](googleapis/sdk-platform-java@774fe6e)) > * **o11y:** introduce `gcp.client.repo` and `gcp.client.artifact` attributes ([ArcadeData#4120](https://redirect.github.com/googleapis/sdk-platform-java/issues/4120)) ([105f644](googleapis/sdk-platform-java@105f644)) > * **o11y:** Introduce `rpc.system.name` and `rpc.method` in gRPC ([ArcadeData#4121](https://redirect.github.com/googleapis/sdk-platform-java/issues/4121)) ([7ab6d2e](googleapis/sdk-platform-java@7ab6d2e)) > * **o11y:** introduce server.port attribute ([ArcadeData#4128](https://redirect.github.com/googleapis/sdk-platform-java/issues/4128)) ([56aa343](googleapis/sdk-platform-java@56aa343)) > > ### Bug Fixes > > * add null checks for ApiTracerFactory in ClientContext ([ArcadeData#4122](https://redirect.github.com/googleapis/sdk-platform-java/issues/4122)) ([4b3dbe2](googleapis/sdk-platform-java@4b3dbe2)) > * Decrease log level for directpath warnings outside GCE ([ArcadeData#4139](https://redirect.github.com/googleapis/sdk-platform-java/issues/4139)) ([c9651e7](googleapis/sdk-platform-java@c9651e7)) > * **gax-grpc:** add pick\_first fallback to direct path service config ([ArcadeData#4143](https://redirect.github.com/googleapis/sdk-platform-java/issues/4143)) ([b150fe9](googleapis/sdk-platform-java@b150fe9)) > * Populate method level attributes in metrics recording ([ArcadeData#4149](https://redirect.github.com/googleapis/sdk-platform-java/issues/4149)) ([7b7e6c9](googleapis/sdk-platform-java@7b7e6c9)) > * suppress warnings in generated projects for non-idiomatic durations ([ArcadeData#4119](https://redirect.github.com/googleapis/sdk-platform-java/issues/4119)) ([4206e6e](googleapis/sdk-platform-java@4206e6e)) > * Use ServiceName + MethodName as the regex for Otel ([ArcadeData#2543](https://redirect.github.com/googleapis/sdk-platform-java/issues/2543)) ([b9ae73f](googleapis/sdk-platform-java@b9ae73f)) > > ### Documentation > > * **hermetic\_build:** fix config field name in readme ([ArcadeData#4130](https://redirect.github.com/googleapis/sdk-platform-java/issues/4130)) ([a0c8f67](googleapis/sdk-platform-java@a0c8f67)) Changelog *Sourced from [com.google.api.grpc:proto-google-common-protos's changelog](https://github.com/googleapis/sdk-platform-java/blob/main/CHANGELOG.md).* > [2.68.0](googleapis/sdk-platform-java@v2.67.0...v2.68.0) (2026-03-17) > ------------------------------------------------------------------------------------------------ > > ### Features > > * Add client request duration metric. ([ArcadeData#4132](https://redirect.github.com/googleapis/sdk-platform-java/issues/4132)) ([6a76397](googleapis/sdk-platform-java@6a76397)) > * Add more attributes to golden signals metrics. ([ArcadeData#4135](https://redirect.github.com/googleapis/sdk-platform-java/issues/4135)) ([59d0624](googleapis/sdk-platform-java@59d0624)) > * **gax-httpjson:** add HttpJsonErrorParser utility ([ArcadeData#4137](https://redirect.github.com/googleapis/sdk-platform-java/issues/4137)) ([a1b7565](googleapis/sdk-platform-java@a1b7565)) > * **generator:** add extra allowed modules that will not be removed from the monorepo if they are present ([ArcadeData#4124](https://redirect.github.com/googleapis/sdk-platform-java/issues/4124)) ([774fe6e](googleapis/sdk-platform-java@774fe6e)) > * **o11y:** introduce `gcp.client.repo` and `gcp.client.artifact` attributes ([ArcadeData#4120](https://redirect.github.com/googleapis/sdk-platform-java/issues/4120)) ([105f644](googleapis/sdk-platform-java@105f644)) > * **o11y:** Introduce `rpc.system.name` and `rpc.method` in gRPC ([ArcadeData#4121](https://redirect.github.com/googleapis/sdk-platform-java/issues/4121)) ([7ab6d2e](googleapis/sdk-platform-java@7ab6d2e)) > * **o11y:** introduce server.port attribute ([ArcadeData#4128](https://redirect.github.com/googleapis/sdk-platform-java/issues/4128)) ([56aa343](googleapis/sdk-platform-java@56aa343)) > > ### Bug Fixes > > * add null checks for ApiTracerFactory in ClientContext ([ArcadeData#4122](https://redirect.github.com/googleapis/sdk-platform-java/issues/4122)) ([4b3dbe2](googleapis/sdk-platform-java@4b3dbe2)) > * Decrease log level for directpath warnings outside GCE ([ArcadeData#4139](https://redirect.github.com/googleapis/sdk-platform-java/issues/4139)) ([c9651e7](googleapis/sdk-platform-java@c9651e7)) > * **gax-grpc:** add pick\_first fallback to direct path service config ([ArcadeData#4143](https://redirect.github.com/googleapis/sdk-platform-java/issues/4143)) ([b150fe9](googleapis/sdk-platform-java@b150fe9)) > * Populate method level attributes in metrics recording ([ArcadeData#4149](https://redirect.github.com/googleapis/sdk-platform-java/issues/4149)) ([7b7e6c9](googleapis/sdk-platform-java@7b7e6c9)) > * suppress warnings in generated projects for non-idiomatic durations ([ArcadeData#4119](https://redirect.github.com/googleapis/sdk-platform-java/issues/4119)) ([4206e6e](googleapis/sdk-platform-java@4206e6e)) > * Use ServiceName + MethodName as the regex for Otel ([ArcadeData#2543](https://redirect.github.com/googleapis/sdk-platform-java/issues/2543)) ([b9ae73f](googleapis/sdk-platform-java@b9ae73f)) > > ### Documentation > > * **hermetic\_build:** fix config field name in readme ([ArcadeData#4130](https://redirect.github.com/googleapis/sdk-platform-java/issues/4130)) ([a0c8f67](googleapis/sdk-platform-java@a0c8f67)) Commits * [`7eca195`](googleapis/sdk-platform-java@7eca195) chore(main): release 2.68.0 ([ArcadeData#4123](https://redirect.github.com/googleapis/sdk-platform-java/issues/4123)) * [`7b7e6c9`](googleapis/sdk-platform-java@7b7e6c9) fix: Populate method level attributes in metrics recording ([ArcadeData#4149](https://redirect.github.com/googleapis/sdk-platform-java/issues/4149)) * [`c9651e7`](googleapis/sdk-platform-java@c9651e7) fix: Decrease log level for directpath warnings outside GCE ([ArcadeData#4139](https://redirect.github.com/googleapis/sdk-platform-java/issues/4139)) * [`55df486`](googleapis/sdk-platform-java@55df486) build(deps): bump black from 24.8.0 to 26.3.1 in /hermetic\_build/common ([ArcadeData#4138](https://redirect.github.com/googleapis/sdk-platform-java/issues/4138)) * [`15c8abf`](googleapis/sdk-platform-java@15c8abf) chore(deps): update upper bound dependencies file ([ArcadeData#4146](https://redirect.github.com/googleapis/sdk-platform-java/issues/4146)) * [`8be3f9d`](googleapis/sdk-platform-java@8be3f9d) chore: update googleapis commit at Thu Feb 19 03:02:27 UTC 2026 ([ArcadeData#4115](https://redirect.github.com/googleapis/sdk-platform-java/issues/4115)) * [`b150fe9`](googleapis/sdk-platform-java@b150fe9) fix(gax-grpc): add pick\_first fallback to direct path service config ([ArcadeData#4143](https://redirect.github.com/googleapis/sdk-platform-java/issues/4143)) * [`4a24d92`](googleapis/sdk-platform-java@4a24d92) build: disable SonarCloud for fork pull requests ([ArcadeData#4142](https://redirect.github.com/googleapis/sdk-platform-java/issues/4142)) * [`59d0624`](googleapis/sdk-platform-java@59d0624) feat: Add more attributes to golden signals metrics. ([ArcadeData#4135](https://redirect.github.com/googleapis/sdk-platform-java/issues/4135)) * [`0275698`](googleapis/sdk-platform-java@0275698) chore: extract channel builder for internal use ([ArcadeData#4136](https://redirect.github.com/googleapis/sdk-platform-java/issues/4136)) * Additional commits viewable in [compare view](googleapis/sdk-platform-java@v2.67.0...v2.68.0) [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- Dependabot commands and options You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
… reuse the same edge (ArcadeData#4115)
Fixed issues: - ArcadeData#4095 (consecutive directed relationship slots reuse the same edge): already fixed in current main by commit 8fbecb7 ArcadeData#4095 fix(cypher): anonymous directed rel patterns must not reuse the same edge (ArcadeData#4115). Added 1 regression test as a guard. - ArcadeData#4094 (CALL db.labels() YIELD label nullifies carried variables): already fixed in current main. Added 1 regression test as a guard. - ArcadeData#4105 (node label-union pattern (n:A|B) matches no rows): fixed. The parser was extracting the labels into a list but losing the OR semantics, so the executor treated :A|B like :A:B. 2 regression tests pass. - ArcadeData#4093 (backslash dropped from string literals): fixed. The string-literal decoder treated unknown escape sequences as "remove the backslash". 4 regression tests pass.
Summary
(Alice, Bob, Alice)for(s)<-[:KNOWS]-(f)-[:KNOWS]->(d).relVarsPerClause. Anonymous rels (rel.getVariable() == null) were never added, sosameClausePrecedingRelVarsstayed empty and the per-row isomorphism check inExpandAll.collectUsedEdgeRidshad nothing to compare against. Same root pattern as the legacyMatchRelationshipStephandles viacomputeHopEdgeTrackingNeeds+ synthetic" relN"names.ExpandAllnow has anedgeTrackingVarfield that stashes the edge in the row under a synthetic property name even whenedgeVariableis null.CypherOptimizer.computeNeedsEdgeTrackingflags anonymous hops whose edge types overlap with a sibling hop in the same MATCH clause, andbuildExpansionChainassigns each one a synthetic" anon_e_N"name, sets it on the operator beforeaddTargetLabelFilterwraps it, and registers it inrelVarsPerClausefor subsequent hops.Test plan
Issue4095DirectedRelReuseIsomorphismTest(4 tests): diverging + converging row count + aggregate countIssue4096UndirectedRelReuseIsomorphismTeststill passes (named-variable path unchanged)Issue4006BoundRelVarPathIsomorphismTeststill passes