You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@age.apache.org by jo...@apache.org on 2022/02/10 01:41:56 UTC

[incubator-age] branch master updated: Implement Merge Clause

This is an automated email from the ASF dual-hosted git repository.

joshinnis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-age.git


The following commit(s) were added to refs/heads/master by this push:
     new bf55ad0  Implement Merge Clause
bf55ad0 is described below

commit bf55ad0268bb4314071aa678c4b03337426d43ff
Author: Josh Innis <Jo...@gmail.com>
AuthorDate: Wed Feb 9 17:37:45 2022 -0800

    Implement Merge Clause
    
    MERGE either matches existing nodes and binds them, or
    it creates new data and binds that. It’s like a
    combination  of  MATCH  and  CREATE  that  additionally
    allows  you  to  specify  what  happens  if  the  data
    was matched or created.
    
    For example, you can specify that the graph must contain
    a node for a user with a certain name. If there isn’t a
    node with the correct name, a new node will be created
    and its name property set.
    
    When  using  MERGE  on  full patterns, the behavior is
    that either the whole pattern matches, or the whole
    pattern is created. MERGE will not partially use existing
    patterns—it’s all or nothing. If partial matches are
    needed, this can be accomplished by splitting a pattern up
    into multiple MERGE clauses.
    
    As with MATCH, MERGE can match multiple occurrences of a
    pattern. If there are multiple matches, they will all be
    passed on to later stages of the query.
---
 Makefile                                  |    2 +
 age--0.7.0.sql                            |    5 +
 regress/expected/cypher_merge.out         |  835 ++++++++++++++++++++++++
 regress/sql/cypher_merge.sql              |  471 ++++++++++++++
 src/backend/executor/cypher_create.c      |   75 +--
 src/backend/executor/cypher_merge.c       |  901 ++++++++++++++++++++++++++
 src/backend/executor/cypher_utils.c       |   88 ++-
 src/backend/nodes/ag_nodes.c              |    8 +-
 src/backend/nodes/cypher_copyfuncs.c      |   15 +-
 src/backend/nodes/cypher_outfuncs.c       |   22 +
 src/backend/nodes/cypher_readfuncs.c      |   17 +
 src/backend/optimizer/cypher_createplan.c |   62 ++
 src/backend/optimizer/cypher_pathnode.c   |   45 +-
 src/backend/optimizer/cypher_paths.c      |   34 +-
 src/backend/parser/cypher_analyze.c       |    4 +-
 src/backend/parser/cypher_clause.c        | 1002 ++++++++++++++++++++++++-----
 src/backend/parser/cypher_gram.y          |   22 +-
 src/backend/parser/cypher_keywords.c      |    1 +
 src/backend/utils/adt/cypher_funcs.c      |    7 +
 src/include/executor/cypher_executor.h    |    4 +
 src/include/executor/cypher_utils.h       |   19 +
 src/include/nodes/ag_nodes.h              |    4 +-
 src/include/nodes/cypher_copyfuncs.h      |    2 +
 src/include/nodes/cypher_nodes.h          |   21 +
 src/include/nodes/cypher_outfuncs.h       |    4 +
 src/include/nodes/cypher_readfuncs.h      |    2 +
 src/include/optimizer/cypher_createplan.h |    8 +-
 src/include/optimizer/cypher_pathnode.h   |    5 +-
 src/include/utils/ag_func.h               |    1 +
 29 files changed, 3442 insertions(+), 244 deletions(-)

diff --git a/Makefile b/Makefile
index d2cf4bc..19c460e 100644
--- a/Makefile
+++ b/Makefile
@@ -27,6 +27,7 @@ OBJS = src/backend/age.o \
        src/backend/commands/graph_commands.o \
        src/backend/commands/label_commands.o \
        src/backend/executor/cypher_create.o \
+       src/backend/executor/cypher_merge.o \
        src/backend/executor/cypher_set.o \
        src/backend/executor/cypher_utils.o \
        src/backend/nodes/ag_nodes.o \
@@ -79,6 +80,7 @@ REGRESS = scan \
           cypher_with \
           cypher_vle \
           cypher_union \
+          cypher_merge \
           drop
 
 srcdir=`pwd`
diff --git a/age--0.7.0.sql b/age--0.7.0.sql
index c97f7f6..851a089 100644
--- a/age--0.7.0.sql
+++ b/age--0.7.0.sql
@@ -3130,6 +3130,11 @@ RETURNS void
 LANGUAGE c
 AS 'MODULE_PATHNAME';
 
+CREATE FUNCTION ag_catalog._cypher_merge_clause(internal)
+RETURNS void
+LANGUAGE c
+AS 'MODULE_PATHNAME';
+
 --
 -- query functions
 --
diff --git a/regress/expected/cypher_merge.out b/regress/expected/cypher_merge.out
new file mode 100644
index 0000000..d9841e2
--- /dev/null
+++ b/regress/expected/cypher_merge.out
@@ -0,0 +1,835 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+LOAD 'age';
+SET search_path TO ag_catalog;
+SELECT create_graph('cypher_merge');
+NOTICE:  graph "cypher_merge" has been created
+ create_graph 
+--------------
+ 
+(1 row)
+
+/*
+ * Section 1: MERGE with single vertex
+ */
+/*
+ * test 1: Single MERGE Clause, path doesn't exist
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n {i: "Hello Merge"})$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+                                        n                                         
+----------------------------------------------------------------------------------
+ {"id": 281474976710657, "label": "", "properties": {"i": "Hello Merge"}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 2: Single MERGE Clause, path exists
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: "Hello Merge"}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test_query
+SELECT * FROM cypher('cypher_merge', $$MERGE ({i: "Hello Merge"})$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+                                        n                                         
+----------------------------------------------------------------------------------
+ {"id": 281474976710658, "label": "", "properties": {"i": "Hello Merge"}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 3: Prev clause returns no results, no data created
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE ({i: n.i})$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+ n 
+---
+(0 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 4: Prev clause has results, path exists
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: "Hello Merge"}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE ({i: n.i})$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+                                        n                                         
+----------------------------------------------------------------------------------
+ {"id": 281474976710659, "label": "", "properties": {"i": "Hello Merge"}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 5: Prev clause has results, path does not exist (differnt property name)
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: "Hello Merge"}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE ({j: n.i})$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+                                        n                                         
+----------------------------------------------------------------------------------
+ {"id": 281474976710660, "label": "", "properties": {"i": "Hello Merge"}}::vertex
+ {"id": 281474976710661, "label": "", "properties": {"j": "Hello Merge"}}::vertex
+(2 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 6: MERGE with no prev clause, filters correctly, data created
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: 2}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n {i: 1}) RETURN n$$) AS (a agtype);
+                                  a                                   
+----------------------------------------------------------------------
+ {"id": 281474976710663, "label": "", "properties": {"i": 1}}::vertex
+(1 row)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+                                  n                                   
+----------------------------------------------------------------------
+ {"id": 281474976710662, "label": "", "properties": {"i": 2}}::vertex
+ {"id": 281474976710663, "label": "", "properties": {"i": 1}}::vertex
+(2 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 7: MERGE with no prev clause, filters correctly, no data created
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: 1}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: 1}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: 2}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_merge', $$CREATE () $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n {i: 1}) RETURN n$$) AS (a agtype);
+                                  a                                   
+----------------------------------------------------------------------
+ {"id": 281474976710664, "label": "", "properties": {"i": 1}}::vertex
+ {"id": 281474976710665, "label": "", "properties": {"i": 1}}::vertex
+(2 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+                                  n                                   
+----------------------------------------------------------------------
+ {"id": 281474976710664, "label": "", "properties": {"i": 1}}::vertex
+ {"id": 281474976710665, "label": "", "properties": {"i": 1}}::vertex
+ {"id": 281474976710666, "label": "", "properties": {"i": 2}}::vertex
+ {"id": 281474976710667, "label": "", "properties": {}}::vertex
+(4 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * Section 2: MERGE with edges
+ */
+/*
+ * test 8: MERGE creates edge
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE () $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE (n)-[:e]->(:v)$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e:e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+                               n                                |                                                           e                                                            |                                m                                 
+----------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------
+ {"id": 281474976710668, "label": "", "properties": {}}::vertex | {"id": 844424930131969, "label": "e", "end_id": 1125899906842625, "start_id": 281474976710668, "properties": {}}::edge | {"id": 1125899906842625, "label": "v", "properties": {}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 9: edge already exists
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n)-[:e]->(:v)$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e:e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+                               n                                |                                                           e                                                            |                                m                                 
+----------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------
+ {"id": 281474976710671, "label": "", "properties": {}}::vertex | {"id": 844424930131971, "label": "e", "end_id": 1125899906842626, "start_id": 281474976710671, "properties": {}}::edge | {"id": 1125899906842626, "label": "v", "properties": {}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 10: edge doesn't exist, using MATCH
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE () $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE (n)-[:e]->(:v)$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e:e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+                               n                                |                                                           e                                                            |                                m                                 
+----------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------
+ {"id": 281474976710672, "label": "", "properties": {}}::vertex | {"id": 844424930131972, "label": "e", "end_id": 1125899906842627, "start_id": 281474976710672, "properties": {}}::edge | {"id": 1125899906842627, "label": "v", "properties": {}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 11: edge already exists, using MATCH
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE (n)-[:e]->(:v)$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e:e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+                               n                                |                                                           e                                                            |                                m                                 
+----------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------
+ {"id": 281474976710673, "label": "", "properties": {}}::vertex | {"id": 844424930131974, "label": "e", "end_id": 1125899906842628, "start_id": 281474976710673, "properties": {}}::edge | {"id": 1125899906842628, "label": "v", "properties": {}}::vertex
+ {"id": 281474976710674, "label": "", "properties": {}}::vertex | {"id": 844424930131975, "label": "e", "end_id": 1125899906842629, "start_id": 281474976710674, "properties": {}}::edge | {"id": 1125899906842629, "label": "v", "properties": {}}::vertex
+(2 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 12: Partial Path Exists, creates whole path
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE ()-[:e]->()-[:e]->()$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+--Returns 3. One for the data setup and 2 for the longer path in MERGE
+SELECT count(*) FROM cypher('cypher_merge', $$MATCH p=()-[e:e]->() RETURN p$$) AS (p agtype)
+-- Returns 1, the path created in MERGE
+SELECT count(*) FROM cypher('cypher_merge', $$MATCH p=()-[:e]->()-[]->() RETURN p$$) AS (p agtype);
+ERROR:  syntax error at or near "SELECT"
+LINE 3: SELECT count(*) FROM cypher('cypher_merge', $$MATCH p=()-[:e...
+        ^
+-- 5 vertices total should have been created
+SELECT count(*) FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+ count 
+-------
+     5
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 13: edge doesn't exists (differnt label), using MATCH
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE (n)-[:e_new]->(:v)$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+                               n                                |                                                              e                                                              |                                m                                 
+----------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------
+ {"id": 281474976710680, "label": "", "properties": {}}::vertex | {"id": 1407374883553281, "label": "e_new", "end_id": 1125899906842630, "start_id": 281474976710680, "properties": {}}::edge | {"id": 1125899906842630, "label": "v", "properties": {}}::vertex
+ {"id": 281474976710681, "label": "", "properties": {}}::vertex | {"id": 1407374883553282, "label": "e_new", "end_id": 1125899906842631, "start_id": 281474976710681, "properties": {}}::edge | {"id": 1125899906842631, "label": "v", "properties": {}}::vertex
+(2 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 14: edge doesn't exists (different label), without MATCH
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n)-[:e_new]->(:v)$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+                               n                                |                                                              e                                                              |                                m                                 
+----------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------
+ {"id": 281474976710684, "label": "", "properties": {}}::vertex | {"id": 1407374883553283, "label": "e_new", "end_id": 1125899906842632, "start_id": 281474976710684, "properties": {}}::edge | {"id": 1125899906842632, "label": "v", "properties": {}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * Section 3: MERGE with writing clauses
+ */
+/*
+ * test 15:
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE () MERGE (n)$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+                               n                                
+----------------------------------------------------------------
+ {"id": 281474976710685, "label": "", "properties": {}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 16:
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE (n) WITH n as a MERGE (a)-[:e]->() $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[:e]->() RETURN p$$) AS (p agtype);
+                                                                                                                               p                                                                                                                               
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710687, "label": "", "properties": {}}::vertex, {"id": 844424930131981, "label": "e", "end_id": 281474976710688, "start_id": 281474976710687, "properties": {}}::edge, {"id": 281474976710688, "label": "", "properties": {}}::vertex]::path
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 17:
+ * XXX: Incorrect Output. To FIX
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE (n) MERGE (n)-[:e]->() $$) AS (a agtype);
+ERROR:  end_id() argument must resolve to a scalar value
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[:e]->() RETURN p$$) AS (p agtype);
+ p 
+---
+(0 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 18:
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1}) SET n.i = 2 MERGE ({i: 2}) $$) AS (a agtype);
+ERROR:  missing FROM-clause entry for table "_age_default_alias_0"
+LINE 5: SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1}) SE...
+                                               ^
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 19:
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1}) SET n.i = 2 WITH n as a MERGE ({i: 2}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+                                  a                                   
+----------------------------------------------------------------------
+ {"id": 281474976710690, "label": "", "properties": {"i": 2}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 20:
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1})$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n {i : 1}) SET n.i = 2 WITH n as a MERGE ({i: 2}) $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+                                  a                                   
+----------------------------------------------------------------------
+ {"id": 281474976710691, "label": "", "properties": {"i": 2}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 21:
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1})$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n {i : 1}) DELETE n  MERGE (n)-[:e]->() $$) AS (a agtype);
+ERROR:  vertex assigned to variable n was deleted
+--validate, transaction was rolled back because of the error message
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+                                  a                                   
+----------------------------------------------------------------------
+ {"id": 281474976710692, "label": "", "properties": {"i": 1}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 22:
+ * MERGE after MERGE
+ */
+SELECT * FROM cypher('cypher_merge', $$
+    CREATE (n:Person {name : "Rob Reiner", bornIn: "New York"})
+$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_merge', $$
+    CREATE (n:Person {name : "Michael Douglas", bornIn: "New Jersey"})
+$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_merge', $$
+    CREATE (n:Person {name : "Martin Sheen", bornIn: "Ohio"})
+$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$
+    MATCH (person:Person)
+    MERGE (city:City {name: person.bornIn})
+    MERGE (person)-[r:BORN_IN]->(city)
+    RETURN person.name, person.bornIn, city
+$$) AS (name agtype, bornIn agtype, city agtype);
+       name        |    bornin    |                                          city                                           
+-------------------+--------------+-----------------------------------------------------------------------------------------
+ "Michael Douglas" | "New Jersey" | {"id": 1970324836974594, "label": "City", "properties": {"name": "New Jersey"}}::vertex
+ "Martin Sheen"    | "Ohio"       | {"id": 1970324836974595, "label": "City", "properties": {"name": "Ohio"}}::vertex
+ "Rob Reiner"      | "New York"   | {"id": 1970324836974593, "label": "City", "properties": {"name": "New York"}}::vertex
+(3 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+                                                           a                                                            
+------------------------------------------------------------------------------------------------------------------------
+ {"id": 1688849860263937, "label": "Person", "properties": {"name": "Rob Reiner", "bornIn": "New York"}}::vertex
+ {"id": 1688849860263938, "label": "Person", "properties": {"name": "Michael Douglas", "bornIn": "New Jersey"}}::vertex
+ {"id": 1688849860263939, "label": "Person", "properties": {"name": "Martin Sheen", "bornIn": "Ohio"}}::vertex
+ {"id": 1970324836974593, "label": "City", "properties": {"name": "New York"}}::vertex
+ {"id": 1970324836974594, "label": "City", "properties": {"name": "New Jersey"}}::vertex
+ {"id": 1970324836974595, "label": "City", "properties": {"name": "Ohio"}}::vertex
+(6 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 23:
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE ()-[:e]-()$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[]->() RETURN p$$) AS (a agtype);
+                                                                                                                               a                                                                                                                               
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710693, "label": "", "properties": {}}::vertex, {"id": 844424930131982, "label": "e", "end_id": 281474976710694, "start_id": 281474976710693, "properties": {}}::edge, {"id": 281474976710694, "label": "", "properties": {}}::vertex]::path
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 24:
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE (a) RETURN a$$) AS (a agtype);
+                               a                                
+----------------------------------------------------------------
+ {"id": 281474976710695, "label": "", "properties": {}}::vertex
+(1 row)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+                               a                                
+----------------------------------------------------------------
+ {"id": 281474976710695, "label": "", "properties": {}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 25:
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE p=()-[:e]-() RETURN p$$) AS (a agtype);
+                                                                                                                               a                                                                                                                               
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710696, "label": "", "properties": {}}::vertex, {"id": 844424930131983, "label": "e", "end_id": 281474976710697, "start_id": 281474976710696, "properties": {}}::edge, {"id": 281474976710697, "label": "", "properties": {}}::vertex]::path
+(1 row)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[]->() RETURN p$$) AS (a agtype);
+                                                                                                                               a                                                                                                                               
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710696, "label": "", "properties": {}}::vertex, {"id": 844424930131983, "label": "e", "end_id": 281474976710697, "start_id": 281474976710696, "properties": {}}::edge, {"id": 281474976710697, "label": "", "properties": {}}::vertex]::path
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 26:
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE (a)-[:e]-(b) RETURN a$$) AS (a agtype);
+                               a                                
+----------------------------------------------------------------
+ {"id": 281474976710698, "label": "", "properties": {}}::vertex
+(1 row)
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[]->() RETURN p$$) AS (a agtype);
+                                                                                                                               a                                                                                                                               
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710698, "label": "", "properties": {}}::vertex, {"id": 844424930131984, "label": "e", "end_id": 281474976710699, "start_id": 281474976710698, "properties": {}}::edge, {"id": 281474976710699, "label": "", "properties": {}}::vertex]::path
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * test 27:
+ */
+SELECT  * FROM cypher('cypher_merge', $$CREATE p=()-[:e]->() RETURN p$$) AS (a agtype);
+                                                                                                                               a                                                                                                                               
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710700, "label": "", "properties": {}}::vertex, {"id": 844424930131985, "label": "e", "end_id": 281474976710701, "start_id": 281474976710700, "properties": {}}::edge, {"id": 281474976710701, "label": "", "properties": {}}::vertex]::path
+(1 row)
+
+SELECT * FROM cypher('cypher_merge', $$MERGE p=()-[:e]-() RETURN p$$) AS (a agtype);
+                                                                                                                               a                                                                                                                               
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710700, "label": "", "properties": {}}::vertex, {"id": 844424930131985, "label": "e", "end_id": 281474976710701, "start_id": 281474976710700, "properties": {}}::edge, {"id": 281474976710701, "label": "", "properties": {}}::vertex]::path
+ [{"id": 281474976710701, "label": "", "properties": {}}::vertex, {"id": 844424930131985, "label": "e", "end_id": 281474976710701, "start_id": 281474976710700, "properties": {}}::edge, {"id": 281474976710700, "label": "", "properties": {}}::vertex]::path
+(2 rows)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * Section 4: Error Messages
+ */
+/*
+ * test 28:
+ * Only single paths allowed
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE (n), (m) RETURN n, m$$) AS (a agtype, b agtype);
+ERROR:  syntax error at or near ","
+LINE 8: SELECT * FROM cypher('cypher_merge', $$MERGE (n), (m) RETURN...
+                                                        ^
+/*
+ * test 29:
+ * Edges cannot reference existing variables
+ */
+SELECT * FROM cypher('cypher_merge', $$MATCH ()-[e]-() MERGE ()-[e]->()$$) AS (a agtype);
+ERROR:  variable e already exists
+LINE 5: ...cypher('cypher_merge', $$MATCH ()-[e]-() MERGE ()-[e]->()$$)...
+                                                             ^
+/*
+ * test 30:
+ * NULL vertex given to MERGE
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE (n)$$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) OPTIONAL MATCH (n)-[:e]->(m) MERGE (m)$$) AS (a agtype);
+ERROR:  Existing variable m cannot be NULL in MERGE clause
+-- validate only 1 vertex exits
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (a agtype);
+                               a                                
+----------------------------------------------------------------
+ {"id": 281474976710702, "label": "", "properties": {}}::vertex
+(1 row)
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+ a 
+---
+(0 rows)
+
+/*
+ * Clean up graph
+ */
+SELECT drop_graph('cypher_merge', true);
+NOTICE:  drop cascades to 8 other objects
+DETAIL:  drop cascades to table cypher_merge._ag_label_vertex
+drop cascades to table cypher_merge._ag_label_edge
+drop cascades to table cypher_merge.e
+drop cascades to table cypher_merge.v
+drop cascades to table cypher_merge.e_new
+drop cascades to table cypher_merge."Person"
+drop cascades to table cypher_merge."City"
+drop cascades to table cypher_merge."BORN_IN"
+NOTICE:  graph "cypher_merge" has been dropped
+ drop_graph 
+------------
+ 
+(1 row)
+
diff --git a/regress/sql/cypher_merge.sql b/regress/sql/cypher_merge.sql
new file mode 100644
index 0000000..03b7515
--- /dev/null
+++ b/regress/sql/cypher_merge.sql
@@ -0,0 +1,471 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+LOAD 'age';
+SET search_path TO ag_catalog;
+
+SELECT create_graph('cypher_merge');
+
+
+/*
+ * Section 1: MERGE with single vertex
+ */
+/*
+ * test 1: Single MERGE Clause, path doesn't exist
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n {i: "Hello Merge"})$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 2: Single MERGE Clause, path exists
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: "Hello Merge"}) $$) AS (a agtype);
+
+--test_query
+SELECT * FROM cypher('cypher_merge', $$MERGE ({i: "Hello Merge"})$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 3: Prev clause returns no results, no data created
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE ({i: n.i})$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 4: Prev clause has results, path exists
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: "Hello Merge"}) $$) AS (a agtype);
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE ({i: n.i})$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 5: Prev clause has results, path does not exist (differnt property name)
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: "Hello Merge"}) $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE ({j: n.i})$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 6: MERGE with no prev clause, filters correctly, data created
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: 2}) $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n {i: 1}) RETURN n$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 7: MERGE with no prev clause, filters correctly, no data created
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: 1}) $$) AS (a agtype);
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: 1}) $$) AS (a agtype);
+SELECT * FROM cypher('cypher_merge', $$CREATE ({i: 2}) $$) AS (a agtype);
+SELECT * FROM cypher('cypher_merge', $$CREATE () $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n {i: 1}) RETURN n$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+
+/*
+ * Section 2: MERGE with edges
+ */
+
+/*
+ * test 8: MERGE creates edge
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE () $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE (n)-[:e]->(:v)$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e:e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+
+/*
+ * test 9: edge already exists
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n)-[:e]->(:v)$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e:e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 10: edge doesn't exist, using MATCH
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE () $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE (n)-[:e]->(:v)$$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e:e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 11: edge already exists, using MATCH
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE (n)-[:e]->(:v)$$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e:e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 12: Partial Path Exists, creates whole path
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE ()-[:e]->()-[:e]->()$$) AS (a agtype);
+
+--validate created correctly
+--Returns 3. One for the data setup and 2 for the longer path in MERGE
+SELECT count(*) FROM cypher('cypher_merge', $$MATCH p=()-[e:e]->() RETURN p$$) AS (p agtype)
+
+-- Returns 1, the path created in MERGE
+SELECT count(*) FROM cypher('cypher_merge', $$MATCH p=()-[:e]->()-[]->() RETURN p$$) AS (p agtype);
+
+-- 5 vertices total should have been created
+SELECT count(*) FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 13: edge doesn't exists (differnt label), using MATCH
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) MERGE (n)-[:e_new]->(:v)$$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 14: edge doesn't exists (different label), without MATCH
+ */
+-- setup
+SELECT * FROM cypher('cypher_merge', $$CREATE ()-[:e]->() $$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MERGE (n)-[:e_new]->(:v)$$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n)-[e]->(m:v) RETURN n, e, m$$) AS (n agtype, e agtype, m agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * Section 3: MERGE with writing clauses
+ */
+
+/*
+ * test 15:
+ */
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE () MERGE (n)$$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (n agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 16:
+ */
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE (n) WITH n as a MERGE (a)-[:e]->() $$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[:e]->() RETURN p$$) AS (p agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+
+/*
+ * test 17:
+ * XXX: Incorrect Output. To FIX
+ */
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE (n) MERGE (n)-[:e]->() $$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[:e]->() RETURN p$$) AS (p agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+
+/*
+ * test 18:
+ */
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1}) SET n.i = 2 MERGE ({i: 2}) $$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 19:
+ */
+--test query
+SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1}) SET n.i = 2 WITH n as a MERGE ({i: 2}) $$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 20:
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1})$$) AS (a agtype);
+
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n {i : 1}) SET n.i = 2 WITH n as a MERGE ({i: 2}) $$) AS (a agtype);
+
+--validate created correctly
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 21:
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE (n {i : 1})$$) AS (a agtype);
+
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n {i : 1}) DELETE n  MERGE (n)-[:e]->() $$) AS (a agtype);
+
+--validate, transaction was rolled back because of the error message
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 22:
+ * MERGE after MERGE
+ */
+SELECT * FROM cypher('cypher_merge', $$
+    CREATE (n:Person {name : "Rob Reiner", bornIn: "New York"})
+$$) AS (a agtype);
+
+SELECT * FROM cypher('cypher_merge', $$
+    CREATE (n:Person {name : "Michael Douglas", bornIn: "New Jersey"})
+$$) AS (a agtype);
+
+SELECT * FROM cypher('cypher_merge', $$
+    CREATE (n:Person {name : "Martin Sheen", bornIn: "Ohio"})
+$$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$
+    MATCH (person:Person)
+    MERGE (city:City {name: person.bornIn})
+    MERGE (person)-[r:BORN_IN]->(city)
+    RETURN person.name, person.bornIn, city
+$$) AS (name agtype, bornIn agtype, city agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 23:
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE ()-[:e]-()$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[]->() RETURN p$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 24:
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE (a) RETURN a$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH (a) RETURN a$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 25:
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE p=()-[:e]-() RETURN p$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[]->() RETURN p$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 26:
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE (a)-[:e]-(b) RETURN a$$) AS (a agtype);
+
+--validate
+SELECT * FROM cypher('cypher_merge', $$MATCH p=()-[]->() RETURN p$$) AS (a agtype);
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * test 27:
+ */
+SELECT  * FROM cypher('cypher_merge', $$CREATE p=()-[:e]->() RETURN p$$) AS (a agtype);
+
+SELECT * FROM cypher('cypher_merge', $$MERGE p=()-[:e]-() RETURN p$$) AS (a agtype);
+
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * Section 4: Error Messages
+ */
+/*
+ * test 28:
+ * Only single paths allowed
+ */
+SELECT * FROM cypher('cypher_merge', $$MERGE (n), (m) RETURN n, m$$) AS (a agtype, b agtype);
+
+/*
+ * test 29:
+ * Edges cannot reference existing variables
+ */
+SELECT * FROM cypher('cypher_merge', $$MATCH ()-[e]-() MERGE ()-[e]->()$$) AS (a agtype);
+
+/*
+ * test 30:
+ * NULL vertex given to MERGE
+ */
+--data setup
+SELECT * FROM cypher('cypher_merge', $$CREATE (n)$$) AS (a agtype);
+
+--test query
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) OPTIONAL MATCH (n)-[:e]->(m) MERGE (m)$$) AS (a agtype);
+
+-- validate only 1 vertex exits
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) RETURN n$$) AS (a agtype);
+
+
+--clean up
+SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
+
+/*
+ * Clean up graph
+ */
+SELECT drop_graph('cypher_merge', true);
+
diff --git a/src/backend/executor/cypher_create.c b/src/backend/executor/cypher_create.c
index b397bc6..8ca6220 100644
--- a/src/backend/executor/cypher_create.c
+++ b/src/backend/executor/cypher_create.c
@@ -51,10 +51,9 @@ static void create_edge(cypher_create_custom_scan_state *css,
 
 static Datum create_vertex(cypher_create_custom_scan_state *css,
                            cypher_target_node *node, ListCell *next);
-static HeapTuple insert_entity_tuple(ResultRelInfo *resultRelInfo,
-                                TupleTableSlot *elemTupleSlot, EState *estate);
+
 static void process_pattern(cypher_create_custom_scan_state *css);
-static bool entity_exists(EState *estate, Oid graph_oid, graphid id);
+
 
 const CustomExecMethods cypher_create_exec_methods = {CREATE_SCAN_STATE_NAME,
                                                       begin_cypher_create,
@@ -594,73 +593,3 @@ static Datum create_vertex(cypher_create_custom_scan_state *css,
     return id;
 }
 
-/*
- * Find out if the entity still exists. This is for 'implicit' deletion
- * of an entity.
- */
-static bool entity_exists(EState *estate, Oid graph_oid, graphid id)
-{
-    label_cache_data *label;
-    ScanKeyData scan_keys[1];
-    HeapScanDesc scan_desc;
-    HeapTuple tuple;
-    Relation rel;
-    bool result = true;
-
-    /*
-     * Extract the label id from the graph id and get the table name
-     * the entity is part of.
-     */
-    label = search_label_graph_id_cache(graph_oid, GET_LABEL_ID(id));
-
-    // Setup the scan key to be the graphid
-    ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber,
-                F_GRAPHIDEQ, GRAPHID_GET_DATUM(id));
-
-    rel = heap_open(label->relation, RowExclusiveLock);
-    scan_desc = heap_beginscan(rel, estate->es_snapshot, 1, scan_keys);
-
-    tuple = heap_getnext(scan_desc, ForwardScanDirection);
-
-    /*
-     * If a single tuple was returned, the tuple is still valid, otherwise'
-     * set to false.
-     */
-    if (!HeapTupleIsValid(tuple))
-        result = false;
-
-    heap_endscan(scan_desc);
-    heap_close(rel, RowExclusiveLock);
-
-    return result;
-}
-
-/*
- * Insert the edge/vertex tuple into the table and indices. If the table's
- * constraints have not been violated.
- */
-static HeapTuple insert_entity_tuple(ResultRelInfo *resultRelInfo,
-                                     TupleTableSlot *elemTupleSlot,
-                                     EState *estate)
-{
-    HeapTuple tuple;
-
-    ExecStoreVirtualTuple(elemTupleSlot);
-    tuple = ExecMaterializeSlot(elemTupleSlot);
-
-    // Check the constraints of the tuple
-    tuple->t_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
-    if (resultRelInfo->ri_RelationDesc->rd_att->constr != NULL)
-        ExecConstraints(resultRelInfo, elemTupleSlot, estate);
-
-    // Insert the tuple normally
-    heap_insert(resultRelInfo->ri_RelationDesc, tuple,
-                GetCurrentCommandId(true), 0, NULL);
-
-    // Insert index entries for the tuple
-    if (resultRelInfo->ri_NumIndices > 0)
-        ExecInsertIndexTuples(elemTupleSlot, &(tuple->t_self), estate, false,
-                              NULL, NIL);
-
-    return tuple;
-}
diff --git a/src/backend/executor/cypher_merge.c b/src/backend/executor/cypher_merge.c
new file mode 100644
index 0000000..4bca293
--- /dev/null
+++ b/src/backend/executor/cypher_merge.c
@@ -0,0 +1,901 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "executor/tuptable.h"
+#include "nodes/execnodes.h"
+#include "nodes/extensible.h"
+#include "nodes/nodes.h"
+#include "nodes/plannodes.h"
+#include "parser/parse_relation.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/rel.h"
+#include "utils/tqual.h"
+
+#include "catalog/ag_label.h"
+#include "executor/cypher_executor.h"
+#include "executor/cypher_utils.h"
+#include "nodes/cypher_nodes.h"
+#include "utils/agtype.h"
+#include "utils/ag_cache.h"
+#include "utils/graphid.h"
+
+static void begin_cypher_merge(CustomScanState *node, EState *estate,
+                               int eflags);
+static TupleTableSlot *exec_cypher_merge(CustomScanState *node);
+static void end_cypher_merge(CustomScanState *node);
+static void rescan_cypher_merge(CustomScanState *node);
+static Datum merge_vertex(cypher_merge_custom_scan_state *css,
+                          cypher_target_node *node, ListCell *next);
+static void merge_edge(cypher_merge_custom_scan_state *css,
+                       cypher_target_node *node, Datum prev_vertex_id,
+                       ListCell *next);
+static void process_simple_merge(CustomScanState *node);
+static bool check_path(cypher_merge_custom_scan_state *css,
+                       TupleTableSlot *slot);
+static void process_path(cypher_merge_custom_scan_state *css);
+static void mark_tts_isnull(TupleTableSlot *slot);
+
+const CustomExecMethods cypher_merge_exec_methods = {MERGE_SCAN_STATE_NAME,
+                                                     begin_cypher_merge,
+                                                     exec_cypher_merge,
+                                                     end_cypher_merge,
+                                                     rescan_cypher_merge,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL};
+
+/*
+ * Initializes the MERGE Execution Node at the begginning of the execution
+ * phase.
+ */
+static void begin_cypher_merge(CustomScanState *node, EState *estate,
+                               int eflags)
+{
+    cypher_merge_custom_scan_state *css =
+        (cypher_merge_custom_scan_state *)node;
+    ListCell *lc;
+    Plan *subplan;
+
+    Assert(list_length(css->cs->custom_plans) == 1);
+
+    // initialize the subplan
+    subplan = linitial(css->cs->custom_plans);
+    node->ss.ps.lefttree = ExecInitNode(subplan, estate, eflags);
+
+    ExecAssignExprContext(estate, &node->ss.ps);
+
+    ExecInitScanTupleSlot(estate, &node->ss,
+                          ExecGetResultType(node->ss.ps.lefttree));
+
+    /*
+     * When MERGE is not the last clause in a cypher query. Setup projection
+     * information to pass to the parent execution node.
+     */
+    if (!CYPHER_CLAUSE_IS_TERMINAL(css->flags))
+    {
+        TupleDesc tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
+
+        ExecAssignProjectionInfo(&node->ss.ps, tupdesc);
+    }
+
+    /*
+     * For each vertex and edge in the path, setup the information
+     * needed if we need to create them.
+     */
+    foreach(lc, css->path->target_nodes)
+    {
+        cypher_target_node *cypher_node =
+            (cypher_target_node *)lfirst(lc);
+        Relation rel;
+
+        /*
+         * This entity is refrences an entity that is already declared. Either
+         * by a previous clause or an entity earlier in the MERGE path. In both
+         * cases, this target_entry will not create data, only reference data
+         * that already exists.
+         */
+        if (!CYPHER_TARGET_NODE_INSERT_ENTITY(cypher_node->flags))
+        {
+            continue;
+        }
+
+        // Open relation and aquire a row exclusive lock.
+        rel = heap_open(cypher_node->relid, RowExclusiveLock);
+
+        // Initialize resultRelInfo for the vertex
+        cypher_node->resultRelInfo = makeNode(ResultRelInfo);
+        InitResultRelInfo(cypher_node->resultRelInfo, rel,
+                          list_length(estate->es_range_table), NULL,
+                          estate->es_instrument);
+
+        // Open all indexes for the relation
+        ExecOpenIndices(cypher_node->resultRelInfo, false);
+
+        // Setup the relation's tuple slot
+        cypher_node->elemTupleSlot = ExecInitExtraTupleSlot(
+            estate,
+            RelationGetDescr(cypher_node->resultRelInfo->ri_RelationDesc));
+
+        if (cypher_node->id_expr != NULL)
+        {
+            cypher_node->id_expr_state =
+                ExecInitExpr(cypher_node->id_expr, (PlanState *)node);
+        }
+
+        if (cypher_node->prop_expr != NULL)
+        {
+            cypher_node->prop_expr_state =
+                ExecInitExpr(cypher_node->prop_expr, (PlanState *)node);
+        }
+
+
+    }
+
+    /*
+     * Postgres does not assign the es_output_cid in queries that do
+     * not write to disk, ie: SELECT commands. We need the command id
+     * for our clauses, and we may need to initialize it. We cannot use
+     * GetCurrentCommandId because there may be other cypher clauses
+     * that have modified the command id.
+     */
+    if (estate->es_output_cid == 0)
+        estate->es_output_cid = estate->es_snapshot->curcid;
+
+    Increment_Estate_CommandId(estate);
+}
+
+/*
+ * Checks the subtree to see if the lateral join representing the MERGE path
+ * found results. Returns true if the path does not exist and must be created,
+ * false otherwise.
+ */
+static bool check_path(cypher_merge_custom_scan_state *css,
+                       TupleTableSlot *slot)
+{
+    cypher_create_path *path = css->path;
+    ListCell *lc;
+
+    foreach(lc, path->target_nodes)
+    {
+        cypher_target_node *node = lfirst(lc);
+
+        /*
+         * If target_node as a valid attribute number and is a node not
+         * declared in a previous clause, check the tuple position in the
+         * slot. If the slot is null, the path was not found. The rules
+         * state that if one part of the path does not exists, the whold
+         * path must be created.
+         */
+        if (node->tuple_position != InvalidAttrNumber ||
+            ((node->flags & CYPHER_TARGET_NODE_MERGE_EXISTS) == 0))
+        {
+            /*
+             * Attribute number is 1 indexed and tts_values is 0 indexed,
+             * offset by 1.
+             */
+            if (slot->tts_isnull[node->tuple_position - 1])
+            {
+                return true;
+            }
+        }
+
+    }
+
+    return false;
+}
+
+static void process_path(cypher_merge_custom_scan_state *css)
+{
+    cypher_create_path *path = css->path;
+
+    ListCell *lc = list_head(path->target_nodes);
+
+    /*
+     * Create the first vertex. The create_vertex function will
+     * create the rest of the path, if necessary.
+     */
+    merge_vertex(css, lfirst(lc), lnext(lc));
+
+
+    /*
+     * If this path is a variable, take the list that was accumulated
+     * in the vertex/edge creation, create a path datum, and add to the
+     * scantuple slot.
+     */
+    if (path->path_attr_num != InvalidAttrNumber)
+    {
+        ExprContext *econtext = css->css.ss.ps.ps_ExprContext;
+        TupleTableSlot *scantuple = econtext->ecxt_scantuple;
+        Datum result;
+
+        result = make_path(css->path_values);
+
+        scantuple->tts_values[path->path_attr_num - 1] = result;
+        scantuple->tts_isnull[path->path_attr_num - 1] = false;
+    }
+}
+
+/*
+ * Function that handles the case where MERGE is the only clause in the query.
+ */
+static void process_simple_merge(CustomScanState *node)
+{
+    cypher_merge_custom_scan_state *css =
+        (cypher_merge_custom_scan_state *)node;
+    EState *estate = css->css.ss.ps.state;
+    TupleTableSlot *slot;
+
+    /*Process the subtree first */
+    Decrement_Estate_CommandId(estate)
+    slot = ExecProcNode(node->ss.ps.lefttree);
+    Increment_Estate_CommandId(estate)
+
+    if (TupIsNull(slot))
+    {
+        ExprContext *econtext = node->ss.ps.ps_ExprContext;
+
+        /* setup the scantuple that the process_path needs */
+        econtext->ecxt_scantuple = node->ss.ps.lefttree->ps_ResultTupleSlot;
+        econtext->ecxt_scantuple->tts_isempty = false;
+
+        process_path(css);
+    }
+}
+
+/*
+ * Iterate through the TupleTableSlot's tts_values and marks the isnull field
+ * with true.
+ */
+static void mark_tts_isnull(TupleTableSlot *slot)
+{
+    int numberOfAttributes = slot->tts_tupleDescriptor->natts;
+    int i;
+
+    for (i = 0; i < numberOfAttributes; i++)
+    {
+        Datum val;
+
+        val = slot->tts_values[i];
+
+        if (val == (Datum)NULL)
+        {
+            slot->tts_isnull[i] = true;
+        }
+    }
+}
+
+/*
+ * Function that is called mid-execution. This function will call
+ * its subtree in the execution tree, and depending on the results
+ * create the new path, and depending on the the context of the MERGE
+ * within the query pass data to the parent execution node.
+ *
+ * Returns a TupleTableSlot with the next tuple to it parent or
+ * Returns NULL when MERGE has no more tuples to emit.
+ */
+static TupleTableSlot *exec_cypher_merge(CustomScanState *node)
+{
+    cypher_merge_custom_scan_state *css =
+        (cypher_merge_custom_scan_state *)node;
+    EState *estate = css->css.ss.ps.state;
+    ExprContext *econtext = css->css.ss.ps.ps_ExprContext;
+    TupleTableSlot *slot;
+    bool terminal = CYPHER_CLAUSE_IS_TERMINAL(css->flags);
+
+    /*
+     * There are three cases that dictate the flow of the execution logic.
+     *
+     * 1. MERGE is not the first clause in the cypher query.
+     * 2. MERGE is the first clause and there are no following clauses.
+     * 3. MERGE is the first clause and there are following clauses.
+     * CYPHER_CLAUSE_FLAG_PREVIOUS_CLAUSE
+     */
+    if (CYPHER_CLAUSE_HAS_PREVIOUS_CLAUSE(css->flags))
+    {
+        /*
+         * Case 1: MERGE is not the first clause in the cypher query.
+         *
+         * For this case, we need to process all tuples give to us by the
+         * previous clause. When we receive a tuple from the previous clause:
+         * check to see if the left lateral join found the pattern already. If
+         * it did, we don't need to create the pattern. If the lateral join did
+         * not find the whole path, create the whole path.
+         *
+         * If this is a terminal clause, process all tuples. If not, pass the
+         * tuple to up the execution tree.
+         */
+        do
+        {
+            /*Process the subtree first */
+            Decrement_Estate_CommandId(estate)
+            slot = ExecProcNode(node->ss.ps.lefttree);
+            Increment_Estate_CommandId(estate)
+
+            /*
+             * We are done processing the subtree, mark as terminal
+             * so the function returns NULL.
+             */
+            if (TupIsNull(slot))
+            {
+                terminal = true;
+                break;
+            }
+
+            /* setup the scantuple that the process_path needs */
+            econtext->ecxt_scantuple =
+                node->ss.ps.lefttree->ps_ProjInfo->pi_exprContext->ecxt_scantuple;
+
+            if (check_path(css, econtext->ecxt_scantuple))
+            {
+                process_path(css);
+            }
+
+        } while (terminal);
+
+        /* if this was a terminal MERGE just return NULL */
+        if (terminal)
+        {
+            return NULL;
+        }
+
+        //return ExecProject(node->ss.ps.ps_ProjInfo);
+        econtext->ecxt_scantuple = ExecProject(node->ss.ps.lefttree->ps_ProjInfo);
+        return ExecProject(node->ss.ps.ps_ProjInfo);
+
+    }
+    else if (terminal)
+    {
+        /*
+         * Case 2: MERGE is the first clause and there are no following clauses
+         *
+         * For case 2, check to see if we found the pattern, if not create it.
+         * Return NULL in either cases, because no rows are expected.
+         */
+        process_simple_merge(node);
+
+        /*
+         * Case 2 always returns NULL the first time exec_cypher_merge is
+         * called.
+         */
+        return NULL;
+    }
+    else
+    {
+        /*
+         * Case 3: MERGE is the first clause and there are following clauses.
+         *
+         * Case three has two subcases:
+         *
+         * 1. The already path exists.
+         * 2. The path does not exist.
+         */
+
+        /*
+         * Part of Case 2.
+         *
+         * If created_new_path is marked as true. The path did not exist and
+         * MERGE created it. We have already passed that information up the
+         * execution tree, and now we tell MERGE's parents in the execution
+         * tree there is no more tuples to pass.
+         */
+        if (css->created_new_path)
+        {
+            /*
+             * If the created_new_path is set to true. Then MERGE should not
+             * have found a path, because this should only be set to true if
+             * merge found sub-case 1
+             */
+            Assert(css->found_a_path == false);
+
+            return NULL;
+        }
+
+        /*
+         * Process the subtree. The subtree will only consist of the MERGE
+         * path.
+         */
+        Decrement_Estate_CommandId(estate)
+        slot = ExecProcNode(node->ss.ps.lefttree);
+        Increment_Estate_CommandId(estate)
+
+        if (!TupIsNull(slot))
+        {
+            /*
+             * Part of Sub-Case 1.
+             *
+             * If we found a path, mark the found_a_path flag to true and
+             * pass the tuple to the next execution tree. When the path
+             * exists, we don't need to create/modify anything.
+             */
+            css->found_a_path = true;
+
+            return node->ss.ps.lefttree->ps_ResultTupleSlot;
+        }
+        else if (TupIsNull(slot) && css->found_a_path)
+        {
+            /*
+             * Part of Sub-Case 2.
+             *
+             * MERGE found the path(s) that already exists and we are done passing
+             * all the found path(s) up the execution tree.
+             */
+            return NULL;
+        }
+        else
+        {
+            /*
+             * Part of Sub-Case 1.
+             *
+             * MERGE could not find the path in memory and the sub-node in the
+             * execution tree returned NULL. We need to create the path and
+             * pass the tuple to the next execution node. The subtrees will
+             * begin its cleanup process when there are no more tuples found.
+             * So we will need to create a TupleTableSlot and populate with the
+             * information from the newly created path that the query needs.
+             */
+            ExprContext *econtext = node->ss.ps.ps_ExprContext;
+            SubqueryScanState *sss = (SubqueryScanState *)node->ss.ps.lefttree;
+            HeapTuple heap_tuple;
+
+            /*
+             * Our child execution node is always a subquery. If not there
+             * is an issue.
+             */
+            Assert(IsA(sss, SubqueryScanState));
+
+            /*
+             * found_a_path should only be set to true if MERGE is following
+             * sub-case 2.
+             */
+            Assert(css->found_a_path == false);
+
+            /*
+             * This block of sub-case 1 should only be exectuted once. To
+             * create the single path if the path does not exist. If we find
+             * ourselves here again, the internal state of the MERGE execution
+             * node was incorrectly altered.
+             */
+            Assert(css->created_new_path == false);
+
+            /*
+             *  Postgres cleared the child tuple table slot, we need to remake
+             *  it.
+             */
+            ExecInitScanTupleSlot(estate, &sss->ss,
+                                  ExecGetResultType(sss->subplan));
+
+
+            /* setup the scantuple that the process_path needs */
+            econtext->ecxt_scantuple = sss->ss.ss_ScanTupleSlot;
+
+            // create the path
+            process_path(css);
+
+            // 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.
+             */
+            mark_tts_isnull(econtext->ecxt_scantuple);
+
+            // create the physical heap tuple
+            heap_tuple = heap_form_tuple(
+                                econtext->ecxt_scantuple->tts_tupleDescriptor,
+                                econtext->ecxt_scantuple->tts_values,
+                                econtext->ecxt_scantuple->tts_isnull);
+
+            // store the heap tuble
+            ExecStoreTuple(heap_tuple, econtext->ecxt_scantuple, InvalidBuffer, false);
+
+            /*
+             * make the subquery's projection scan slot be the tuple table we
+             * created and run the projection logic.
+             */
+            sss->ss.ps.ps_ProjInfo->pi_exprContext->ecxt_scantuple =
+                                                        econtext->ecxt_scantuple;
+
+            // assign this to be our scantuple
+            econtext->ecxt_scantuple = ExecProject(node->ss.ps.lefttree->ps_ProjInfo);
+
+            /*
+             *  run the merge's projection logic and pass to its parent
+             *  execution node
+             */
+            return ExecProject(node->ss.ps.ps_ProjInfo);
+        }
+    }
+}
+
+
+/*
+ * Function called at the end of the execution phase to cleanup
+ * MERGE.
+ */
+static void end_cypher_merge(CustomScanState *node)
+{
+    cypher_merge_custom_scan_state *css =
+        (cypher_merge_custom_scan_state *)node;
+    cypher_create_path *path = css->path;
+    ListCell *lc;
+
+    // increment the command counter
+    CommandCounterIncrement();
+
+    ExecEndNode(node->ss.ps.lefttree);
+
+    foreach (lc, path->target_nodes)
+    {
+        cypher_target_node *cypher_node =
+            (cypher_target_node *)lfirst(lc);
+
+        if (!CYPHER_TARGET_NODE_INSERT_ENTITY(cypher_node->flags))
+            continue;
+
+        // close all indices for the node
+        ExecCloseIndices(cypher_node->resultRelInfo);
+
+        // close the relation itself
+        heap_close(cypher_node->resultRelInfo->ri_RelationDesc,
+                   RowExclusiveLock);
+    }
+}
+
+/*
+ * Rescan is mostly used by join execution nodes, and several others.
+ * Since we are creating data here its not safe to rescan the node. Throw
+ * an error and try to help the uer understand what went wrong.
+ */
+static void rescan_cypher_merge(CustomScanState *node)
+{
+    ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                    errmsg("cypher merge clause cannot be rescaned"),
+                    errhint("its unsafe to use joins in a query with a Cypher MERGE clause")));
+}
+
+/*
+ * Extracts the metadata information that MERGE needs from the
+ * merge_custom_scan node and creates the cypher_merge_custom_scan_state
+ * for the execution phase.
+ */
+Node *create_cypher_merge_plan_state(CustomScan *cscan)
+{
+    cypher_merge_custom_scan_state *cypher_css =
+        palloc0(sizeof(cypher_merge_custom_scan_state));
+    cypher_merge_information *merge_information;
+    char *serialized_data;
+    Const *c;
+
+    cypher_css->cs = cscan;
+
+    // get the serialized data structure from the Const and deserialize it.
+    c = linitial(cscan->custom_private);
+    serialized_data = (char *)c->constvalue;
+    merge_information = stringToNode(serialized_data);
+
+    Assert(is_ag_node(merge_information, cypher_merge_information));
+
+    cypher_css->merge_information = merge_information;
+    cypher_css->flags = merge_information->flags;
+    cypher_css->merge_function_attr = merge_information->merge_function_attr;
+    cypher_css->path = merge_information->path;
+    cypher_css->created_new_path = false;
+    cypher_css->found_a_path = false;
+    cypher_css->graph_oid = merge_information->graph_oid;
+
+    cypher_css->css.ss.ps.type = T_CustomScanState;
+    cypher_css->css.methods = &cypher_merge_exec_methods;
+
+    return (Node *)cypher_css;
+}
+
+/*
+ * Creates the vertex entity, returns the vertex's id in case the caller is
+ * the create_edge function.
+ */
+static Datum merge_vertex(cypher_merge_custom_scan_state *css,
+                          cypher_target_node *node, ListCell *next)
+{
+    bool isNull;
+    Datum id;
+    EState *estate = css->css.ss.ps.state;
+    ExprContext *econtext = css->css.ss.ps.ps_ExprContext;
+    ResultRelInfo *resultRelInfo = node->resultRelInfo;
+    TupleTableSlot *elemTupleSlot = node->elemTupleSlot;
+    TupleTableSlot *scanTupleSlot = econtext->ecxt_scantuple;
+
+    Assert(node->type == LABEL_KIND_VERTEX);
+
+    /*
+     * Vertices in a path might already exists. If they do get the id
+     * to pass to the edges before and after it. Otherwise, insert the
+     * new vertex into it's table and then pass the id along.
+     */
+    if (CYPHER_TARGET_NODE_INSERT_ENTITY(node->flags))
+    {
+        ResultRelInfo *old_estate_es_result_relation_info = NULL;
+        Datum prop;
+        /*
+         * Set estate's result relation to the vertex's result
+         * relation.
+         *
+         * Note: This obliterates what was their previously
+         */
+
+        /* save the old result relation info */
+        old_estate_es_result_relation_info = estate->es_result_relation_info;
+
+        estate->es_result_relation_info = resultRelInfo;
+
+        ExecClearTuple(elemTupleSlot);
+
+        // get the next graphid for this vertex.
+        id = ExecEvalExpr(node->id_expr_state, econtext, &isNull);
+        elemTupleSlot->tts_values[vertex_tuple_id] = id;
+        elemTupleSlot->tts_isnull[vertex_tuple_id] = isNull;
+
+        // get the properties for this vertex
+        prop = ExecEvalExpr(node->prop_expr_state, econtext, &isNull);
+        elemTupleSlot->tts_values[vertex_tuple_properties] = prop;
+        elemTupleSlot->tts_isnull[vertex_tuple_properties] = isNull;
+
+        // Insert the new vertex
+        insert_entity_tuple(resultRelInfo, elemTupleSlot, estate);
+
+        /* restore the old result relation info */
+        estate->es_result_relation_info = old_estate_es_result_relation_info;
+
+        /*
+         * When the vertex is used by clauses higher in the execution tree
+         * we need to create a vertex datum. When the vertex is a variable,
+         * add to the scantuple slot. When the vertex is part of a path
+         * variable, add to the list.
+         */
+        if (CYPHER_TARGET_NODE_OUTPUT(node->flags))
+        {
+            Datum result;
+
+            // make the vertex agtype
+            result = make_vertex(
+                id, CStringGetDatum(node->label_name), prop);
+
+            // append to the path list
+            if (CYPHER_TARGET_NODE_IN_PATH(node->flags))
+            {
+                css->path_values = lappend(css->path_values,
+                                           DatumGetPointer(result));
+            }
+
+            /*
+             * Put the vertex in the correct spot in the scantuple, so parent
+             * execution nodes can reference the newly created variable.
+             */
+            if (CYPHER_TARGET_NODE_IS_VARIABLE(node->flags))
+            {
+                scanTupleSlot->tts_values[node->tuple_position - 1] = result;
+                scanTupleSlot->tts_isnull[node->tuple_position - 1] = false;
+            }
+        }
+    }
+    else
+    {
+        agtype *a;
+        Datum d;
+        agtype_value *v;
+        agtype_value *id_value;
+        TupleTableSlot *scantuple;
+        PlanState *ps;
+
+        ps = css->css.ss.ps.lefttree;
+        scantuple = ps->ps_ExprContext->ecxt_scantuple;
+
+        if (scantuple->tts_isnull[node->tuple_position - 1])
+        {
+            ereport(ERROR,
+                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                 errmsg("Existing variable %s cannot be NULL in MERGE clause",
+                 node->variable_name)));
+        }
+
+        // get the vertex agtype in the scanTupleSlot
+        d = scantuple->tts_values[node->tuple_position - 1];
+        a = DATUM_GET_AGTYPE_P(d);
+
+        // Convert to an agtype value
+        v = get_ith_agtype_value_from_container(&a->root, 0);
+
+        if (v->type != AGTV_VERTEX)
+            ereport(ERROR,
+                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                     errmsg("agtype must resolve to a vertex")));
+
+        // extract the id agtype field
+        id_value = get_agtype_value_object_value(v, "id");
+
+        // extract the graphid and cast to a Datum
+        id = GRAPHID_GET_DATUM(id_value->val.int_value);
+
+        /*
+         * Its possible the variable has already been deleted. There are two
+         * ways this can happen. One is the query explicitly deleted the
+         * variable, the is_deleted flag will catch that. However, it is
+         * possible the user deleted the vertex using another variable name. We
+         * need to scan the table to find the vertex's current status relative
+         * to this CREATE clause. If the variable was initially created in this
+         * clause, we can skip this check, because the transaction system
+         * guarantees that nothing can happen to that tuple, as far as we are
+         * concerned with at this time.
+         */
+        if (!SAFE_TO_SKIP_EXISTENCE_CHECK(node->flags))
+        {
+            if (!entity_exists(estate, css->graph_oid, DATUM_GET_GRAPHID(id)))
+            {
+                ereport(ERROR,
+                    (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                     errmsg("vertex assigned to variable %s was deleted",
+                            node->variable_name)));
+            }
+        }
+
+        // add the Datum to the list of entities for creating the path variable
+        if (CYPHER_TARGET_NODE_IN_PATH(node->flags))
+        {
+            Datum vertex = scanTupleSlot->tts_values[node->tuple_position - 1];
+            css->path_values = lappend(css->path_values,
+                                       DatumGetPointer(vertex));
+        }
+    }
+
+    // If the path continues, create the next edge, passing the vertex's id.
+    if (next != NULL)
+    {
+        merge_edge(css, lfirst(next), id, lnext(next));
+    }
+
+    return id;
+}
+
+/*
+ * Create the edge entity.
+ */
+static void merge_edge(cypher_merge_custom_scan_state *css,
+                       cypher_target_node *node, Datum prev_vertex_id,
+                       ListCell *next)
+{
+    bool isNull;
+    EState *estate = css->css.ss.ps.state;
+    ExprContext *econtext = css->css.ss.ps.ps_ExprContext;
+    ResultRelInfo *resultRelInfo = node->resultRelInfo;
+    ResultRelInfo *old_estate_es_result_relation_info = NULL;
+    TupleTableSlot *elemTupleSlot = node->elemTupleSlot;
+    Datum id;
+    Datum start_id, end_id, next_vertex_id;
+    List *prev_path = css->path_values;
+    Datum prop;
+
+    Assert(node->type == LABEL_KIND_EDGE);
+    Assert(lfirst(next) != NULL);
+
+    /*
+     * Create the next vertex before creating the edge. We need the
+     * next vertex's id.
+     */
+    css->path_values = NIL;
+    next_vertex_id = merge_vertex(css, lfirst(next), lnext(next));
+
+    /*
+     * Set the start and end vertex ids
+     */
+    if (node->dir == CYPHER_REL_DIR_RIGHT || node->dir == CYPHER_REL_DIR_NONE)
+    {
+        // create pattern (prev_vertex)-[edge]->(next_vertex)
+        start_id = prev_vertex_id;
+        end_id = next_vertex_id;
+    }
+    else if (node->dir == CYPHER_REL_DIR_LEFT)
+    {
+        // create pattern (prev_vertex)<-[edge]-(next_vertex)
+        start_id = next_vertex_id;
+        end_id = prev_vertex_id;
+    }
+    else
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                 errmsg("edge direction must be specified in a MERGE clause")));
+    }
+
+    /*
+     * Set estate's result relation to the vertex's result
+     * relation.
+     *
+     * Note: This obliterates what was their previously
+     */
+
+    /* save the old result relation info */
+    old_estate_es_result_relation_info = estate->es_result_relation_info;
+
+    estate->es_result_relation_info = resultRelInfo;
+
+    ExecClearTuple(elemTupleSlot);
+
+    // Graph Id for the edge
+    id = ExecEvalExpr(node->id_expr_state, econtext, &isNull);
+    elemTupleSlot->tts_values[edge_tuple_id] = id;
+    elemTupleSlot->tts_isnull[edge_tuple_id] = isNull;
+
+    // Graph id for the starting vertex
+    elemTupleSlot->tts_values[edge_tuple_start_id] = start_id;
+    elemTupleSlot->tts_isnull[edge_tuple_start_id] = false;
+
+    // Graph id for the ending vertex
+    elemTupleSlot->tts_values[edge_tuple_end_id] = end_id;
+    elemTupleSlot->tts_isnull[edge_tuple_end_id] = false;
+
+    // Edge's properties map
+    prop = ExecEvalExpr(node->prop_expr_state, econtext, &isNull);
+    elemTupleSlot->tts_values[edge_tuple_properties] = prop;
+    elemTupleSlot->tts_isnull[edge_tuple_properties] = isNull;
+
+    // Insert the new edge
+    insert_entity_tuple(resultRelInfo, elemTupleSlot, estate);
+
+    /* restore the old result relation info */
+    estate->es_result_relation_info = old_estate_es_result_relation_info;
+
+    /*
+     * When the edge is used by clauses higher in the execution tree
+     * we need to create an edge datum. When the edge is a variable,
+     * add to the scantuple slot. When the edge is part of a path
+     * variable, add to the list.
+     */
+    if (CYPHER_TARGET_NODE_OUTPUT(node->flags))
+    {
+        Datum result;
+
+        result = make_edge(
+            id, start_id, end_id, CStringGetDatum(node->label_name), prop);
+
+        // add the Datum to the list of entities for creating the path variable
+        if (CYPHER_TARGET_NODE_IN_PATH(node->flags))
+        {
+            prev_path = lappend(prev_path, DatumGetPointer(result));
+            css->path_values = list_concat(prev_path, css->path_values);
+        }
+
+        // Add the entity to the TupleTableSlot if necessary
+        if (CYPHER_TARGET_NODE_IS_VARIABLE(node->flags))
+        {
+            TupleTableSlot *scantuple = econtext->ecxt_scantuple;
+
+            scantuple->tts_values[node->tuple_position - 1] = result;
+            scantuple->tts_isnull[node->tuple_position - 1] = false;
+        }
+    }
+}
diff --git a/src/backend/executor/cypher_utils.c b/src/backend/executor/cypher_utils.c
index c9e0506..ec086fd 100644
--- a/src/backend/executor/cypher_utils.c
+++ b/src/backend/executor/cypher_utils.c
@@ -43,6 +43,7 @@
 #include "executor/cypher_executor.h"
 #include "executor/cypher_utils.h"
 #include "utils/agtype.h"
+#include "utils/ag_cache.h"
 #include "utils/graphid.h"
 
 ResultRelInfo *create_entity_result_rel_info(EState *estate, char *graph_name, char *label_name)
@@ -81,8 +82,10 @@ TupleTableSlot *populate_vertex_tts(
     bool properties_isnull;
 
     if (id == NULL)
+    {
         ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                         errmsg("vertex id field cannot be NULL")));
+    }
 
     properties_isnull = properties == NULL;
 
@@ -103,16 +106,21 @@ TupleTableSlot *populate_edge_tts(
     bool properties_isnull;
 
     if (id == NULL)
+    {
         ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                         errmsg("edge id field cannot be NULL")));
+    }
     if (startid == NULL)
+    {
         ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                         errmsg("edge start_id field cannot be NULL")));
+    }
 
     if (endid == NULL)
+    {
         ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                         errmsg("edge end_id field cannot be NULL")));
-
+    }
 
     properties_isnull = properties == NULL;
 
@@ -134,3 +142,81 @@ TupleTableSlot *populate_edge_tts(
 
     return elemTupleSlot;
 }
+
+
+/*
+ * Find out if the entity still exists. This is for 'implicit' deletion
+ * of an entity.
+ */
+bool entity_exists(EState *estate, Oid graph_oid, graphid id)
+{
+    label_cache_data *label;
+    ScanKeyData scan_keys[1];
+    HeapScanDesc scan_desc;
+    HeapTuple tuple;
+    Relation rel;
+    bool result = true;
+
+    /*
+     * Extract the label id from the graph id and get the table name
+     * the entity is part of.
+     */
+    label = search_label_graph_id_cache(graph_oid, GET_LABEL_ID(id));
+
+    // Setup the scan key to be the graphid
+    ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber,
+                F_GRAPHIDEQ, GRAPHID_GET_DATUM(id));
+
+    rel = heap_open(label->relation, RowExclusiveLock);
+    scan_desc = heap_beginscan(rel, estate->es_snapshot, 1, scan_keys);
+
+    tuple = heap_getnext(scan_desc, ForwardScanDirection);
+
+    /*
+     * If a single tuple was returned, the tuple is still valid, otherwise'
+     * set to false.
+     */
+    if (!HeapTupleIsValid(tuple))
+    {
+        result = false;
+    }
+
+    heap_endscan(scan_desc);
+    heap_close(rel, RowExclusiveLock);
+
+    return result;
+}
+
+/*
+ * Insert the edge/vertex tuple into the table and indices. If the table's
+ * constraints have not been violated.
+ */
+HeapTuple insert_entity_tuple(ResultRelInfo *resultRelInfo,
+                              TupleTableSlot *elemTupleSlot,
+                              EState *estate)
+{
+    HeapTuple tuple;
+
+    ExecStoreVirtualTuple(elemTupleSlot);
+    tuple = ExecMaterializeSlot(elemTupleSlot);
+
+    // Check the constraints of the tuple
+    tuple->t_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
+    if (resultRelInfo->ri_RelationDesc->rd_att->constr != NULL)
+    {
+        ExecConstraints(resultRelInfo, elemTupleSlot, estate);
+    }
+
+    // Insert the tuple normally
+    heap_insert(resultRelInfo->ri_RelationDesc, tuple,
+                GetCurrentCommandId(true), 0, NULL);
+
+    // Insert index entries for the tuple
+    if (resultRelInfo->ri_NumIndices > 0)
+    {
+        ExecInsertIndexTuples(elemTupleSlot, &(tuple->t_self), estate, false,
+                              NULL, NIL);
+    }
+
+    return tuple;
+}
diff --git a/src/backend/nodes/ag_nodes.c b/src/backend/nodes/ag_nodes.c
index d3fbb1b..95d2b2d 100644
--- a/src/backend/nodes/ag_nodes.c
+++ b/src/backend/nodes/ag_nodes.c
@@ -41,6 +41,7 @@ const char *node_names[] = {
     "cypher_delete",
     "cypher_union",
     "cypher_unwind",
+    "cypher_merge",
     "cypher_path",
     "cypher_node",
     "cypher_relationship",
@@ -58,7 +59,8 @@ const char *node_names[] = {
     "cypher_update_information",
     "cypher_update_item",
     "cypher_delete_information",
-    "cypher_delete_item"
+    "cypher_delete_item",
+    "cypher_merge_information"
 };
 
 /*
@@ -102,6 +104,7 @@ const ExtensibleNodeMethods node_methods[] = {
     DEFINE_NODE_METHODS(cypher_delete),
     DEFINE_NODE_METHODS(cypher_union),
     DEFINE_NODE_METHODS(cypher_unwind),
+    DEFINE_NODE_METHODS(cypher_merge),
     DEFINE_NODE_METHODS(cypher_path),
     DEFINE_NODE_METHODS(cypher_node),
     DEFINE_NODE_METHODS(cypher_relationship),
@@ -119,7 +122,8 @@ const ExtensibleNodeMethods node_methods[] = {
     DEFINE_NODE_METHODS_EXTENDED(cypher_update_information),
     DEFINE_NODE_METHODS_EXTENDED(cypher_update_item),
     DEFINE_NODE_METHODS_EXTENDED(cypher_delete_information),
-    DEFINE_NODE_METHODS_EXTENDED(cypher_delete_item)
+    DEFINE_NODE_METHODS_EXTENDED(cypher_delete_item),
+    DEFINE_NODE_METHODS_EXTENDED(cypher_merge_information)
 };
 
 static bool equal_ag_node(const ExtensibleNode *a, const ExtensibleNode *b)
diff --git a/src/backend/nodes/cypher_copyfuncs.c b/src/backend/nodes/cypher_copyfuncs.c
index e1546e9..242ccfa 100644
--- a/src/backend/nodes/cypher_copyfuncs.c
+++ b/src/backend/nodes/cypher_copyfuncs.c
@@ -88,7 +88,7 @@ void copy_cypher_create_path(ExtensibleNode *newnode, const ExtensibleNode *from
     COPY_LOCALS(cypher_create_path);
 
     COPY_SCALAR_FIELD(path_attr_num);
-
+    COPY_STRING_FIELD(var_name);
     COPY_NODE_FIELD(target_nodes);
 }
 
@@ -109,6 +109,8 @@ void copy_cypher_target_node(ExtensibleNode *newnode, const ExtensibleNode *from
 
     COPY_NODE_FIELD(id_expr);
     COPY_NODE_FIELD(id_expr_state);
+    COPY_NODE_FIELD(prop_expr);
+    COPY_NODE_FIELD(prop_expr_state);
     COPY_NODE_FIELD(resultRelInfo);
     COPY_NODE_FIELD(elemTupleSlot);
 }
@@ -158,3 +160,14 @@ void copy_cypher_delete_item(ExtensibleNode *newnode, const ExtensibleNode *from
     COPY_NODE_FIELD(entity_position);
     COPY_STRING_FIELD(var_name);
 }
+
+// copy function for cypher_merge_information
+void copy_cypher_merge_information(ExtensibleNode *newnode, const ExtensibleNode *from)
+{
+    COPY_LOCALS(cypher_merge_information);
+
+    COPY_SCALAR_FIELD(flags);
+    COPY_SCALAR_FIELD(graph_oid);
+    COPY_SCALAR_FIELD(merge_function_attr);
+    COPY_NODE_FIELD(path);
+}
diff --git a/src/backend/nodes/cypher_outfuncs.c b/src/backend/nodes/cypher_outfuncs.c
index a1b3913..b97a247 100644
--- a/src/backend/nodes/cypher_outfuncs.c
+++ b/src/backend/nodes/cypher_outfuncs.c
@@ -175,6 +175,14 @@ void out_cypher_unwind(StringInfo str, const ExtensibleNode *node)
     WRITE_NODE_FIELD(target);
 }
 
+// serialization function for the cypher_delete ExtensibleNode.
+void out_cypher_merge(StringInfo str, const ExtensibleNode *node)
+{
+    DEFINE_AG_NODE(cypher_merge);
+
+    WRITE_NODE_FIELD(path);
+}
+
 // serialization function for the cypher_path ExtensibleNode.
 void out_cypher_path(StringInfo str, const ExtensibleNode *node)
 {
@@ -300,6 +308,7 @@ void out_cypher_create_path(StringInfo str, const ExtensibleNode *node)
 
     WRITE_NODE_FIELD(target_nodes);
     WRITE_INT32_FIELD(path_attr_num);
+    WRITE_STRING_FIELD(var_name);
 }
 
 // serialization function for the cypher_target_node ExtensibleNode.
@@ -312,6 +321,8 @@ void out_cypher_target_node(StringInfo str, const ExtensibleNode *node)
     WRITE_ENUM_FIELD(dir, cypher_rel_dir);
     WRITE_NODE_FIELD(id_expr);
     WRITE_NODE_FIELD(id_expr_state);
+    WRITE_NODE_FIELD(prop_expr);
+    WRITE_NODE_FIELD(prop_expr_state);
     WRITE_INT32_FIELD(prop_attr_num);
     WRITE_NODE_FIELD(resultRelInfo);
     WRITE_NODE_FIELD(elemTupleSlot);
@@ -377,6 +388,17 @@ void out_cypher_union(StringInfo str, const ExtensibleNode *node)
     WRITE_NODE_FIELD(rarg);
 }
 
+// serialization function for the cypher_merge_information ExtensibleNode.
+void out_cypher_merge_information(StringInfo str, const ExtensibleNode *node)
+{
+    DEFINE_AG_NODE(cypher_merge_information);
+
+    WRITE_INT32_FIELD(flags);
+    WRITE_OID_FIELD(graph_oid);
+    WRITE_INT32_FIELD(merge_function_attr);
+    WRITE_NODE_FIELD(path);
+}
+
 /*
  * Copied from Postgres
  *
diff --git a/src/backend/nodes/cypher_readfuncs.c b/src/backend/nodes/cypher_readfuncs.c
index 76fe63b..3d3249d 100644
--- a/src/backend/nodes/cypher_readfuncs.c
+++ b/src/backend/nodes/cypher_readfuncs.c
@@ -192,6 +192,7 @@ void read_cypher_create_path(struct ExtensibleNode *node)
 
     READ_NODE_FIELD(target_nodes);
     READ_INT_FIELD(path_attr_num);
+    READ_STRING_FIELD(var_name);
 }
 
 /*
@@ -207,6 +208,8 @@ void read_cypher_target_node(struct ExtensibleNode *node)
     READ_ENUM_FIELD(dir, cypher_rel_dir);
     READ_NODE_FIELD(id_expr);
     READ_NODE_FIELD(id_expr_state);
+    READ_NODE_FIELD(prop_expr);
+    READ_NODE_FIELD(prop_expr_state);
     READ_INT_FIELD(prop_attr_num);
     READ_NODE_FIELD(resultRelInfo);
     READ_NODE_FIELD(elemTupleSlot);
@@ -273,3 +276,17 @@ void read_cypher_delete_item(struct ExtensibleNode *node)
     READ_NODE_FIELD(entity_position);
     READ_STRING_FIELD(var_name);
 }
+
+/*
+ * Deserialize a string representing the cypher_merge_information
+ * data structure.
+ */
+void read_cypher_merge_information(struct ExtensibleNode *node)
+{
+    READ_LOCALS(cypher_merge_information);
+
+    READ_INT_FIELD(flags);
+    READ_OID_FIELD(graph_oid);
+    READ_INT_FIELD(merge_function_attr);
+    READ_NODE_FIELD(path);
+}
diff --git a/src/backend/optimizer/cypher_createplan.c b/src/backend/optimizer/cypher_createplan.c
index d11d9f3..9e08634 100644
--- a/src/backend/optimizer/cypher_createplan.c
+++ b/src/backend/optimizer/cypher_createplan.c
@@ -36,6 +36,8 @@ const CustomScanMethods cypher_set_plan_methods = {
     "Cypher Set", create_cypher_set_plan_state};
 const CustomScanMethods cypher_delete_plan_methods = {
     "Cypher Delete", create_cypher_delete_plan_state};
+const CustomScanMethods cypher_merge_plan_methods = {
+    "Cypher Merge", create_cypher_merge_plan_state};
 
 Plan *plan_cypher_create_path(PlannerInfo *root, RelOptInfo *rel,
                               CustomPath *best_path, List *tlist,
@@ -183,3 +185,63 @@ Plan *plan_cypher_delete_path(PlannerInfo *root, RelOptInfo *rel,
     return (Plan *)cs;
 }
 
+/*
+ * Coverts the Scan node representing the delete clause
+ * to the merge Plan node
+ */
+Plan *plan_cypher_merge_path(PlannerInfo *root, RelOptInfo *rel,
+                           CustomPath *best_path, List *tlist,
+                           List *clauses, List *custom_plans)
+{
+    CustomScan *cs;
+    Plan *subplan = linitial(custom_plans);
+
+    cs = makeNode(CustomScan);
+
+    cs->scan.plan.startup_cost = best_path->path.startup_cost;
+    cs->scan.plan.total_cost = best_path->path.total_cost;
+
+    cs->scan.plan.plan_rows = best_path->path.rows;
+    cs->scan.plan.plan_width = 0;
+
+    cs->scan.plan.parallel_aware = best_path->path.parallel_aware;
+    cs->scan.plan.parallel_safe = best_path->path.parallel_safe;
+
+    cs->scan.plan.plan_node_id = 0; // Set later in set_plan_refs
+    /*
+     * the scan list of the delete node, used for its ScanTupleSlot used
+     * by its parent in the execution phase.
+     */
+    cs->scan.plan.targetlist = tlist;
+    cs->scan.plan.qual = NIL;
+    cs->scan.plan.lefttree = NULL;
+    cs->scan.plan.righttree = NULL;
+    cs->scan.plan.initPlan = NIL;
+
+    cs->scan.plan.extParam = NULL;
+    cs->scan.plan.allParam = NULL;
+
+    /*
+     * We do not want Postgres to assume we are scanning a table, postgres'
+     * optimizer will make assumptions about our targetlist that are false
+     */
+    cs->scan.scanrelid = 0;
+
+    cs->flags = best_path->flags;
+
+    // child plan nodes are here, Postgres processed them for us.
+    cs->custom_plans = custom_plans;
+    cs->custom_exprs = NIL;
+    // transfer delete metadata needed by the delete clause.
+    cs->custom_private = best_path->custom_private;
+    /*
+     * the scan list of the merge node's children, used for ScanTupleSlot
+     * in execution.
+     */
+    cs->custom_scan_tlist = subplan->targetlist;
+
+    cs->custom_relids = NULL;
+    cs->methods = &cypher_merge_plan_methods;
+
+    return (Plan *)cs;
+}
diff --git a/src/backend/optimizer/cypher_pathnode.c b/src/backend/optimizer/cypher_pathnode.c
index fce17a0..4e04b75 100644
--- a/src/backend/optimizer/cypher_pathnode.c
+++ b/src/backend/optimizer/cypher_pathnode.c
@@ -33,7 +33,8 @@ const CustomPathMethods cypher_set_path_methods = {
     SET_PATH_NAME, plan_cypher_set_path, NULL};
 const CustomPathMethods cypher_delete_path_methods = {
     DELETE_PATH_NAME, plan_cypher_delete_path, NULL};
-
+const CustomPathMethods cypher_merge_path_methods = {
+    MERGE_PATH_NAME, plan_cypher_merge_path, NULL};
 
 CustomPath *create_cypher_create_path(PlannerInfo *root, RelOptInfo *rel,
                                       List *custom_private)
@@ -150,3 +151,45 @@ CustomPath *create_cypher_delete_path(PlannerInfo *root, RelOptInfo *rel,
     return cp;
 }
 
+/*
+ * Creates a Delete Path. Makes the original path a child of the new
+ * path. We leave it to the caller to replace the pathlist of the rel.
+ */
+CustomPath *create_cypher_merge_path(PlannerInfo *root, RelOptInfo *rel,
+                                   List *custom_private)
+{
+    CustomPath *cp;
+
+    cp = makeNode(CustomPath);
+
+    cp->path.pathtype = T_CustomScan;
+
+    cp->path.parent = rel;
+    cp->path.pathtarget = rel->reltarget;
+
+    cp->path.param_info = NULL;
+
+    // Do not allow parallel methods
+    cp->path.parallel_aware = false;
+    cp->path.parallel_safe = false;
+    cp->path.parallel_workers = 0;
+
+    cp->path.rows = 0;
+    cp->path.startup_cost = 0;
+    cp->path.total_cost = 0;
+
+    // No output ordering for basic SET
+    cp->path.pathkeys = NULL;
+
+    // Disable all custom flags for now
+    cp->flags = 0;
+
+    // Make the original paths the children of the new path
+    cp->custom_paths = rel->pathlist;
+    // Store the metadata Delete will need in the execution phase.
+    cp->custom_private = custom_private;
+    // Tells Postgres how to turn this path to the correct CustomScan
+    cp->methods = &cypher_merge_path_methods;
+
+    return cp;
+}
diff --git a/src/backend/optimizer/cypher_paths.c b/src/backend/optimizer/cypher_paths.c
index ac353ec..b1ac693 100644
--- a/src/backend/optimizer/cypher_paths.c
+++ b/src/backend/optimizer/cypher_paths.c
@@ -36,7 +36,8 @@ typedef enum cypher_clause_kind
     CYPHER_CLAUSE_NONE,
     CYPHER_CLAUSE_CREATE,
     CYPHER_CLAUSE_SET,
-    CYPHER_CLAUSE_DELETE
+    CYPHER_CLAUSE_DELETE,
+    CYPHER_CLAUSE_MERGE
 } cypher_clause_kind;
 
 static set_rel_pathlist_hook_type prev_set_rel_pathlist_hook;
@@ -50,6 +51,8 @@ static void handle_cypher_set_clause(PlannerInfo *root, RelOptInfo *rel,
                                      Index rti, RangeTblEntry *rte);
 static void handle_cypher_delete_clause(PlannerInfo *root, RelOptInfo *rel,
                                         Index rti, RangeTblEntry *rte);
+static void handle_cypher_merge_clause(PlannerInfo *root, RelOptInfo *rel,
+                                        Index rti, RangeTblEntry *rte);
 
 void set_rel_pathlist_init(void)
 {
@@ -79,6 +82,9 @@ static void set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti,
     case CYPHER_CLAUSE_DELETE:
         handle_cypher_delete_clause(root, rel, rti, rte);
         break;
+    case CYPHER_CLAUSE_MERGE:
+        handle_cypher_merge_clause(root, rel, rti, rte);
+        break;
     case CYPHER_CLAUSE_NONE:
         break;
     default:
@@ -119,6 +125,8 @@ static cypher_clause_kind get_cypher_clause_kind(RangeTblEntry *rte)
         return CYPHER_CLAUSE_SET;
     if (is_oid_ag_func(fe->funcid, DELETE_CLAUSE_FUNCTION_NAME))
         return CYPHER_CLAUSE_DELETE;
+    if (is_oid_ag_func(fe->funcid, MERGE_CLAUSE_FUNCTION_NAME))
+        return CYPHER_CLAUSE_MERGE;
     else
         return CYPHER_CLAUSE_NONE;
 }
@@ -199,3 +207,27 @@ static void handle_cypher_set_clause(PlannerInfo *root, RelOptInfo *rel,
 
     add_path(rel, (Path *)cp);
 }
+
+// replace all possible paths with our CustomPath
+static void handle_cypher_merge_clause(PlannerInfo *root, RelOptInfo *rel,
+                                        Index rti, RangeTblEntry *rte)
+{
+    TargetEntry *te;
+    FuncExpr *fe;
+    List *custom_private;
+    CustomPath *cp;
+
+    // Add the pattern to the CustomPath
+    te = (TargetEntry *)llast(rte->subquery->targetList);
+    fe = (FuncExpr *)te->expr;
+    // pass the const that holds the data structure to the path.
+    custom_private = fe->args;
+
+    cp = create_cypher_merge_path(root, rel, custom_private);
+
+    // Discard any pre-existing paths
+    rel->pathlist = NIL;
+    rel->partial_pathlist = NIL;
+
+    add_path(rel, (Path *)cp);
+}
diff --git a/src/backend/parser/cypher_analyze.c b/src/backend/parser/cypher_analyze.c
index 4bc9ea7..15222ff 100644
--- a/src/backend/parser/cypher_analyze.c
+++ b/src/backend/parser/cypher_analyze.c
@@ -402,8 +402,8 @@ static void convert_cypher_to_subquery(RangeTblEntry *rte, ParseState *pstate)
      * coercion logic applied to them because we are forcing the column
      * definition list to be a particular way in this case.
      */
-    if (is_ag_node(llast(stmt), cypher_create)  || is_ag_node(llast(stmt), cypher_set) ||
-        is_ag_node(llast(stmt), cypher_delete))
+    if (is_ag_node(llast(stmt), cypher_create) || is_ag_node(llast(stmt), cypher_set) ||
+        is_ag_node(llast(stmt), cypher_delete) || is_ag_node(llast(stmt), cypher_merge))
     {
         // column definition list must be ... AS relname(colname agtype) ...
         if (!(rtfunc->funccolcount == 1 &&
diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c
index 151da2d..2eb738d 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -80,6 +80,7 @@
 #define AGE_VARNAME_CREATE_CLAUSE AGE_DEFAULT_VARNAME_PREFIX"create_clause"
 #define AGE_VARNAME_CREATE_NULL_VALUE AGE_DEFAULT_VARNAME_PREFIX"create_null_value"
 #define AGE_VARNAME_DELETE_CLAUSE AGE_DEFAULT_VARNAME_PREFIX"delete_clause"
+#define AGE_VARNAME_MERGE_CLAUSE AGE_DEFAULT_VARNAME_PREFIX"merge_clause"
 #define AGE_VARNAME_ID AGE_DEFAULT_VARNAME_PREFIX"id"
 #define AGE_VARNAME_SET_CLAUSE AGE_DEFAULT_VARNAME_PREFIX"set_clause"
 
@@ -257,6 +258,8 @@ static Expr *cypher_create_properties(cypher_parsestate *cpstate,
 static Expr *add_volatile_wrapper(Expr *node);
 static bool variable_exists(cypher_parsestate *cpstate, char *name);
 static int get_target_entry_resno(List *target_list, char *name);
+static void handle_prev_clause(cypher_parsestate *cpstate, Query *query,
+                               cypher_clause *clause, bool first_rte);
 static TargetEntry *placeholder_target_entry(cypher_parsestate *cpstate,
                                              char *name);
 static Query *transform_cypher_sub_pattern(cypher_parsestate *cpstate,
@@ -276,7 +279,6 @@ static Query *transform_cypher_delete(cypher_parsestate *cpstate,
 static List *transform_cypher_delete_item_list(cypher_parsestate *cpstate,
                                                List *delete_item_list,
                                                Query *query);
-
 //set operators
 static Query *transform_cypher_union(cypher_parsestate *cpstate,
                                      cypher_clause *clause);
@@ -291,16 +293,48 @@ Query *cypher_parse_sub_analyze_union(cypher_clause *clause,
                                       CommonTableExpr *parentCTE,
                                       bool locked_from_parent,
                                       bool resolve_unknowns);
-
+static void get_res_cols(ParseState *pstate, RangeTblEntry *l_rte,
+                         RangeTblEntry *r_rte, List **res_colnames,
+                         List **res_colvars);
 // unwind
 static Query *transform_cypher_unwind(cypher_parsestate *cpstate,
                                       cypher_clause *clause);
+// merge
+static Query *transform_cypher_merge(cypher_parsestate *cpstate,
+                                     cypher_clause *clause);
+static cypher_create_path *
+transform_merge_make_lateral_join(cypher_parsestate *cpstate, Query *query,
+                                  cypher_clause *clause,
+                                  cypher_clause *isolated_merge_clause);
+static cypher_create_path *
+transform_cypher_merge_path(cypher_parsestate *cpstate, List **target_list,
+                            cypher_path *path);
+static cypher_target_node *
+transform_merge_cypher_edge(cypher_parsestate *cpstate, List **target_list,
+                            cypher_relationship *edge);
+static cypher_target_node *
+transform_merge_cypher_node(cypher_parsestate *cpstate, List **target_list,
+                            cypher_node *node);
+static Node *transform_clause_for_join(cypher_parsestate *cpstate,
+                                       cypher_clause *clause,
+                                       RangeTblEntry **rte,
+                                       ParseNamespaceItem **nsitem,
+                                       Alias* alias);
+static cypher_clause *convert_merge_to_match(cypher_merge *merge);
+static void
+transform_cypher_merge_mark_tuple_position(List *target_list,
+                                           cypher_create_path *path);
+
 // transform
 #define PREV_CYPHER_CLAUSE_ALIAS    "_"
 #define CYPHER_OPT_RIGHT_ALIAS      "_R"
 #define transform_prev_cypher_clause(cpstate, prev_clause, add_rte_to_query) \
     transform_cypher_clause_as_subquery(cpstate, transform_cypher_clause, \
                                         prev_clause, NULL, add_rte_to_query)
+static char *get_next_default_alias(cypher_parsestate *cpstate);
+static transform_entity *find_transform_entity(cypher_parsestate *cpstate,
+                                               char *name,
+                                               enum transform_entity_type type);
 
 static RangeTblEntry *transform_cypher_clause_as_subquery(cypher_parsestate *cpstate,
                                                           transform_method transform,
@@ -310,11 +344,6 @@ static RangeTblEntry *transform_cypher_clause_as_subquery(cypher_parsestate *cps
 static Query *analyze_cypher_clause(transform_method transform,
                                     cypher_clause *clause,
                                     cypher_parsestate *parent_cpstate);
-static ParseNamespaceItem *makeNamespaceItem(RangeTblEntry *rte,
-                                             bool rel_visible,
-                                             bool cols_visible,
-                                             bool lateral_only,
-                                             bool lateral_ok);
 static List *transform_group_clause(cypher_parsestate *cpstate,
                                     List *grouplist, List **groupingSets,
                                     List **targetlist, List *sortClause,
@@ -332,7 +361,15 @@ static List *add_target_to_group_list(cypher_parsestate *cpstate,
                                       TargetEntry *tle, List *grouplist,
                                       List *targetlist, int location);
 static void advance_transform_entities_to_next_clause(List *entities);
-static List *make_target_list_from_join(ParseState *pstate, RangeTblEntry *rte);
+
+static ParseNamespaceItem *create_namespace_item(RangeTblEntry *rte, bool p_rel_visible,
+                                             bool p_cols_visible, bool p_lateral_only,
+                                             bool p_lateral_ok);
+static List *make_target_list_from_join(ParseState *pstate,
+                                    RangeTblEntry *rte);
+static Expr *add_volatile_wrapper(Expr *node);
+static FuncExpr *make_clause_func_expr(char *function_name,
+                                       Node *clause_information);
 
 /* for VLE support */
 static RangeTblEntry *transform_RangeFunction(cypher_parsestate *cpstate,
@@ -361,11 +398,11 @@ Query *transform_cypher_clause(cypher_parsestate *cpstate,
     }
     else if (is_ag_node(self, cypher_with))
     {
-        return transform_cypher_with(cpstate, clause);
+        result = transform_cypher_with(cpstate, clause);
     }
     else if (is_ag_node(self, cypher_match))
     {
-        return transform_cypher_match(cpstate, clause);
+        result = transform_cypher_match(cpstate, clause);
     }
     else if (is_ag_node(self, cypher_create))
     {
@@ -373,11 +410,15 @@ Query *transform_cypher_clause(cypher_parsestate *cpstate,
     }
     else if (is_ag_node(self, cypher_set))
     {
-        return transform_cypher_set(cpstate, clause);
+        result = transform_cypher_set(cpstate, clause);
     }
     else if (is_ag_node(self, cypher_delete))
     {
-        return transform_cypher_delete(cpstate, clause);
+        result = transform_cypher_delete(cpstate, clause);
+    }
+    else if (is_ag_node(self, cypher_merge))
+    {
+        result = transform_cypher_merge(cpstate, clause);
     }
     else if (is_ag_node(self, cypher_sub_pattern))
     {
@@ -990,12 +1031,7 @@ static Query *transform_cypher_delete(cypher_parsestate *cpstate,
     cypher_delete *self = (cypher_delete *)clause->self;
     Query *query;
     TargetEntry *tle;
-    Oid func_set_oid;
-    Const *pattern_const;
-    Expr *func_expr;
-    RangeTblEntry *rte;
-    int rtindex;
-    StringInfo str = makeStringInfo();
+    FuncExpr *func_expr;
 
     cypher_delete_information *delete_data;
 
@@ -1012,16 +1048,10 @@ static Query *transform_cypher_delete(cypher_parsestate *cpstate,
                  errmsg("DELETE cannot be the first clause in a Cypher query"),
                  parser_errposition(pstate, self->location)));
     }
-
-    rte = transform_prev_cypher_clause(cpstate, clause->prev, true);
-    rtindex = list_length(pstate->p_rtable);
-
-    // rte is the first RangeTblEntry in pstate
-    Assert(rtindex == 1);
-
-    query->targetList = expandRelAttrs(pstate, rte, rtindex, 0, -1);
-
-    func_set_oid = get_ag_func_oid(DELETE_CLAUSE_FUNCTION_NAME, 1, INTERNALOID);
+    else
+    {
+        handle_prev_clause(cpstate, query, clause->prev, true);
+    }
 
     delete_data->delete_items = transform_cypher_delete_item_list(cpstate,
                                                                   self->exprs,
@@ -1035,27 +1065,11 @@ static Query *transform_cypher_delete(cypher_parsestate *cpstate,
         delete_data->flags |= CYPHER_CLAUSE_FLAG_TERMINAL;
     }
 
-    /*
-     * Serialize the cypher_delete_information data structure. In certain
-     * cases (Prepared Statements and PL/pgsql), the MemoryContext that
-     * it is stored in will be destroyed. We need to get it into a format
-     * that Postgres' can copy between MemoryContexts. Just making it into
-     * an ExtensibleNode does not work, because there are certain parts of
-     * Postgres that cannot handle an ExtensibleNode in a function call.
-     * So we serialize the data structure and place it into a Const node
-     * that can handle these situations AND be copied correctly.
-     */
-    outNode(str, delete_data);
-
-    pattern_const = makeConst(INTERNALOID, -1, InvalidOid, str->len,
-                              PointerGetDatum(str->data), false, false);
-
-    func_expr = (Expr *)makeFuncExpr(func_set_oid, AGTYPEOID,
-                                     list_make1(pattern_const), InvalidOid,
-                                     InvalidOid, COERCE_EXPLICIT_CALL);
+    func_expr = make_clause_func_expr(DELETE_CLAUSE_FUNCTION_NAME,
+                                      (Node *)delete_data);
 
     // Create the target entry
-    tle = makeTargetEntry(func_expr, pstate->p_next_resno++,
+    tle = makeTargetEntry((Expr *)func_expr, pstate->p_next_resno++,
                           AGE_VARNAME_DELETE_CLAUSE, false);
     query->targetList = lappend(query->targetList, tle);
 
@@ -1112,6 +1126,7 @@ static Query *transform_cypher_unwind(cypher_parsestate *cpstate,
 
     unwind = makeFuncCall(list_make1(makeString("age_unnest")), NIL, -1);
 
+
     old_expr_kind = pstate->p_expr_kind;
     pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET;
     funcexpr = ParseFuncOrColumn(pstate, unwind->funcname,
@@ -1208,11 +1223,8 @@ static Query *transform_cypher_set(cypher_parsestate *cpstate,
     Query *query;
     cypher_update_information *set_items_target_list;
     TargetEntry *tle;
-    Oid func_set_oid;
-    Const *pattern_const;
-    Expr *func_expr;
+    FuncExpr *func_expr;
     char *clause_name;
-    StringInfo str = makeStringInfo();
 
     query = makeNode(Query);
     query->commandType = CMD_SELECT;
@@ -1237,22 +1249,9 @@ static Query *transform_cypher_set(cypher_parsestate *cpstate,
     }
     else
     {
-        RangeTblEntry *rte;
-        int rtindex;
-
-        rte = transform_prev_cypher_clause(cpstate, clause->prev, true);
-        rtindex = list_length(pstate->p_rtable);
-        Assert(rtindex == 1); // rte is the first RangeTblEntry in pstate
-        query->targetList = expandRelAttrs(pstate, rte, rtindex, 0, -1);
+        handle_prev_clause(cpstate, query, clause->prev, true);
     }
 
-    func_set_oid = get_ag_func_oid("_cypher_set_clause", 1, INTERNALOID);
-
-    /*if (list_length(self->items) != 1)
-        ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-            errmsg("%s clause does not yet support updating more than one property", clause_name),
-            parser_errposition(pstate, self->location)));
-*/
     if (self->is_remove == true)
     {
         set_items_target_list = transform_cypher_remove_item_list(cpstate,
@@ -1274,27 +1273,11 @@ static Query *transform_cypher_set(cypher_parsestate *cpstate,
         set_items_target_list->flags |= CYPHER_CLAUSE_FLAG_TERMINAL;
     }
 
-    /*
-     * Serialize the cypher_update_information data structure. In certain
-     * cases (Prepared Statements and PL/pgsql), the MemoryContext that
-     * it is stored in will be destroyed. We need to get it into a format
-     * that Postgres' can copy between MemoryContexts. Just making it into
-     * an ExtensibleNode does not work, because there are certain parts of
-     * Postgres that cannot handle an ExtensibleNode in a function call.
-     * So we serialize the data structure and place it into a Const node
-     * that can handle these situations AND be copied correctly.
-     */
-    outNode(str, set_items_target_list);
-
-    pattern_const = makeConst(INTERNALOID, -1, InvalidOid, str->len,
-                              PointerGetDatum(str->data), false, false);
-
-    func_expr = (Expr *)makeFuncExpr(func_set_oid, AGTYPEOID,
-                                     list_make1(pattern_const), InvalidOid,
-                                     InvalidOid, COERCE_EXPLICIT_CALL);
+    func_expr = make_clause_func_expr(SET_CLAUSE_FUNCTION_NAME,
+                                      (Node *)set_items_target_list);
 
     // Create the target entry
-    tle = makeTargetEntry(func_expr, pstate->p_next_resno++,
+    tle = makeTargetEntry((Expr *)func_expr, pstate->p_next_resno++,
                           AGE_VARNAME_SET_CLAUSE, false);
     query->targetList = lappend(query->targetList, tle);
 
@@ -2012,6 +1995,11 @@ static Query *transform_cypher_match(cypher_parsestate *cpstate,
         cpstate, transform_cypher_match_pattern, clause, self->where);
 }
 
+/*
+ * Transform the clause into a subquery. This subquery will be used
+ * in a join so setup the namespace item and the created the rtr
+ * for the join to use.
+ */
 static Node *transform_clause_for_join(cypher_parsestate *cpstate,
                                        cypher_clause *clause,
                                        RangeTblEntry **rte,
@@ -2025,7 +2013,7 @@ static Node *transform_clause_for_join(cypher_parsestate *cpstate,
                                                transform_cypher_clause,
                                                clause, alias, false);
 
-    *nsitem = makeNamespaceItem(*rte, false, true, false, true);
+    *nsitem = create_namespace_item(*rte, false, true, false, true);
 
     rtr = makeNode(RangeTblRef);
     rtr->rtindex = RTERangeTablePosn(pstate, *rte, NULL);
@@ -2033,6 +2021,14 @@ static Node *transform_clause_for_join(cypher_parsestate *cpstate,
     return (Node *) rtr;
 }
 
+/*
+ * For cases where we need to join two subqueries together (OPTIONAL MATCH and
+ * MERGE) we need to take the columns available in each rte and merge them
+ * together. The l_rte has precedence when there is a conflict, because that
+ * means that the pattern create in the current clause is referencing a
+ * variable declared in a previous clause (the l_rte). The output is the
+ * res_colnames and res_colvars that are passed in.
+ */
 static void get_res_cols(ParseState *pstate, RangeTblEntry *l_rte,
                          RangeTblEntry *r_rte, List **res_colnames,
                          List **res_colvars)
@@ -2048,9 +2044,11 @@ static void get_res_cols(ParseState *pstate, RangeTblEntry *l_rte,
     expandRTE(r_rte, RTERangeTablePosn(pstate, r_rte, NULL), 0, -1, false,
               &r_colnames, &r_colvars);
 
+    // add in all colnames and colvars from the l_rte.
     *res_colnames = list_concat(*res_colnames, l_colnames);
     *res_colvars = list_concat(*res_colvars, l_colvars);
 
+    // find new columns and if they are a var, pass them in.
     forboth(r_lname, r_colnames, r_lvar, r_colvars)
     {
         char *r_colname = strVal(lfirst(r_lname));
@@ -2083,13 +2081,12 @@ static void get_res_cols(ParseState *pstate, RangeTblEntry *l_rte,
 /*
  * transform_cypher_optional_match_clause
  *      Transform the previous clauses and OPTIONAL MATCH clauses to be LATERAL LEFT JOIN
- *      to construct a result value.
+ *   transform_cypher_optional_match_clause   to construct a result value.
  */
 static RangeTblEntry *transform_cypher_optional_match_clause(cypher_parsestate *cpstate,
                                                              cypher_clause *clause)
 {
     cypher_clause *prevclause;
-    cypher_match *self = (cypher_match *)clause->self;
     RangeTblEntry *rte;
     RangeTblEntry *l_rte, *r_rte;
     ParseNamespaceItem *l_nsitem, *r_nsitem;
@@ -2109,22 +2106,32 @@ static RangeTblEntry *transform_cypher_optional_match_clause(cypher_parsestate *
                                         &l_nsitem, l_alias);
     pstate->p_namespace = lappend(pstate->p_namespace, l_nsitem);
 
+    /*
+     * Remove the previous clause so when the transform_clause_for_join function
+     * transforms the OPTIONAL MATCH, the previous clause will not be transformed
+     * again.
+     */
     prevclause = clause->prev;
     clause->prev = NULL;
-    self->optional = true;
-    cpstate->p_opt_match = true;
+
+    //set the lateral flag to true
     pstate->p_lateral_active = true;
 
     j->rarg = transform_clause_for_join(cpstate, clause, &r_rte,
                                         &r_nsitem, r_alias);
 
-    cpstate->p_opt_match = false;
+    // we are done transform the lateral left join
     pstate->p_lateral_active = false;
-    self->optional = true;
+
+    /*
+     * We are done with the previous clause in the transform phase, but
+     * reattach the previous clause for semantics.
+     */
     clause->prev = prevclause;
 
     pstate->p_namespace = NIL;
 
+    // get the colnames and colvars from the rtes
     get_res_cols(pstate, l_rte, r_rte, &res_colnames, &res_colvars);
 
     rte = addRangeTableEntryForJoin(pstate, res_colnames, j->jointype,
@@ -2133,13 +2140,15 @@ static RangeTblEntry *transform_cypher_optional_match_clause(cypher_parsestate *
     j->rtindex = RTERangeTablePosn(pstate, rte, NULL);
 
     for (i = list_length(pstate->p_joinexprs) + 1; i < j->rtindex; i++)
+    {
         pstate->p_joinexprs = lappend(pstate->p_joinexprs, NULL);
+    }
     pstate->p_joinexprs = lappend(pstate->p_joinexprs, j);
     Assert(list_length(pstate->p_joinexprs) == j->rtindex);
 
     pstate->p_joinlist = lappend(pstate->p_joinlist, j);
 
-    nsitem = makeNamespaceItem(rte, false, true, false, true);
+    nsitem = create_namespace_item(rte, false, true, false, true);
     pstate->p_namespace = lappend(pstate->p_namespace, nsitem);
 
     return rte;
@@ -2195,7 +2204,7 @@ static Query *transform_cypher_match_pattern(cypher_parsestate *cpstate,
 }
 
 /*
- * Function to make a target list from an RTE. Borrowed from AgensGraph and PG
+ * Function to make a target list from an RTE. Taken from AgensGraph and PG
  */
 static List *make_target_list_from_join(ParseState *pstate, RangeTblEntry *rte)
 {
@@ -2360,8 +2369,8 @@ static Node *transform_VLE_Function(cypher_parsestate *cpstate, Node *n,
         Assert(rte == rt_fetch(rtindex, pstate->p_rtable));
         *top_rte = rte;
         *top_rti = rtindex;
-        *namespace = list_make1(makeNamespaceItem(rte, true, true, true,
-                                                  true));
+        *namespace = list_make1(create_namespace_item(rte, true, true, true,
+                                                      true));
         rtr = makeNode(RangeTblRef);
         rtr->rtindex = rtindex;
         return (Node *) rtr;
@@ -3933,10 +3942,6 @@ static Expr *transform_cypher_node(cypher_parsestate *cpstate,
         {
             transform_entity *entity = find_variable(cpstate, node->name);
 
-            if (cpstate->p_opt_match)
-            {
-                return te->expr;
-            }
             /*
              * TODO: openCypher allows a variable to be used before it
              * is properly declared. This logic is not satifactory
@@ -4099,14 +4104,11 @@ static Query *transform_cypher_create(cypher_parsestate *cpstate,
     ParseState *pstate = (ParseState *)cpstate;
     cypher_create *self = (cypher_create *)clause->self;
     cypher_create_target_nodes *target_nodes;
-    Const *pattern_const;
     Const *null_const;
     List *transformed_pattern;
-    Expr *func_expr;
-    Oid func_create_oid;
+    FuncExpr *func_expr;
     Query *query;
     TargetEntry *tle;
-    StringInfo str = makeStringInfo();
 
     target_nodes = make_ag_node(cypher_create_target_nodes);
     target_nodes->flags = CYPHER_CLAUSE_FLAG_NONE;
@@ -4118,20 +4120,11 @@ static Query *transform_cypher_create(cypher_parsestate *cpstate,
 
     if (clause->prev)
     {
-        RangeTblEntry *rte;
-        int rtindex;
-
-        rte = transform_prev_cypher_clause(cpstate, clause->prev, true);
-        rtindex = list_length(pstate->p_rtable);
-        Assert(rtindex == 1); // rte is the first RangeTblEntry in pstate
-        query->targetList = expandRelAttrs(pstate, rte, rtindex, 0, -1);
+        handle_prev_clause(cpstate, query, clause->prev, true);
 
         target_nodes->flags |= CYPHER_CLAUSE_FLAG_PREVIOUS_CLAUSE;
     }
 
-    func_create_oid = get_ag_func_oid(CREATE_CLAUSE_FUNCTION_NAME, 1,
-                                      INTERNALOID);
-
     null_const = makeNullConst(AGTYPEOID, -1, InvalidOid);
     tle = makeTargetEntry((Expr *)null_const, pstate->p_next_resno++,
                           AGE_VARNAME_CREATE_NULL_VALUE, false);
@@ -4151,33 +4144,12 @@ static Query *transform_cypher_create(cypher_parsestate *cpstate,
         target_nodes->flags |= CYPHER_CLAUSE_FLAG_TERMINAL;
     }
 
-    /*
-     * Serialize the cypher_create_target_nodes data structure. In certain
-     * cases (Prepared Statements and PL/pgsql), the MemoryContext that
-     * it is stored in will be destroyed. We need to get it into a format
-     * that Postgres' can copy between MemoryContexts. Just making it into
-     * an ExtensibleNode does not work, because there are certain parts of
-     * Postgres that cannot handle an ExtensibleNode in a function call.
-     * So we serialize the data structure and place it into a Const node
-     * that can handle these situations AND be copied correctly.
-     */
-    outNode(str, target_nodes);
-
-    pattern_const = makeConst(INTERNALOID, -1, InvalidOid, str->len,
-                              PointerGetDatum(str->data), false, false);
 
-    /*
-     * Create the FuncExpr Node.
-     * NOTE: We can't use Postgres' transformExpr function, because it will
-     * recursively transform the arguments, and our internal type would
-     * force an error to be thrown.
-     */
-    func_expr = (Expr *)makeFuncExpr(func_create_oid, AGTYPEOID,
-                                     list_make1(pattern_const), InvalidOid,
-                                     InvalidOid, COERCE_EXPLICIT_CALL);
+    func_expr = make_clause_func_expr(CREATE_CLAUSE_FUNCTION_NAME,
+                                      (Node *)target_nodes);
 
     // Create the target entry
-    tle = makeTargetEntry(func_expr, pstate->p_next_resno++,
+    tle = makeTargetEntry((Expr *)func_expr, pstate->p_next_resno++,
                           AGE_VARNAME_CREATE_CLAUSE, false);
     query->targetList = lappend(query->targetList, tle);
 
@@ -4645,6 +4617,10 @@ transform_create_cypher_new_node(cypher_parsestate *cpstate,
         *target_list = lappend(*target_list, te);
         rel->flags |= CYPHER_TARGET_NODE_IS_VAR;
     }
+    else
+    {
+        node->name = get_next_default_alias(cpstate);
+    }
 
     return rel;
 }
@@ -4718,26 +4694,6 @@ static Expr *cypher_create_properties(cypher_parsestate *cpstate,
 }
 
 /*
- * makeNamespaceItem (from PG makeNamespaceItem)-
- *        Convenience subroutine to construct a ParseNamespaceItem.
- */
-static ParseNamespaceItem *makeNamespaceItem(RangeTblEntry *rte,
-                                             bool rel_visible,
-                                             bool cols_visible,
-                                             bool lateral_only, bool lateral_ok)
-{
-    ParseNamespaceItem *nsitem;
-
-    nsitem = (ParseNamespaceItem *) palloc(sizeof(ParseNamespaceItem));
-    nsitem->p_rte = rte;
-    nsitem->p_rel_visible = rel_visible;
-    nsitem->p_cols_visible = cols_visible;
-    nsitem->p_lateral_only = lateral_only;
-    nsitem->p_lateral_ok = lateral_ok;
-    return nsitem;
-}
-
-/*
  * This function is similar to transformFromClause() that is called with a
  * single RangeSubselect.
  */
@@ -4759,6 +4715,7 @@ transform_cypher_clause_as_subquery(cypher_parsestate *cpstate,
      * to be added depending. However, at this time, only these are needed.
      */
     Assert(pstate->p_expr_kind == EXPR_KIND_NONE ||
+           pstate->p_expr_kind == EXPR_KIND_OTHER ||
            pstate->p_expr_kind == EXPR_KIND_WHERE ||
            pstate->p_expr_kind == EXPR_KIND_FROM_SUBSELECT);
 
@@ -4770,6 +4727,12 @@ transform_cypher_clause_as_subquery(cypher_parsestate *cpstate,
     {
         pstate->p_expr_kind = EXPR_KIND_FROM_SUBSELECT;
     }
+    else if (pstate->p_expr_kind == EXPR_KIND_OTHER)
+    {
+	// this is a lateral subselect for the MERGE
+        pstate->p_expr_kind = EXPR_KIND_FROM_SUBSELECT;
+        lateral = true;
+    }
     /*
      * If this is a WHERE, pass it through and set lateral to true because it
      * needs to see what comes before it.
@@ -4807,7 +4770,7 @@ transform_cypher_clause_as_subquery(cypher_parsestate *cpstate,
                      errmsg("rte must be last entry in p_rtable")));
         }
 
-        namespace = list_make1(makeNamespaceItem(rte, true, true, false, true));
+        namespace = list_make1(create_namespace_item(rte, true, true, false, true));
 
         checkNameSpaceConflicts(pstate, pstate->p_namespace, namespace);
     }
@@ -4957,3 +4920,726 @@ Query *cypher_parse_sub_analyze(Node *parseTree,
 
     return query;
 }
+
+/*
+ * Function for transforming MERGE.
+ *
+ * There are two cases for the form Query that is returned from here will
+ * take:
+ *
+ * 1. If there is no previous clause, the query will have a subquery that
+ * represents the path as a select staement, similar to match with a targetList
+ * that is all declared variables and the FuncExpr that represents the MERGE
+ * clause with its needed metadata information, that will be caught in the
+ * planner phase and converted into a path.
+ *
+ * 2. If there is a previous clause then the query will have two subqueries.
+ * The first query will be for the previous clause that we recursively handle.
+ * The second query will be for the path that this MERGE clause defines. The
+ * two subqueries will be joined together using a LATERAL LEFT JOIN with the
+ * previous query on the left and the MERGE path subquery on the right. Like
+ * case 1 the targetList will have all the decalred variables and a FuncExpr
+ * that represents the MERGE clause with its needed metadata information, that
+ * will be caught in the planner phase and converted into a path.
+ *
+ * This will allow us to be capable of handling the 2 cases that exist with a
+ * MERGE clause correctly.
+ *
+ * Case 1: the path already exists. In this case we do not need to create
+ * the path and MERGE will simply pass the tuple information up the execution
+ * tree.
+ *
+ * Case 2: the path does not exist. In this case the LEFT part of the join
+ * will not prevent the tuples from the previous clause from being emitted. We
+ * can catch when this happens in the execution phase and create the missing
+ * data, before passing up the execution tree.
+ *
+ * It should be noted that both cases can happen in the same query. If the
+ * MERGE clause references a variable from a previous clause, it could be that
+ * for one tuple the path exists (or there is multiple paths that exist and all
+ * paths must be emitted) and for another the path does not exist. This is
+ * similar to OPTIONAL MATCH, however with the added feature of creating the
+ * path if not there, rather than just emiting NULL.
+ */
+static Query *transform_cypher_merge(cypher_parsestate *cpstate,
+                                     cypher_clause *clause)
+{
+    ParseState *pstate = (ParseState *) cpstate;
+    cypher_clause *merge_clause_as_match;
+    cypher_create_path *merge_path;
+    cypher_merge *self = (cypher_merge *)clause->self;
+    cypher_merge_information *merge_information;
+    Query *query;
+    FuncExpr *func_expr;
+    TargetEntry *tle;
+
+    Assert(is_ag_node(self->path, cypher_path));
+
+    merge_information = make_ag_node(cypher_merge_information);
+
+    query = makeNode(Query);
+    query->commandType = CMD_SELECT;
+    query->targetList = NIL;
+
+    merge_information->flags = CYPHER_CLAUSE_FLAG_NONE;
+
+    // make the merge node into a match node
+    merge_clause_as_match = convert_merge_to_match(self);
+
+    /*
+     * If there is a previous clause we need to turn this query into a lateral
+     * join. See the function transform_merge_make_lateral_join for details.
+     */
+    if (clause->prev != NULL)
+    {
+        merge_path = transform_merge_make_lateral_join(cpstate, query, clause,
+                                                       merge_clause_as_match);
+
+        merge_information->flags |= CYPHER_CLAUSE_FLAG_PREVIOUS_CLAUSE;
+    }
+    else
+    {
+        // make the merge node into a match node
+        cypher_clause *merge_clause_as_match = convert_merge_to_match(self);
+
+        /*
+         * Create the metadata needed for creating missing paths.
+         */
+        merge_path = transform_cypher_merge_path(cpstate, &query->targetList,
+                                                 (cypher_path *)self->path);
+
+        /*
+         * If there is not a previous clause, then treat the MERGE's path
+         * itself as the previous clause. We need to do this because if the
+         * pattern exists, then we need to path all paths that match the
+         * query patterns in the execution phase. WE way to do that by
+         * converting the merge to a match and have the match logic create the
+         * query. the merge execution phase will just pass the results up the
+         * execution tree if the path exists.
+         */
+        handle_prev_clause(cpstate, query, merge_clause_as_match, false);
+
+        /*
+         * For the metadata need to create paths, find the tuple position that
+         * will represent the entity in the execution phase.
+         */
+        transform_cypher_merge_mark_tuple_position(query->targetList,
+                                                   merge_path);
+    }
+
+    merge_information->graph_oid = cpstate->graph_oid;
+    merge_information->path = merge_path;
+
+    if (!clause->next)
+    {
+        merge_information->flags |= CYPHER_CLAUSE_FLAG_TERMINAL;
+    }
+
+    /*
+     * Creates the function expression that the planner will find and
+     * convert to a MERGE path.
+     */
+    func_expr = make_clause_func_expr(MERGE_CLAUSE_FUNCTION_NAME,
+                                      (Node *)merge_information);
+
+    // Create the target entry
+    tle = makeTargetEntry((Expr *)func_expr, pstate->p_next_resno++,
+                          AGE_VARNAME_MERGE_CLAUSE, false);
+
+    merge_information->merge_function_attr = tle->resno;
+    query->targetList = lappend(query->targetList, tle);
+
+    markTargetListOrigins(pstate, query->targetList);
+
+    query->rtable = pstate->p_rtable;
+    query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+
+    query->hasSubLinks = pstate->p_hasSubLinks;
+
+    assign_query_collations(pstate, query);
+
+    return query;
+}
+
+/*
+ * This function does the heavy lifting of transforming a MERGE clause that has
+ * a clause before it in the query of turning that into a lateral left join.
+ * The previous clause will still be able to emit tuples if the path defined in
+ * MERGE clause is not found. In that case variable assinged in the MERGE
+ * clause will be emitted as NULL (same as OPTIONAL MATCH).
+ */
+static cypher_create_path *
+transform_merge_make_lateral_join(cypher_parsestate *cpstate, Query *query,
+                                  cypher_clause *clause,
+                                  cypher_clause *isolated_merge_clause)
+{
+    cypher_create_path *merge_path;
+    ParseState *pstate = (ParseState *) cpstate;
+    int i;
+    Alias *l_alias;
+    Alias *r_alias;
+    RangeTblEntry *rte;
+    RangeTblEntry *l_rte, *r_rte;
+    ParseNamespaceItem *l_nsitem, *r_nsitem;
+    JoinExpr *j = makeNode(JoinExpr);
+    List *res_colnames = NIL, *res_colvars = NIL;
+    ParseNamespaceItem *nsitem;
+    ParseExprKind tmp;
+    cypher_merge *self = (cypher_merge *)clause->self;
+    cypher_path *path;
+
+    Assert(is_ag_node(self->path, cypher_path));
+
+    path = (cypher_path *)self->path;
+
+    r_alias = makeAlias(CYPHER_OPT_RIGHT_ALIAS, NIL);
+    l_alias = makeAlias(PREV_CYPHER_CLAUSE_ALIAS, NIL);
+
+    j->jointype = JOIN_LEFT;
+
+    /*
+     * transform the previous clause
+     */
+    j->larg = transform_clause_for_join(cpstate, clause->prev, &l_rte,
+                                            &l_nsitem, l_alias);
+    pstate->p_namespace = lappend(pstate->p_namespace, l_nsitem);
+
+    /*
+     * Get the merge path now. This is the only moment where it is simple
+     * to know if a variable was declared in the MERGE clause or a previous
+     * clause. Unlike create, we do not add these missing variables to the
+     * targetList, we just create all the metadata necessary to make the
+     * potentially missing parts of the path.
+     */
+    merge_path = transform_cypher_merge_path(cpstate, &query->targetList,
+                                             path);
+
+    /*
+     * Transform this MERGE clause as a match clause, mark the parsestate
+     * with the flag that a lateral join is active
+     */
+    pstate->p_lateral_active = true;
+    tmp = pstate->p_expr_kind;
+    pstate->p_expr_kind = EXPR_KIND_OTHER;
+
+    // transform MERGE
+    j->rarg = transform_clause_for_join(cpstate, isolated_merge_clause, &r_rte,
+                                            &r_nsitem, r_alias);
+
+    // deactivate the lateral flag
+    pstate->p_lateral_active = false;
+
+    pstate->p_namespace = NIL;
+
+    /*
+     * Resolve the column names and variables between the two subqueries,
+     * in most cases, we can expect there to be overlap
+     */
+    get_res_cols(pstate, l_rte, r_rte, &res_colnames, &res_colvars);
+
+    // make the RTE for the join
+    rte = addRangeTableEntryForJoin(pstate, res_colnames, j->jointype,
+                                        res_colvars, j->alias, true);
+
+    j->rtindex = RTERangeTablePosn(pstate, rte, NULL);
+
+    /*
+     * The index of a node in the p_joinexpr list is expected to match the
+     * rtindex the join expression is for. Add NULLs for all the previous
+     * rtindexes and add the JoinExpr.
+     */
+    for (i = list_length(pstate->p_joinexprs) + 1; i < j->rtindex; i++)
+    {
+        pstate->p_joinexprs = lappend(pstate->p_joinexprs, NULL);
+    }
+
+    pstate->p_joinexprs = lappend(pstate->p_joinexprs, j);
+
+    Assert(list_length(pstate->p_joinexprs) == j->rtindex);
+
+    pstate->p_joinlist = lappend(pstate->p_joinlist, j);
+
+    pstate->p_expr_kind = tmp;
+
+    /*
+     * Create the namespace item for the joined subqueries, and append
+     * to the ParseState's list of namespaces.
+     */
+    nsitem = create_namespace_item(rte, true, true, false, true);
+
+    pstate->p_namespace = lappend(pstate->p_namespace, nsitem);
+
+    /*
+     * Create the targetList from the joined subqueries, add everything.
+     */
+    query->targetList = list_concat(query->targetList,
+                                    make_target_list_from_join(pstate, rte));
+
+    /*
+     * For the metadata need to create paths, find the tuple position that
+     * will represent the entity in the execution phase.
+     */
+    transform_cypher_merge_mark_tuple_position(query->targetList,
+                                               merge_path);
+
+    return merge_path;
+}
+
+/*
+ * Iterate through the path and find the TargetEntry in the target_list
+ * that each cypher_target_node is referencing. Add the volatile wrapper
+ * function to keep the optimizer from removing the TargetEntry.
+ */
+static void
+transform_cypher_merge_mark_tuple_position(List *target_list,
+                                           cypher_create_path *path)
+{
+    ListCell *lc = NULL;
+
+    if (path->var_name)
+    {
+        TargetEntry *te = findTarget(target_list, path->var_name);
+
+        /*
+         * Add the volatile wrapper function around the expression, this
+         * ensures the optimizer will not remove the expression, if nothing
+         * other than a private data structure needs it.
+         */
+        te->expr = add_volatile_wrapper(te->expr);
+
+        // Mark the tuple position the target_node is for.
+        path->path_attr_num = te->resno;
+    }
+
+    foreach (lc, path->target_nodes)
+    {
+        cypher_target_node *node = lfirst(lc);
+
+        TargetEntry *te = findTarget(target_list, node->variable_name);
+
+        /*
+         * Add the volatile wrapper function around the expression, this
+         * ensures the optimizer will not remove the expression, if nothing
+         * other than a private data structure needs it.
+         */
+        te->expr = add_volatile_wrapper(te->expr);
+
+        // Mark the tuple position the target_node is for.
+        node->tuple_position = te->resno;
+    }
+}
+
+/*
+ * Creates the target nodes for a merge path. If MERGE has a path that doesn't
+ * exist then in the MERGE clause we act like a CREATE clause. This function
+ * sets up the metadata needed for that process.
+ */
+static cypher_create_path *
+transform_cypher_merge_path(cypher_parsestate *cpstate, List **target_list,
+                            cypher_path *path)
+{
+    ListCell *lc;
+    List *transformed_path = NIL;
+    cypher_create_path *ccp = make_ag_node(cypher_create_path);
+    bool in_path = path->var_name != NULL;
+
+    ccp->path_attr_num = InvalidAttrNumber;
+
+    foreach (lc, path->path)
+    {
+        if (is_ag_node(lfirst(lc), cypher_node))
+        {
+            cypher_node *node = lfirst(lc);
+
+            cypher_target_node *rel =
+                transform_merge_cypher_node(cpstate, target_list, node);
+
+            if (in_path)
+            {
+                rel->flags |= CYPHER_TARGET_NODE_IN_PATH_VAR;
+            }
+
+            transformed_path = lappend(transformed_path, rel);
+        }
+        else if (is_ag_node(lfirst(lc), cypher_relationship))
+        {
+            cypher_relationship *edge = lfirst(lc);
+
+            cypher_target_node *rel =
+                transform_merge_cypher_edge(cpstate, target_list, edge);
+
+            if (in_path)
+            {
+                rel->flags |= CYPHER_TARGET_NODE_IN_PATH_VAR;
+            }
+
+            transformed_path = lappend(transformed_path, rel);
+        }
+        else
+        {
+            ereport(ERROR,
+                    (errmsg_internal("unreconized node in create pattern")));
+        }
+    }
+
+    // store the path's variable name
+    if (path->var_name)
+    {
+        ccp->var_name = path->var_name;
+    }
+
+    ccp->target_nodes = transformed_path;
+
+    return ccp;
+}
+
+/*
+ * Transforms the parse cypher_relationship to a target_entry for merge.
+ * All edges that have variables assigned in a merge must be declared in
+ * the merge. Throw an error otherwise.
+ */
+static cypher_target_node *
+transform_merge_cypher_edge(cypher_parsestate *cpstate, List **target_list,
+                             cypher_relationship *edge)
+{
+    ParseState *pstate = (ParseState *)cpstate;
+    cypher_target_node *rel = make_ag_node(cypher_target_node);
+    Relation label_relation;
+    RangeVar *rv;
+    RangeTblEntry *rte;
+
+    if (edge->name != NULL)
+    {
+        transform_entity *entity = find_transform_entity(cpstate, edge->name,
+                                                         ENT_EDGE);
+
+        // We found a variable with this variable name, throw an error.
+        if (entity != NULL)
+        {
+            ereport(ERROR,
+                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                 errmsg("variable %s already exists", edge->name),
+                 parser_errposition(pstate, edge->location)));
+        }
+
+	rel->flags |= CYPHER_TARGET_NODE_IS_VAR;
+    }
+    else
+    {
+        // assign a default variable name.
+        edge->name = get_next_default_alias(cpstate);
+    }
+
+    rel->type = LABEL_KIND_EDGE;
+
+    // all edges are marked with insert
+    rel->flags |= CYPHER_TARGET_NODE_FLAG_INSERT;
+    rel->label_name = edge->label;
+    rel->variable_name = edge->name;
+    rel->resultRelInfo = NULL;
+
+    rel->dir = edge->dir;
+
+    if (!edge->label)
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                 errmsg("edges declared in a MERGE clause must have a label"),
+                 parser_errposition(&cpstate->pstate, edge->location)));
+    }
+
+
+    // check to see if the label exists, create the label entry if it does not.
+    if (edge->label && !label_exists(edge->label, cpstate->graph_oid))
+    {
+        List *parent;
+        RangeVar *rv;
+
+        /*
+         * setup the default edge table as the parent table, that we
+         * will inherit from.
+         */
+        rv = get_label_range_var(cpstate->graph_name, cpstate->graph_oid,
+                                 AG_DEFAULT_LABEL_EDGE);
+
+        parent = list_make1(rv);
+
+        // create the label
+        create_label(cpstate->graph_name, edge->label, LABEL_TYPE_EDGE,
+                     parent);
+    }
+
+    // lock the relation of the label
+    rv = makeRangeVar(cpstate->graph_name, edge->label, -1);
+    label_relation = parserOpenTable(&cpstate->pstate, rv, RowExclusiveLock);
+
+    // Store the relid
+    rel->relid = RelationGetRelid(label_relation);
+
+    rte = addRangeTableEntryForRelation((ParseState *)cpstate, label_relation,
+                                        NULL, false, false);
+    rte->requiredPerms = ACL_INSERT;
+
+    // Build Id expression, always use the default logic
+    rel->id_expr = (Expr *)build_column_default(label_relation,
+                                      Anum_ag_label_edge_table_id);
+
+    rel->prop_expr = cypher_create_properties(cpstate, rel, label_relation,
+                                              edge->props, ENT_EDGE);
+
+    // Keep the lock
+    heap_close(label_relation, NoLock);
+
+    return rel;
+}
+
+/*
+ * Function for creating the metadata MERGE will need if MERGE does not find
+ * a path to exist
+ */
+static cypher_target_node *
+transform_merge_cypher_node(cypher_parsestate *cpstate, List **target_list,
+                            cypher_node *node)
+{
+    cypher_target_node *rel = make_ag_node(cypher_target_node);
+    Relation label_relation;
+    RangeVar *rv;
+    RangeTblEntry *rte;
+
+    if (node->name != NULL)
+    {
+
+        transform_entity *entity = find_transform_entity(cpstate, node->name,
+                                                         ENT_VERTEX);
+
+        /*
+         *  the vertex was previously declared, we do not need to do any setup
+         *  to create the node.
+         */
+        if (entity != NULL)
+        {
+                rel->type = LABEL_KIND_VERTEX;
+                rel->tuple_position = InvalidAttrNumber;
+                rel->variable_name = node->name;
+                rel->resultRelInfo = NULL;
+
+                rel->flags |= CYPHER_TARGET_NODE_MERGE_EXISTS;
+                return rel;
+        }
+        rel->flags |= CYPHER_TARGET_NODE_IS_VAR;
+    }
+    else
+    {
+        // assign a default variable name.
+        node->name = get_next_default_alias(cpstate);
+    }
+
+    rel->type = LABEL_KIND_VERTEX;
+    rel->tuple_position = InvalidAttrNumber;
+    rel->variable_name = node->name;
+    rel->resultRelInfo = NULL;
+
+    if (!node->label)
+    {
+        rel->label_name = "";
+        /*
+         *  If no label is specified, assign the generic label name that
+         *  all labels are descendents of.
+         */
+        node->label = AG_DEFAULT_LABEL_VERTEX;
+    }
+    else
+    {
+        rel->label_name = node->label;
+    }
+
+    // check to see if the label exists, create the label entry if it does not.
+    if (node->label && !label_exists(node->label, cpstate->graph_oid))
+    {
+        List *parent;
+        RangeVar *rv;
+
+        /*
+         * setup the default vertex table as the parent table, that we
+         * will inherit from.
+         */
+        rv = get_label_range_var(cpstate->graph_name, cpstate->graph_oid,
+                                 AG_DEFAULT_LABEL_VERTEX);
+
+        parent = list_make1(rv);
+
+        // create the label
+        create_label(cpstate->graph_name, node->label, LABEL_TYPE_VERTEX,
+                     parent);
+    }
+
+    rel->flags |= CYPHER_TARGET_NODE_FLAG_INSERT;
+
+    rv = makeRangeVar(cpstate->graph_name, node->label, -1);
+    label_relation = parserOpenTable(&cpstate->pstate, rv, RowExclusiveLock);
+
+    // Store the relid
+    rel->relid = RelationGetRelid(label_relation);
+
+    rte = addRangeTableEntryForRelation((ParseState *)cpstate, label_relation,
+                                        NULL, false, false);
+    rte->requiredPerms = ACL_INSERT;
+
+    // id
+    rel->id_expr = (Expr *)build_column_default(label_relation,
+                                                Anum_ag_label_vertex_table_id);
+
+    rel->prop_expr = cypher_create_properties(cpstate, rel, label_relation,
+                                              node->props, ENT_VERTEX);
+
+    heap_close(label_relation, NoLock);
+
+    return rel;
+}
+
+/*
+ * Takes a MERGE parse node and converts it to a MATCH parse node
+ */
+static cypher_clause *convert_merge_to_match(cypher_merge *merge)
+{
+    cypher_match *match = make_ag_node(cypher_match);
+    cypher_clause *clause = palloc(sizeof(cypher_clause));
+
+    // match supports multiple paths, whereas merge only supports one.
+    match->pattern = list_make1(merge->path);
+    // MERGE does not support where
+    match->where = NULL;
+
+    /*
+     *  We do not want the transform logic to transform the previous clauses
+     *  with this, just handle this one clause.
+     */
+    clause->prev = NULL;
+    clause->self = (Node *)match;
+    clause->next = NULL;
+
+    return clause;
+}
+
+/*
+ * Finds the transform_entity in the cypher_parstate for a the given name and
+ * type.
+ */
+static transform_entity *find_transform_entity(cypher_parsestate *cpstate,
+                                               char *name,
+                                               enum transform_entity_type type)
+{
+    ListCell *lc;
+
+    foreach(lc, cpstate->entities)
+    {
+        transform_entity *entity = lfirst(lc);
+
+        if (entity->type != type)
+        {
+            continue;
+        }
+
+        if (type == ENT_VERTEX)
+        {
+            if (!strcmp(entity->entity.node->name, name))
+            {
+                return entity;
+            }
+        }
+        else if (type == ENT_EDGE || type == ENT_VLE_EDGE)
+        {
+            if (!strcmp(entity->entity.rel->name, name))
+            {
+                return entity;
+            }
+        }
+    }
+
+    return NULL;
+}
+
+/*
+ * Creates a namespace item for the given rte. boolean arguements will
+ * let the rest of the ParseState know if the relation and/or columns are
+ * visible, whether the rte is only usable in lateral joins, and if the rte
+ * is accessible in lateral joins.
+ */
+static ParseNamespaceItem *create_namespace_item(RangeTblEntry *rte,
+                                                 bool p_rel_visible,
+					         bool p_cols_visible,
+                                                 bool p_lateral_only,
+					         bool p_lateral_ok)
+{
+    ParseNamespaceItem *nsitem;
+
+    nsitem = palloc(sizeof(*nsitem));
+    nsitem->p_rte = rte;
+    nsitem->p_rel_visible = p_rel_visible;
+    nsitem->p_cols_visible = p_cols_visible;
+    nsitem->p_lateral_only = p_lateral_only;
+    nsitem->p_lateral_ok = p_lateral_ok;
+
+    return nsitem;
+}
+
+/*
+ * Creates the function expression that represents the clause. Adds the
+ * extensible node that represents the metadata that the clause needs to
+ * handle the clause in the execution phase.
+ */
+static FuncExpr *make_clause_func_expr(char *function_name,
+                                       Node *clause_information)
+{
+    Const *clause_information_const;
+    Oid func_oid;
+    FuncExpr *func_expr;
+
+    StringInfo str = makeStringInfo();
+    /*
+     * Serialize the clause_information data structure. In certain
+     * cases (Prepared Statements and PL/pgsql), the MemoryContext that
+     * it is stored in will be destroyed. We need to get it into a format
+     * that Postgres' can copy between MemoryContexts. Just making it into
+     * an ExtensibleNode does not work, because there are certain parts of
+     * Postgres that cannot handle an ExtensibleNode in a function call.
+     * So we serialize the data structure and place it into a Const node
+     * that can handle these situations AND be copied correctly.
+     */
+    outNode(str, clause_information);
+
+    clause_information_const = makeConst(INTERNALOID, -1, InvalidOid, str->len,
+                             PointerGetDatum(str->data), false, false);
+
+    func_oid = get_ag_func_oid(function_name, 1, INTERNALOID);
+
+    func_expr = makeFuncExpr(func_oid, AGTYPEOID,
+                             list_make1(clause_information_const), InvalidOid,
+                             InvalidOid, COERCE_EXPLICIT_CALL);
+
+    return func_expr;
+}
+
+/*
+ * Utility function that helps a clause add the information needed to
+ * the query from the previous clause.
+ */
+static void handle_prev_clause(cypher_parsestate *cpstate, Query *query,
+                               cypher_clause *clause, bool first_rte)
+{
+    ParseState *pstate = (ParseState *) cpstate;
+    RangeTblEntry *rte;
+    int rtindex;
+
+    rte = transform_prev_cypher_clause(cpstate, clause, true);
+    rtindex = list_length(pstate->p_rtable);
+
+    // rte is the first RangeTblEntry in pstate
+    if (first_rte)
+    {
+        Assert(rtindex == 1);
+    }
+
+    // add all the rte's attributes to the current queries targetlist
+    query->targetList = expandRelAttrs(pstate, rte, rtindex, 0, -1);
+}
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index 34b5b2f..b5ec362 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -85,7 +85,7 @@
                  FALSE_P
                  IN IS
                  LIMIT
-                 MATCH
+                 MATCH MERGE
                  NOT NULL_P
                  OPTIONAL OR ORDER
                  REMOVE RETURN
@@ -127,6 +127,9 @@
 %type <node> delete
 %type <boolean> detach_opt
 
+/* MERGE clause */
+%type <node> merge
+
 /* common */
 %type <node> where_opt
 
@@ -405,6 +408,7 @@ updating_clause:
     | set
     | remove
     | delete
+    | merge
     ;
 
 cypher_varlen_opt:
@@ -897,6 +901,21 @@ detach_opt:
     ;
 
 /*
+ * MERGE clause
+ */
+merge:
+    MERGE path
+        {
+            cypher_merge *n;
+
+            n = make_ag_node(cypher_merge);
+            n->path = $2;
+
+            $$ = (Node *)n;
+        }
+    ;
+
+/*
  * common
  */
 
@@ -1837,6 +1856,7 @@ safe_keywords:
     | IS         { $$ = pnstrdup($1, 2); }
     | LIMIT      { $$ = pnstrdup($1, 6); }
     | MATCH      { $$ = pnstrdup($1, 6); }
+    | MERGE      { $$ = pnstrdup($1, 6); }
     | NOT        { $$ = pnstrdup($1, 3); }
     | OPTIONAL   { $$ = pnstrdup($1, 8); }
     | OR         { $$ = pnstrdup($1, 2); }
diff --git a/src/backend/parser/cypher_keywords.c b/src/backend/parser/cypher_keywords.c
index d0ad0bf..63e2fb0 100644
--- a/src/backend/parser/cypher_keywords.c
+++ b/src/backend/parser/cypher_keywords.c
@@ -65,6 +65,7 @@ const ScanKeyword cypher_keywords[] = {
     {"is", IS, RESERVED_KEYWORD},
     {"limit", LIMIT, RESERVED_KEYWORD},
     {"match", MATCH, RESERVED_KEYWORD},
+    {"merge", MERGE, RESERVED_KEYWORD},
     {"not", NOT, RESERVED_KEYWORD},
     {"null", NULL_P, RESERVED_KEYWORD},
     {"optional", OPTIONAL, RESERVED_KEYWORD},
diff --git a/src/backend/utils/adt/cypher_funcs.c b/src/backend/utils/adt/cypher_funcs.c
index 97ed35c..af7287a 100644
--- a/src/backend/utils/adt/cypher_funcs.c
+++ b/src/backend/utils/adt/cypher_funcs.c
@@ -55,3 +55,10 @@ Datum _cypher_delete_clause(PG_FUNCTION_ARGS)
 {
     PG_RETURN_NULL();
 }
+
+PG_FUNCTION_INFO_V1(_cypher_merge_clause);
+
+Datum _cypher_merge_clause(PG_FUNCTION_ARGS)
+{
+    PG_RETURN_NULL();
+}
diff --git a/src/include/executor/cypher_executor.h b/src/include/executor/cypher_executor.h
index 8801593..5722285 100644
--- a/src/include/executor/cypher_executor.h
+++ b/src/include/executor/cypher_executor.h
@@ -27,6 +27,7 @@
 #define DELETE_SCAN_STATE_NAME "Cypher Delete"
 #define SET_SCAN_STATE_NAME "Cypher Set"
 #define CREATE_SCAN_STATE_NAME "Cypher Create"
+#define MERGE_SCAN_STATE_NAME "Cypher Merge"
 
 Node *create_cypher_create_plan_state(CustomScan *cscan);
 extern const CustomExecMethods cypher_create_exec_methods;
@@ -37,4 +38,7 @@ extern const CustomExecMethods cypher_set_exec_methods;
 Node *create_cypher_delete_plan_state(CustomScan *cscan);
 extern const CustomExecMethods cypher_delete_exec_methods;
 
+Node *create_cypher_merge_plan_state(CustomScan *cscan);
+extern const CustomExecMethods cypher_merge_exec_methods;
+
 #endif
diff --git a/src/include/executor/cypher_utils.h b/src/include/executor/cypher_utils.h
index bd57f9d..b2102d4 100644
--- a/src/include/executor/cypher_utils.h
+++ b/src/include/executor/cypher_utils.h
@@ -75,6 +75,20 @@ typedef struct cypher_delete_custom_scan_state
     List *edge_labels;
 } cypher_delete_custom_scan_state;
 
+typedef struct cypher_merge_custom_scan_state
+{
+    CustomScanState css;
+    CustomScan *cs;
+    cypher_merge_information *merge_information;
+    int flags;
+    cypher_create_path *path;
+    List *path_values;
+    Oid graph_oid;
+    AttrNumber merge_function_attr;
+    bool created_new_path;
+    bool found_a_path;
+} cypher_merge_custom_scan_state;
+
 TupleTableSlot *populate_vertex_tts(TupleTableSlot *elemTupleSlot, agtype_value *id, agtype_value *properties);
 TupleTableSlot *populate_edge_tts(
     TupleTableSlot *elemTupleSlot, agtype_value *id, agtype_value *startid,
@@ -82,4 +96,9 @@ TupleTableSlot *populate_edge_tts(
 
 ResultRelInfo *create_entity_result_rel_info(EState *estate, char *graph_name, char *label_name);
 
+bool entity_exists(EState *estate, Oid graph_oid, graphid id);
+HeapTuple insert_entity_tuple(ResultRelInfo *resultRelInfo,
+                              TupleTableSlot *elemTupleSlot,
+                              EState *estate);
+
 #endif
diff --git a/src/include/nodes/ag_nodes.h b/src/include/nodes/ag_nodes.h
index 242cd4d..8585b33 100644
--- a/src/include/nodes/ag_nodes.h
+++ b/src/include/nodes/ag_nodes.h
@@ -42,6 +42,7 @@ typedef enum ag_node_tag
     cypher_delete_t,
     cypher_union_t,
     cypher_unwind_t,
+    cypher_merge_t,
     // pattern
     cypher_path_t,
     cypher_node_t,
@@ -68,7 +69,8 @@ typedef enum ag_node_tag
     cypher_update_item_t,
     // delete data structures
     cypher_delete_information_t,
-    cypher_delete_item_t
+    cypher_delete_item_t,
+    cypher_merge_information_t
 } ag_node_tag;
 
 void register_ag_nodes(void);
diff --git a/src/include/nodes/cypher_copyfuncs.h b/src/include/nodes/cypher_copyfuncs.h
index d55c12f..314cbab 100644
--- a/src/include/nodes/cypher_copyfuncs.h
+++ b/src/include/nodes/cypher_copyfuncs.h
@@ -47,4 +47,6 @@ void copy_cypher_update_item(ExtensibleNode *newnode, const ExtensibleNode *from
 void copy_cypher_delete_information(ExtensibleNode *newnode, const ExtensibleNode *from);
 void copy_cypher_delete_item(ExtensibleNode *newnode, const ExtensibleNode *from);
 
+// merge data structure
+void copy_cypher_merge_information(ExtensibleNode *newnode, const ExtensibleNode *from);
 #endif
diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h
index 03a28b4..e41574c 100644
--- a/src/include/nodes/cypher_nodes.h
+++ b/src/include/nodes/cypher_nodes.h
@@ -122,6 +122,12 @@ typedef struct cypher_unwind
     ResTarget *target;
 } cypher_unwind;
 
+typedef struct cypher_merge
+{
+    ExtensibleNode extensible;
+    Node *path;
+} cypher_merge;
+
 /*
  * pattern
  */
@@ -231,6 +237,7 @@ typedef struct cypher_create_path
     ExtensibleNode extensible;
     List *target_nodes;
     AttrNumber path_attr_num;
+    char *var_name;
 } cypher_create_path;
 
 #define CYPHER_CLAUSE_FLAG_NONE 0x0000
@@ -272,6 +279,9 @@ typedef struct cypher_target_node
      */
     Expr *id_expr;
     ExprState *id_expr_state;
+
+    Expr *prop_expr;
+    ExprState *prop_expr_state;
     /*
      * Attribute Number that this entity's properties
      * are stored in the CustomScanState's child TupleTableSlot
@@ -310,6 +320,8 @@ typedef struct cypher_target_node
 // node is an element in a path variable
 #define CYPHER_TARGET_NODE_IN_PATH_VAR 0x0008
 
+#define CYPHER_TARGET_NODE_MERGE_EXISTS 0x0010
+
 #define CYPHER_TARGET_NODE_OUTPUT(flags) \
     (flags & (CYPHER_TARGET_NODE_IS_VAR | CYPHER_TARGET_NODE_IN_PATH_VAR))
 
@@ -371,6 +383,15 @@ typedef struct cypher_delete_item
     char *var_name;
 } cypher_delete_item;
 
+typedef struct cypher_merge_information
+{
+    ExtensibleNode extensible;
+    int flags;
+    Oid graph_oid;
+    AttrNumber merge_function_attr;
+    cypher_create_path *path;
+} cypher_merge_information;
+
 /* grammar node for typecasts */
 typedef struct cypher_typecast
 {
diff --git a/src/include/nodes/cypher_outfuncs.h b/src/include/nodes/cypher_outfuncs.h
index d7a6abd..b1129f9 100644
--- a/src/include/nodes/cypher_outfuncs.h
+++ b/src/include/nodes/cypher_outfuncs.h
@@ -42,6 +42,7 @@ void out_cypher_delete(StringInfo str, const ExtensibleNode *node);
 void out_cypher_union(StringInfo str, const ExtensibleNode *node);
 void out_cypher_union_stmt(StringInfo str, const ExtensibleNode *node);
 void out_cypher_unwind(StringInfo str, const ExtensibleNode *node);
+void out_cypher_merge(StringInfo str, const ExtensibleNode *node);
 
 // pattern
 void out_cypher_path(StringInfo str, const ExtensibleNode *node);
@@ -79,4 +80,7 @@ void out_cypher_update_item(StringInfo str, const ExtensibleNode *node);
 void out_cypher_delete_information(StringInfo str, const ExtensibleNode *node);
 void out_cypher_delete_item(StringInfo str, const ExtensibleNode *node);
 
+// merge private data structures
+void out_cypher_merge_information(StringInfo str, const ExtensibleNode *node);
+
 #endif
diff --git a/src/include/nodes/cypher_readfuncs.h b/src/include/nodes/cypher_readfuncs.h
index 65637fa..5d4b0a5 100644
--- a/src/include/nodes/cypher_readfuncs.h
+++ b/src/include/nodes/cypher_readfuncs.h
@@ -52,4 +52,6 @@ void read_cypher_update_item(struct ExtensibleNode *node);
 void read_cypher_delete_information(struct ExtensibleNode *node);
 void read_cypher_delete_item(struct ExtensibleNode *node);
 
+void read_cypher_merge_information(struct ExtensibleNode *node);
+
 #endif
diff --git a/src/include/optimizer/cypher_createplan.h b/src/include/optimizer/cypher_createplan.h
index b3bba9f..50a86e1 100644
--- a/src/include/optimizer/cypher_createplan.h
+++ b/src/include/optimizer/cypher_createplan.h
@@ -33,7 +33,11 @@ Plan *plan_cypher_set_path(PlannerInfo *root, RelOptInfo *rel,
                            List *clauses, List *custom_plans);
 
 Plan *plan_cypher_delete_path(PlannerInfo *root, RelOptInfo *rel,
-                           CustomPath *best_path, List *tlist,
-                           List *clauses, List *custom_plans);
+                              CustomPath *best_path, List *tlist,
+                              List *clauses, List *custom_plans);
+
+Plan *plan_cypher_merge_path(PlannerInfo *root, RelOptInfo *rel,
+                             CustomPath *best_path, List *tlist,
+                             List *clauses, List *custom_plans);
 
 #endif
diff --git a/src/include/optimizer/cypher_pathnode.h b/src/include/optimizer/cypher_pathnode.h
index 79003ef..638268b 100644
--- a/src/include/optimizer/cypher_pathnode.h
+++ b/src/include/optimizer/cypher_pathnode.h
@@ -26,12 +26,15 @@
 #define CREATE_PATH_NAME "Cypher Create"
 #define SET_PATH_NAME "Cypher Set"
 #define DELETE_PATH_NAME "Cypher Delete"
+#define MERGE_PATH_NAME "Cypher Merge"
 
 CustomPath *create_cypher_create_path(PlannerInfo *root, RelOptInfo *rel,
                                       List *custom_private);
 CustomPath *create_cypher_set_path(PlannerInfo *root, RelOptInfo *rel,
                                    List *custom_private);
 CustomPath *create_cypher_delete_path(PlannerInfo *root, RelOptInfo *rel,
-                                   List *custom_private);
+                                      List *custom_private);
+CustomPath *create_cypher_merge_path(PlannerInfo *root, RelOptInfo *rel,
+                                     List *custom_private);
 
 #endif
diff --git a/src/include/utils/ag_func.h b/src/include/utils/ag_func.h
index 0167958..fe59b21 100644
--- a/src/include/utils/ag_func.h
+++ b/src/include/utils/ag_func.h
@@ -30,6 +30,7 @@
 #define CREATE_CLAUSE_FUNCTION_NAME "_cypher_create_clause"
 #define SET_CLAUSE_FUNCTION_NAME "_cypher_set_clause"
 #define DELETE_CLAUSE_FUNCTION_NAME "_cypher_delete_clause"
+#define MERGE_CLAUSE_FUNCTION_NAME "_cypher_merge_clause"
 
 bool is_oid_ag_func(Oid func_oid, const char *func_name);
 Oid get_ag_func_oid(const char *func_name, const int nargs, ...);