Skip to content

Fix Cypher RETURN clause to unwrap single element results#3268

Merged
lvca merged 3 commits intoArcadeData:mainfrom
ExtReMLapin:claude/fix-cypher-result-format-giDzl
Jan 28, 2026
Merged

Fix Cypher RETURN clause to unwrap single element results#3268
lvca merged 3 commits intoArcadeData:mainfrom
ExtReMLapin:claude/fix-cypher-result-format-giDzl

Conversation

@ExtReMLapin
Copy link
Contributor

What does this PR do?

This PR fixes the OpenCypher query engine to properly unwrap single element results when returning a single node or edge variable. When a Cypher query like MATCH (n) RETURN n is executed, the result should be the element directly (a Vertex or Edge), not wrapped in a projection map like {"n": element}. This aligns with standard Cypher behavior and matches the behavior of the Gremlin-based Cypher engine.

Changes Made:

  1. FinalProjectionStep.java: Added logic to detect when a single property is being returned that resolves to a Document (vertex/edge) and return it as an element result directly instead of wrapping it in a projection.

  2. CypherResultFormatTest.java (new): Added comprehensive test coverage for the new behavior, including:

    • Single node returns should be elements
    • Single edge returns should be elements
    • Property access returns should still be projections
    • Multiple column returns should still be projections
    • Aliased single elements should still unwrap
  3. Test Updates: Updated 15+ existing test files to use result.toElement() instead of result.getProperty("varName") when accessing single returned elements, reflecting the new behavior.

Motivation

The previous behavior of wrapping single element returns in projection maps was inconsistent with standard Cypher semantics and the Gremlin-based implementation. This caused confusion and required workarounds in test code. By unwrapping single element results, we provide a more intuitive and standards-compliant API.

Related issues

This fix addresses the inconsistency between the OpenCypher and Gremlin-based Cypher engines in result formatting.

Additional Notes

  • The change is backward compatible in the sense that result.toElement() provides the same functionality as the previous result.getProperty("varName") approach
  • Multi-column returns and property projections continue to work as before
  • All existing tests have been updated to reflect the new behavior
  • The implementation correctly handles edge cases like aliased returns and aggregate functions

Checklist

  • I have run the build using mvn clean package command
  • My unit tests cover both failure and success scenarios

https://claude.ai/code/session_01LmNejBtujWGirVHcc2ackZ

When a Cypher query uses a single-variable RETURN clause (e.g.,
MATCH (n) RETURN n), the new OpenCypher engine was wrapping results
as {"n": element} instead of returning the element directly. This
differed from the Gremlin-based Cypher engine behavior where
MATCH (n) RETURN n returns [a, b, c] not [{"n":a}, {"n":b}, {"n":c}].

The fix is in FinalProjectionStep.filterResult(): when the RETURN
clause has exactly one item and its value is a Document (vertex/edge),
return it as an element-based ResultInternal instead of wrapping it
in a property map. Multi-variable RETURNs (e.g., RETURN n, m) are
unaffected and continue to use property maps.

https://claude.ai/code/session_01LmNejBtujWGirVHcc2ackZ
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ExtReMLapin, 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 refines the OpenCypher query engine's result formatting to ensure consistency with standard Cypher semantics and the Gremlin-based implementation. Specifically, it modifies the FinalProjectionStep to directly return a Document (representing a Vertex or Edge) when a Cypher query returns a single element, rather than wrapping it within a projection map. This change simplifies how single element results are consumed and enhances the overall intuitiveness of the API.

Highlights

  • Cypher RETURN Clause Behavior: The core logic in FinalProjectionStep.java has been updated to automatically unwrap single Document (Vertex or Edge) results when returned by a Cypher query, aligning with standard Cypher behavior.
  • New Comprehensive Test Suite: A new test file, CypherResultFormatTest.java, has been added to provide comprehensive coverage for the new result formatting, including scenarios for single node/edge returns, property access, multiple columns, aliased elements, and aggregate functions.
  • Extensive Test Adaptations: Over 15 existing test files across the opencypher module have been updated to use the new result.toElement() method instead of result.getProperty("varName") when expecting a single unwrapped element, ensuring compatibility and correctness with the new behavior.

