Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ __pycache__
**/apache_age_python.egg-info

drivers/python/build
*.bc
141 changes: 141 additions & 0 deletions regress/expected/cypher_merge.out
Original file line number Diff line number Diff line change
Expand Up @@ -1888,9 +1888,150 @@ SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype
---
(0 rows)

--
-- ON CREATE SET / ON MATCH SET tests (issue #1619)
--
SELECT create_graph('merge_actions');
NOTICE: graph "merge_actions" has been created
create_graph
--------------

(1 row)

-- Basic ON CREATE SET: first run creates the node
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Alice'})
ON CREATE SET n.created = true
RETURN n.name, n.created
$$) AS (name agtype, created agtype);
name | created
---------+---------
"Alice" | true
(1 row)

-- ON MATCH SET: second run matches the existing node
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Alice'})
ON MATCH SET n.found = true
RETURN n.name, n.created, n.found
$$) AS (name agtype, created agtype, found agtype);
name | created | found
---------+---------+-------
"Alice" | true | true
(1 row)

-- Both ON CREATE SET and ON MATCH SET (first run = create)
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Bob'})
ON CREATE SET n.created = true
ON MATCH SET n.matched = true
RETURN n.name, n.created, n.matched
$$) AS (name agtype, created agtype, matched agtype);
name | created | matched
-------+---------+---------
"Bob" | true |
(1 row)

-- Both ON CREATE SET and ON MATCH SET (second run = match)
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Bob'})
ON CREATE SET n.created = true
ON MATCH SET n.matched = true
RETURN n.name, n.created, n.matched
$$) AS (name agtype, created agtype, matched agtype);
name | created | matched
-------+---------+---------
"Bob" | true | true
(1 row)

-- ON CREATE SET with MERGE after MATCH (Case 1: has predecessor, first run = create)
SELECT * FROM cypher('merge_actions', $$
MATCH (a:Person {name: 'Alice'})
MERGE (a)-[:KNOWS]->(b:Person {name: 'Charlie'})
ON CREATE SET b.source = 'merge_create'
RETURN a.name, b.name, b.source
$$) AS (a agtype, b agtype, source agtype);
a | b | source
---------+-----------+----------------
"Alice" | "Charlie" | "merge_create"
(1 row)

-- ON MATCH SET with MERGE after MATCH (Case 1: has predecessor, second run = match)
SELECT * FROM cypher('merge_actions', $$
MATCH (a:Person {name: 'Alice'})
MERGE (a)-[:KNOWS]->(b:Person {name: 'Charlie'})
ON MATCH SET b.visited = true
RETURN a.name, b.name, b.visited
$$) AS (a agtype, b agtype, visited agtype);
a | b | visited
---------+-----------+---------
"Alice" | "Charlie" | true
(1 row)

-- Multiple SET items in a single ON CREATE SET
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Dave'})
ON CREATE SET n.a = 1, n.b = 2
RETURN n.name, n.a, n.b
$$) AS (name agtype, a agtype, b agtype);
name | a | b
--------+---+---
"Dave" | 1 | 2
(1 row)

-- Reverse order: ON MATCH before ON CREATE should work
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Eve'})
ON MATCH SET n.seen = true
ON CREATE SET n.new = true
RETURN n.name, n.new
$$) AS (name agtype, new agtype);
name | new
-------+------
"Eve" | true
(1 row)

-- Error: ON CREATE SET specified more than once
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Bad'})
ON CREATE SET n.a = 1
ON CREATE SET n.b = 2
RETURN n
$$) AS (n agtype);
ERROR: ON CREATE SET specified more than once
LINE 1: SELECT * FROM cypher('merge_actions', $$
^
-- Error: ON MATCH SET specified more than once
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Bad'})
ON MATCH SET n.a = 1
ON MATCH SET n.b = 2
RETURN n
$$) AS (n agtype);
ERROR: ON MATCH SET specified more than once
LINE 1: SELECT * FROM cypher('merge_actions', $$
^
-- cleanup
SELECT * FROM cypher('merge_actions', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
a
---
(0 rows)

--
-- delete graphs
--
SELECT drop_graph('merge_actions', true);
NOTICE: drop cascades to 4 other objects
DETAIL: drop cascades to table merge_actions._ag_label_vertex
drop cascades to table merge_actions._ag_label_edge
drop cascades to table merge_actions."Person"
drop cascades to table merge_actions."KNOWS"
NOTICE: graph "merge_actions" has been dropped
drop_graph
------------

(1 row)

SELECT drop_graph('issue_1907', true);
NOTICE: drop cascades to 4 other objects
DETAIL: drop cascades to table issue_1907._ag_label_vertex
Expand Down
86 changes: 86 additions & 0 deletions regress/sql/cypher_merge.sql
Original file line number Diff line number Diff line change
Expand Up @@ -868,9 +868,95 @@ SELECT * FROM cypher('issue_1630', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype
SELECT * FROM cypher('issue_1709', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);

--
-- ON CREATE SET / ON MATCH SET tests (issue #1619)
--
SELECT create_graph('merge_actions');

-- Basic ON CREATE SET: first run creates the node
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Alice'})
ON CREATE SET n.created = true
RETURN n.name, n.created
$$) AS (name agtype, created agtype);

-- ON MATCH SET: second run matches the existing node
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Alice'})
ON MATCH SET n.found = true
RETURN n.name, n.created, n.found
$$) AS (name agtype, created agtype, found agtype);

-- Both ON CREATE SET and ON MATCH SET (first run = create)
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Bob'})
ON CREATE SET n.created = true
ON MATCH SET n.matched = true
RETURN n.name, n.created, n.matched
$$) AS (name agtype, created agtype, matched agtype);

-- Both ON CREATE SET and ON MATCH SET (second run = match)
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Bob'})
ON CREATE SET n.created = true
ON MATCH SET n.matched = true
RETURN n.name, n.created, n.matched
$$) AS (name agtype, created agtype, matched agtype);

