Skip to content

feat: implement MERGE enhancements - after MATCH, relationship patterns, and SET support#183

Merged
DecisionNerd merged 2 commits into
mainfrom
feature/163-merge-enhancements
Feb 16, 2026
Merged

feat: implement MERGE enhancements - after MATCH, relationship patterns, and SET support#183
DecisionNerd merged 2 commits into
mainfrom
feature/163-merge-enhancements

Conversation

@DecisionNerd
Copy link
Copy Markdown
Owner

@DecisionNerd DecisionNerd commented Feb 16, 2026

Closes #163, #164, #165

Summary

  • Implements MERGE after MATCH patterns (e.g., MATCH (a) MERGE (a)-[r]->(b))
  • Implements relationship MERGE patterns (e.g., MERGE (a)-[r:KNOWS]->(b))
  • Implements SET after MERGE without ON CREATE/ON MATCH (e.g., MERGE (n) SET n.prop = value)

Changes

Grammar (cypher.lark)

  • Added merge_clause set_clause? return_clause? to allow SET after MERGE
  • Added match_clause where_clause? merge_clause+ set_clause? return_clause? ... to allow MERGE after MATCH

Executor (executor.py)

  • Extended _execute_merge() to handle relationship patterns (3+ part patterns)
  • Added _merge_relationship_pattern() to match/create node-rel-node patterns
  • Added _merge_node_in_context() helper to resolve or create nodes with variable binding support
  • Properly handles:
    • Existing nodes from MATCH clause (variable binding)
    • Finding existing relationships by type and properties
    • Creating missing nodes and/or relationships
    • NULL property matching semantics

Tests

  • Unskipped 4 test cases in test_complex_merge_patterns.py:
    • test_merge_relationship_both_nodes_exist
    • test_merge_relationship_creates_nodes
    • test_merge_with_set_clause
    • test_merge_after_match
    • test_merge_with_where_clause

Test Results

All 76 MERGE-related integration tests pass:

  • test_complex_merge_patterns.py: 10/10 pass
  • All other MERGE tests: 66/66 pass

Coverage: 90.38% total

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • MERGE now supports node–relationship–node patterns, including directed/undirected relationships and property-aware matching.
    • SET clauses can be applied immediately after MERGE.
  • Bug Fixes / Improvements

    • Improved MERGE validation and clearer error handling for unsupported patterns.
  • Tests

    • Re-enabled and expanded integration tests covering complex MERGE patterns and property/direction behaviors.

