Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
129 changes: 129 additions & 0 deletions regress/expected/cypher_merge.out
Original file line number Diff line number Diff line change
Expand Up @@ -1888,9 +1888,138 @@ 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 MATCH SET with MERGE after MATCH (Case 1: has predecessor)
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)

-- 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
78 changes: 78 additions & 0 deletions regress/sql/cypher_merge.sql
Original file line number Diff line number Diff line change
Expand Up @@ -868,9 +868,87 @@ 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 MATCH SET with MERGE after MATCH (Case 1: has predecessor)
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);

-- 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);

-- 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
84 changes: 75 additions & 9 deletions src/backend/executor/cypher_merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,29 @@ static void process_simple_merge(CustomScanState *node)

/* setup the scantuple that the process_path needs */
econtext->ecxt_scantuple = sss->ss.ss_ScanTupleSlot;
mark_tts_isnull(econtext->ecxt_scantuple);

process_path(css, NULL, true);

/* ON CREATE SET: path was just created */
if (css->on_create_set_info)
{
ExecStoreVirtualTuple(econtext->ecxt_scantuple);
apply_update_list(&css->css, css->on_create_set_info);
}
}
else
{
/* ON MATCH SET: path already exists */
if (css->on_match_set_info)
{
ExprContext *econtext = node->ss.ps.ps_ExprContext;

econtext->ecxt_scantuple =
node->ss.ps.lefttree->ps_ProjInfo->pi_exprContext->ecxt_scantuple;

apply_update_list(&css->css, css->on_match_set_info);
}
}
}

Expand Down Expand Up @@ -657,6 +678,11 @@ static TupleTableSlot *exec_cypher_merge(CustomScanState *node)
free_path_entry_array(prebuilt_path_array,
path_length);
process_path(css, found_path_array, false);

/* ON MATCH SET: path was found as duplicate */
if (css->on_match_set_info)
apply_update_list(&css->css,
css->on_match_set_info);
}
else
{
Expand All @@ -668,8 +694,19 @@ static TupleTableSlot *exec_cypher_merge(CustomScanState *node)
css->created_paths_list = new_path;

process_path(css, prebuilt_path_array, true);

/* ON CREATE SET: path was just created */
if (css->on_create_set_info)
apply_update_list(&css->css,
css->on_create_set_info);
}
}
else
{
/* ON MATCH SET: path already existed from lateral join */
if (css->on_match_set_info)
apply_update_list(&css->css, css->on_match_set_info);
}

/* Project the result and save a copy */
econtext->ecxt_scantuple =
Expand Down Expand Up @@ -742,6 +779,10 @@ static TupleTableSlot *exec_cypher_merge(CustomScanState *node)
{
free_path_entry_array(prebuilt_path_array, path_length);
process_path(css, found_path_array, false);

/* ON MATCH SET: path was found as duplicate */
if (css->on_match_set_info)
apply_update_list(&css->css, css->on_match_set_info);
}
else
{
Expand All @@ -752,8 +793,18 @@ static TupleTableSlot *exec_cypher_merge(CustomScanState *node)
css->created_paths_list = new_path;

process_path(css, prebuilt_path_array, true);

/* ON CREATE SET: path was just created */
if (css->on_create_set_info)
apply_update_list(&css->css, css->on_create_set_info);
}
}
else
{
/* ON MATCH SET: path already existed from lateral join */
if (css->on_match_set_info)
apply_update_list(&css->css, css->on_match_set_info);
}

} while (true);

Expand Down Expand Up @@ -826,6 +877,14 @@ static TupleTableSlot *exec_cypher_merge(CustomScanState *node)
*/
css->found_a_path = true;

/* ON MATCH SET: path already exists */
if (css->on_match_set_info)
{
econtext->ecxt_scantuple =
node->ss.ps.lefttree->ps_ProjInfo->pi_exprContext->ecxt_scantuple;
apply_update_list(&css->css, css->on_match_set_info);
}

econtext->ecxt_scantuple = ExecProject(node->ss.ps.lefttree->ps_ProjInfo);
return ExecProject(node->ss.ps.ps_ProjInfo);
}
Expand Down Expand Up @@ -886,21 +945,26 @@ static TupleTableSlot *exec_cypher_merge(CustomScanState *node)
/* setup the scantuple that the process_path needs */
econtext->ecxt_scantuple = sss->ss.ss_ScanTupleSlot;

/* create the path */
process_path(css, NULL, true);

/* mark the create_new_path flag to true. */
css->created_new_path = true;

/*
* find the tts_values that process_path did not populate and
* mark as null.
* Initialize the scan tuple slot as all-null before process_path
* populates it with the created entities. This ensures the slot
* is properly set up for apply_update_list.
*/
mark_tts_isnull(econtext->ecxt_scantuple);

/* store the heap tuble */
/* create the path */
process_path(css, NULL, true);

/* mark the slot as valid so tts_nvalid reflects natts */
ExecStoreVirtualTuple(econtext->ecxt_scantuple);

/* ON CREATE SET: path was just created */
if (css->on_create_set_info)
apply_update_list(&css->css, css->on_create_set_info);

/* mark the create_new_path flag to true. */
css->created_new_path = true;

/*
* make the subquery's projection scan slot be the tuple table we
* created and run the projection logic.
Expand Down Expand Up @@ -1029,6 +1093,8 @@ Node *create_cypher_merge_plan_state(CustomScan *cscan)
cypher_css->created_new_path = false;
cypher_css->found_a_path = false;
cypher_css->graph_oid = merge_information->graph_oid;
cypher_css->on_match_set_info = merge_information->on_match_set_info;
cypher_css->on_create_set_info = merge_information->on_create_set_info;

cypher_css->css.ss.ps.type = T_CustomScanState;
cypher_css->css.methods = &cypher_merge_exec_methods;
Expand Down
Loading