-- ON CREATE SET with MERGE after MATCH (Case 1: has predecessor, first run = create)
SELECT * FROM cypher('merge_actions', $$
MATCH (a:Person {name: 'Alice'})
MERGE (a)-[:KNOWS]->(b:Person {name: 'Charlie'})
ON CREATE SET b.source = 'merge_create'
RETURN a.name, b.name, b.source
$$) AS (a agtype, b agtype, source agtype);

-- ON MATCH SET with MERGE after MATCH (Case 1: has predecessor, second run = match)
SELECT * FROM cypher('merge_actions', $$
MATCH (a:Person {name: 'Alice'})
MERGE (a)-[:KNOWS]->(b:Person {name: 'Charlie'})
ON MATCH SET b.visited = true
RETURN a.name, b.name, b.visited
$$) AS (a agtype, b agtype, visited agtype);

-- Multiple SET items in a single ON CREATE SET
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Dave'})
ON CREATE SET n.a = 1, n.b = 2
RETURN n.name, n.a, n.b
$$) AS (name agtype, a agtype, b agtype);

-- Reverse order: ON MATCH before ON CREATE should work
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Eve'})
ON MATCH SET n.seen = true
ON CREATE SET n.new = true
RETURN n.name, n.new
$$) AS (name agtype, new agtype);

-- Error: ON CREATE SET specified more than once
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Bad'})
ON CREATE SET n.a = 1
ON CREATE SET n.b = 2
RETURN n
$$) AS (n agtype);

-- Error: ON MATCH SET specified more than once
SELECT * FROM cypher('merge_actions', $$
MERGE (n:Person {name: 'Bad'})
ON MATCH SET n.a = 1
ON MATCH SET n.b = 2
RETURN n
$$) AS (n agtype);

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

There is no test covering the interaction of ON CREATE SET / ON MATCH SET with chained (non-terminal) MERGE statements, which exercises the eager-buffering code path added in PR #2344. The non-terminal MERGE path (lines 664-750 in cypher_merge.c) has ON CREATE SET and ON MATCH SET logic, but the tests only cover Case 1 with a MATCH predecessor (not a MERGE-then-MERGE chain). A test like:

MERGE (a:A {name: 'X'}) ON CREATE SET a.new = true
MERGE (b:B {name: 'Y'}) ON CREATE SET b.new = true
RETURN a, b

or with a chained terminal MERGE following a non-terminal MERGE with ON SET would validate that the eager buffering path handles ON SET correctly.

Suggested change
-- Chained MERGE with ON CREATE SET in non-terminal and terminal clauses
SELECT * FROM cypher('merge_actions', $$
MERGE (a:Person {name: 'ChainCreateA'})
ON CREATE SET a.new = true
MERGE (b:Person {name: 'ChainCreateB'})
ON CREATE SET b.new = true
RETURN a.name, a.new, b.name, b.new
$$) AS (a_name agtype, a_new agtype, b_name agtype, b_new agtype);
-- Chained MERGE with non-terminal ON CREATE SET and terminal ON MATCH SET
-- Setup an existing node to be matched by the second MERGE
SELECT * FROM cypher('merge_actions', $$
CREATE (p:Person {name: 'ChainMatch', seen: false})
$$) AS (n agtype);
SELECT * FROM cypher('merge_actions', $$
MERGE (a:Person {name: 'ChainCreateOnce'})
ON CREATE SET a.created = true
MERGE (b:Person {name: 'ChainMatch'})
ON MATCH SET b.seen = true
RETURN a.name, a.created, b.name, b.seen
$$) AS (a_name agtype, a_created agtype, b_name agtype, b_seen agtype);

Copilot uses AI. Check for mistakes.
-- cleanup
SELECT * FROM cypher('merge_actions', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);

--
-- delete graphs
--
SELECT drop_graph('merge_actions', true);
SELECT drop_graph('issue_1907', true);
SELECT drop_graph('cypher_merge', true);
SELECT drop_graph('issue_1630', true);
Expand Down
Loading