…ns, and SET support (#163, #164, #165)

- Grammar: Allow MERGE after MATCH (issue #163)
- Grammar: Allow SET after MERGE without ON CREATE/ON MATCH (issue #165)
- Executor: Implement relationship MERGE patterns (issue #164)
- Executor: Add _merge_relationship_pattern() for node-rel-node patterns
- Executor: Add _merge_node_in_context() helper for node resolution
- Tests: Unskip 4 previously failing test cases

All tests passing, coverage at 90.38% total.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 16, 2026

Walkthrough

Adds executor support for MERGE on node-relationship-node patterns and node-creation helpers, extends the grammar to allow MERGE (and optional SET) after MATCH, and enables previously skipped integration tests for complex MERGE patterns.

Changes

Cohort / File(s) Summary
MERGE Relationship Pattern Execution
src/graphforge/executor/executor.py
Adds _merge_relationship_pattern and _merge_node_in_context, integrates node-rel-node MERGE handling into MERGE flow, adds validation/error handling, and reuses existing creation/binding helpers.
Grammar Extensions
src/graphforge/parser/cypher.lark
Updates write_query to allow merge_clause set_clause? return_clause? and match_clause ... merge_clause+ set_clause? ..., enabling MERGE after MATCH and optional SET after MERGE.
Test Integration
tests/integration/test_complex_merge_patterns.py
Removes skip decorators and re-enables integration tests covering incoming, undirected, and property-based MERGE patterns and idempotence.

Sequence Diagram(s)

sequenceDiagram
    participant Parser
    participant Executor
    participant NodeOps as Node Operations
    participant RelOps as Relationship Operations
    participant Context

    Parser->>Executor: Execute MERGE pattern (a)-[r]->(b)
    Executor->>NodeOps: Resolve or create source node (a)
    NodeOps->>Context: Bind source node
    Executor->>NodeOps: Resolve or create destination node (b)
    NodeOps->>Context: Bind destination node
    Executor->>RelOps: Search for relationship by type/properties between nodes
    alt Relationship found
        RelOps->>Context: Bind existing relationship (was_created=false)
    else Relationship not found
        RelOps->>Executor: Create relationship
        RelOps->>Context: Bind new relationship (was_created=true)
    end
    Executor->>Context: Apply ON CREATE / ON MATCH SET based on was_created
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #163: feat: support MERGE after MATCH patterns — Grammar changes directly address the objective to allow MERGE after MATCH.
  • feat: support relationship MERGE patterns #164 — Implements relationship-pattern MERGE handling in executor; closely related to the executor changes here.

Possibly related PRs

Suggested labels

enhancement, parser

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: MERGE enhancements with three key features (after MATCH, relationship patterns, SET support).
Description check ✅ Passed The description covers all key sections: summary of changes, grammar updates, executor changes, tests, and test results with coverage metrics. All critical information is present and well-organized.
Linked Issues check ✅ Passed The PR successfully addresses issue #163 by implementing MERGE after MATCH patterns through grammar updates and executor enhancements, with integration tests unskipped and passing.
Out of Scope Changes check ✅ Passed All changes are directly related to the three stated objectives: grammar updates for MERGE sequencing, executor methods for relationship patterns and node context handling, and integration test enablement.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/163-merge-enhancements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/graphforge/executor/executor.py`:
- Around line 2264-2315: The _merge_relationship_pattern logic only inspects
outgoing edges and ignores rel_pattern.direction; update it to branch on
rel_pattern.direction (Direction.OUT, Direction.IN, Direction.UNDIRECTED): for
OUT continue using get_outgoing_edges(src_node.id) and check edge.dst.id, for IN
use get_incoming_edges(src_node.id) and check edge.src.id, and for UNDIRECTED
iterate both incoming and outgoing (merge/deduplicate edges before property
checks) to find found_rel; finally ensure when creating a new relationship for
the IN direction you swap src_node and dst_node in the creation call so the
stored edge direction is correct.
🧹 Nitpick comments (4)
src/graphforge/executor/executor.py (3)

2236-2252: Use TypeError for type validation checks.

Lines 2248, 2250, and 2252 validate that pattern elements are the correct type (NodePattern, RelationshipPattern). Per Python conventions and the Ruff TRY004 hint, these should raise TypeError rather than ValueError.

♻️ Proposed fix
-        if not isinstance(src_pattern, NodePattern):
-            raise ValueError("First element of relationship pattern must be a node")
-        if not isinstance(rel_pattern, RelationshipPattern):
-            raise ValueError("Second element of relationship pattern must be a relationship")
-        if not isinstance(dst_pattern, NodePattern):
-            raise ValueError("Third element of relationship pattern must be a node")
+        if not isinstance(src_pattern, NodePattern):
+            raise TypeError("First element of relationship pattern must be a node")
+        if not isinstance(rel_pattern, RelationshipPattern):
+            raise TypeError("Second element of relationship pattern must be a relationship")
+        if not isinstance(dst_pattern, NodePattern):
+            raise TypeError("Third element of relationship pattern must be a node")

2332-2427: Significant code duplication with _execute_merge node-finding logic.

_merge_node_in_context (lines 2354–2416) is nearly identical to the node-matching block in _execute_merge (lines 2128–2195). Consider extracting the shared "find node by labels+properties" logic into a private helper (e.g., _find_node_by_pattern) that both call sites invoke. This would reduce maintenance burden and the risk of the two paths drifting apart.


2224-2330: MERGE relationship semantics differ from Neo4j's atomic pattern matching.

In Neo4j, MERGE (a:X)-[r:R]->(b:Y) treats the entire pattern atomically: it looks for the full triple and, if not found, creates all three elements fresh (even if matching nodes already exist). This implementation resolves nodes independently first (_merge_node_in_context for src, then dst), then checks only the relationship — which reuses existing nodes. This is a deliberate simplification but diverges from standard openCypher semantics.

If this is intentional, a brief doc-comment would help future maintainers. If full openCypher fidelity is a goal, this needs to be revisited.

src/graphforge/parser/cypher.lark (1)

48-48: Standalone MERGE path lacks ORDER BY / SKIP / LIMIT support.

Line 48 (merge_clause set_clause? return_clause?) does not allow ORDER BY, SKIP, or LIMIT after RETURN, while the MATCH+MERGE alternative on line 50 does. A query like MERGE (n:Person) RETURN n ORDER BY n.name LIMIT 5 will fail to parse. This is consistent with the existing create_clause return_clause? on line 47, so it's pre-existing, but now that you're touching this line (adding set_clause?), it might be worth aligning:

-           | merge_clause set_clause? return_clause?
+           | merge_clause set_clause? return_clause? order_by_clause? skip_clause? limit_clause?

Comment thread src/graphforge/executor/executor.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 16, 2026

Codecov Report

❌ Patch coverage is 50.00000% with 65 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.37%. Comparing base (d127ac4) to head (f73a386).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

❌ Your patch status has failed because the patch coverage (50.00%) is below the target coverage (75.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #183      +/-   ##
==========================================
- Coverage   88.18%   87.37%   -0.81%     
==========================================
  Files          34       34              
  Lines        5892     6022     +130     
  Branches     1563     1610      +47     
==========================================
+ Hits         5196     5262      +66     
- Misses        396      430      +34     
- Partials      300      330      +30     
Flag Coverage Δ
full-coverage 87.37% <50.00%> (-0.81%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
parser 92.34% <ø> (ø)
planner 95.95% <ø> (ø)
executor 80.97% <50.00%> (-1.51%) ⬇️
storage 99.62% <ø> (ø)
ast 93.84% <ø> (ø)
types 95.36% <ø> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d127ac4...f73a386. Read the comment docs.

- Handle Direction.IN by checking incoming edges and swapping src/dst on creation
- Handle Direction.UNDIRECTED by checking both directions
- Add 3 new tests for incoming and undirected MERGE patterns
- Fix linter warning with simplified comparison

All tests passing (13/13 complex MERGE, 76/76 total MERGE tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
tests/integration/test_complex_merge_patterns.py (1)

9-320: Consider extracting a shared fixture for test isolation.

All tests in this class manually instantiate GraphForge(). A @pytest.fixture yielding a fresh instance would reduce boilerplate and make the isolation guarantee explicit. This is a pre-existing pattern, so not blocking.

Example fixture
import pytest

`@pytest.fixture`
def gf():
    return GraphForge()

Then each test method would accept gf as a parameter instead of creating its own.

As per coding guidelines: "Use test fixtures to ensure test isolation with fresh GraphForge instances for each test."

src/graphforge/executor/executor.py (3)

2247-2252: Use TypeError for type-validation checks.

These guard invalid pattern types, not invalid values. Ruff (TRY004) correctly flags this.

Proposed fix
-        if not isinstance(src_pattern, NodePattern):
-            raise ValueError("First element of relationship pattern must be a node")
-        if not isinstance(rel_pattern, RelationshipPattern):
-            raise ValueError("Second element of relationship pattern must be a relationship")
-        if not isinstance(dst_pattern, NodePattern):
-            raise ValueError("Third element of relationship pattern must be a node")
+        if not isinstance(src_pattern, NodePattern):
+            raise TypeError("First element of relationship pattern must be a node")
+        if not isinstance(rel_pattern, RelationshipPattern):
+            raise TypeError("Second element of relationship pattern must be a relationship")
+        if not isinstance(dst_pattern, NodePattern):
+            raise TypeError("Third element of relationship pattern must be a node")

2366-2461: Significant duplication with _execute_merge node-matching logic.

The node-finding logic in _merge_node_in_context (lines 2388–2450) is nearly identical to the node-matching block in _execute_merge (lines 2128–2206). Consider extracting a shared _find_matching_node(node_pattern, ctx) helper that both paths call, to reduce the maintenance surface.

This isn't blocking since the code is correct, but the ~60 duplicated lines will diverge over time.


2224-2241: TODO left for multi-hop MERGE support.

Line 2237 has a TODO for multi-hop pattern support. This is fine for now since 3-part patterns cover the common case, but consider tracking this as an issue.

Would you like me to open a GitHub issue to track multi-hop MERGE pattern support?

@DecisionNerd DecisionNerd merged commit f3e3341 into main Feb 16, 2026
21 of 22 checks passed
@DecisionNerd DecisionNerd deleted the feature/163-merge-enhancements branch February 19, 2026 02:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: support MERGE + SET without ON CREATE/ON MATCH feat: support relationship MERGE patterns feat: support MERGE after MATCH patterns

1 participant