🧠 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.

Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@ExtReMLapin
Copy link
Contributor Author

Claude wrote all of this, I just described the issue and the diagnosis.

@mergify
Copy link
Contributor

mergify bot commented Jan 28, 2026

🧪 CI Insights

Here's what we observed from your CI run for 1290aee.

🟢 All jobs passed!

But CI Insights is watching 👀

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly aligns the OpenCypher engine's behavior with standard Cypher semantics by unwrapping single-element results. The core logic change in FinalProjectionStep.java is concise and effective. The addition of CypherResultFormatTest.java provides excellent, focused test coverage for this new behavior, and the updates to existing tests ensure the entire test suite is consistent with the change.

I've added a few suggestions to improve code clarity and ensure compatibility with older Java versions. Overall, this is a solid improvement that enhances the engine's consistency and usability.

Comment on lines +90 to +91
assertThat(results.getFirst().isElement())
.as("Edge result should be an element, not a projection").isTrue();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The List.getFirst() method was introduced in Java 21. If this project targets an earlier Java version, this will cause a compilation error. For better backward compatibility and consistency with other tests, consider using results.get(0) instead.

Suggested change
assertThat(results.getFirst().isElement())
.as("Edge result should be an element, not a projection").isTrue();
assertThat(results.get(0).isElement())
.as("Edge result should be an element, not a projection").isTrue();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we target >= Java 21

Comment on lines +127 to +133
final String singleProp = requestedProperties.iterator().next();
if (inputResult.hasProperty(singleProp)) {
final Object value = inputResult.getProperty(singleProp);
if (value instanceof Document doc) {
return new ResultInternal(doc);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The inputResult.hasProperty(singleProp) check is redundant. The getProperty(singleProp) method will return null if the property doesn't exist, and null instanceof Document evaluates to false. You can simplify the code by removing the hasProperty check.

      final String singleProp = requestedProperties.iterator().next();
      final Object value = inputResult.getProperty(singleProp);
      if (value instanceof Document doc) {
        return new ResultInternal(doc);
      }

Tests for startNode() and endNode() Cypher functions also use
single-variable RETURN clauses that return Vertex results. Update
them to use result.toElement() instead of result.getProperty().

https://claude.ai/code/session_01LmNejBtujWGirVHcc2ackZ
@lvca lvca merged commit 1e65c9a into ArcadeData:main Jan 28, 2026
6 of 15 checks passed
@lvca
Copy link
Member

lvca commented Jan 28, 2026

Amazing, thanks!

@lvca lvca added this to the 26.2.1 milestone Jan 28, 2026
@lvca lvca self-requested a review January 28, 2026 18:14
robfrank pushed a commit that referenced this pull request Feb 17, 2026
* fix: single-variable Cypher RETURN now returns elements directly

When a Cypher query uses a single-variable RETURN clause (e.g.,
MATCH (n) RETURN n), the new OpenCypher engine was wrapping results
as {"n": element} instead of returning the element directly. This
differed from the Gremlin-based Cypher engine behavior where
MATCH (n) RETURN n returns [a, b, c] not [{"n":a}, {"n":b}, {"n":c}].

The fix is in FinalProjectionStep.filterResult(): when the RETURN
clause has exactly one item and its value is a Document (vertex/edge),
return it as an element-based ResultInternal instead of wrapping it
in a property map. Multi-variable RETURNs (e.g., RETURN n, m) are
unaffected and continue to use property maps.

https://claude.ai/code/session_01LmNejBtujWGirVHcc2ackZ

* Fixed broken tests

* fix: update startNode/endNode function tests for single-var RETURN

Tests for startNode() and endNode() Cypher functions also use
single-variable RETURN clauses that return Vertex results. Update
them to use result.toElement() instead of result.getProperty().

https://claude.ai/code/session_01LmNejBtujWGirVHcc2ackZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: CNE Pierre FICHEPOIL <pierre-1.fichepoil@gendarmerie.interieur.gouv.fr>
(cherry picked from commit 1e65c9a)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants