diff --git a/README.md b/README.md
index 28979e1f7..c6e14e4d8 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
-
+
@@ -56,7 +56,7 @@
-
What is Apache AGE?
+
What is Apache AGE?
[Apache AGE](https://age.apache.org/#) is an extension for PostgreSQL that enables users to leverage a graph database on top of the existing relational databases. AGE is an acronym for A Graph Extension and is inspired by Bitnine's AgensGraph, a multi-model database fork of PostgreSQL. The basic principle of the project is to create a single storage that handles both the relational and graph data model so that the users can use the standard ANSI SQL along with openCypher, one of the most popular graph query languages today.
@@ -134,7 +134,7 @@ Apache AGE is intended to be simple to install and run. It can be installed with
You will need to install an AGE compatible version of Postgres, for now AGE supports Postgres 11, 12 & 13. Supporting the latest versions is on AGE roadmap.
- Install From Package Manager
+ Installation via Package Manager
You can use a package management that your OS provides to download AGE.
@@ -146,7 +146,7 @@ sudo apt install postgresql
```
- Install From Source Code
+ Installation From Source Code
You can download the Postgres source code and install your own instance of Postgres. You can read instructions on how to install from source code for different versions on the official Postgres Website.
@@ -156,7 +156,7 @@ You can download the Postgres

Install AGE on Linux and MacOS
-Clone the github repository or download thedownload an official release.
+Clone the github repository or download the download an official release.
Run the pg_config utility and check the version of PostgreSQL. Currently, only PostgreSQL versions 11, 12 & 13 are supported. If you have any other version of Postgres, you will need to install PostgreSQL version 11, 12 or 13.
@@ -332,7 +332,7 @@ Starting with Apache AGE is very simple. You can easily select your platform and
Join the AGE community for help, questions, discussions, and contributions.
- Check our [website](https://age.apache.org/)
-- Chat live with us on [Discord](https://discord.com/invite/NMsBs9X8Ss/)
+- Join the Live Chat on [Discord](https://discord.com/invite/NMsBs9X8Ss/)
- Follow us on [Twitter](https://twitter.com/apache_age?s=20&t=7Hu8Txk4vjvuEp-ryakacg)
- Subscribe to our developer mailing list by sending an email to dev-subscribe@age.apache.org
- Subscribe to our user mailing list by sending an email to users-subscribe@age.apache.org
diff --git a/regress/expected/cypher_merge.out b/regress/expected/cypher_merge.out
index 9699794ec..ecfc0c1d1 100644
--- a/regress/expected/cypher_merge.out
+++ b/regress/expected/cypher_merge.out
@@ -901,6 +901,53 @@ SELECT * FROM cypher('cypher_merge', $$ MATCH (n:node) RETURN n $$) AS (n agtype
{"id": 2533274790395907, "label": "node", "properties": {"age": 23, "name": "Lisa", "gender": "Female"}}::vertex
(2 rows)
+--
+-- Complex MERGE w/wo RETURN values
+--
+-- These should each create a path, if it doesn't already exist.
+-- TODO Until the issue with variable reuse of 'x' in MERGE is corrected,
+-- these commands will each create a new path.
+SELECT * FROM cypher('cypher_merge', $$ MERGE ()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) $$) AS (x agtype);
+ x
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_merge', $$ MERGE ()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) RETURN x $$) AS (x agtype);
+ x
+------------------------------------------------------------------
+ {"id": 3096224743817220, "label": "C", "properties": {}}::vertex
+(1 row)
+
+SELECT * FROM cypher('cypher_merge', $$ MERGE p=()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) $$) AS (p agtype);
+ p
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_merge', $$ MERGE p=()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) RETURN p $$) AS (p agtype);
+ p
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710708, "label": "", "properties": {}}::vertex, {"id": 2814749767106564, "label": "B", "end_id": 3096224743817223, "start_id": 281474976710708, "properties": {}}::edge, {"id": 3096224743817223, "label": "C", "properties": {}}::vertex, {"id": 3377699720527876, "label": "E", "end_id": 3096224743817224, "start_id": 3096224743817223, "properties": {}}::edge, {"id": 3096224743817224, "label": "C", "properties": {}}::vertex, {"id": 3659174697238532, "label": "F", "end_id": 3096224743817224, "start_id": 3940649673949188, "properties": {}}::edge, {"id": 3940649673949188, "label": "I", "properties": {}}::vertex]::path
+(1 row)
+
+SELECT * FROM cypher('cypher_merge', $$ MERGE p=()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) RETURN p $$) AS (p agtype);
+ p
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710709, "label": "", "properties": {}}::vertex, {"id": 2814749767106565, "label": "B", "end_id": 3096224743817225, "start_id": 281474976710709, "properties": {}}::edge, {"id": 3096224743817225, "label": "C", "properties": {}}::vertex, {"id": 3377699720527877, "label": "E", "end_id": 3096224743817226, "start_id": 3096224743817225, "properties": {}}::edge, {"id": 3096224743817226, "label": "C", "properties": {}}::vertex, {"id": 3659174697238533, "label": "F", "end_id": 3096224743817226, "start_id": 3940649673949189, "properties": {}}::edge, {"id": 3940649673949189, "label": "I", "properties": {}}::vertex]::path
+(1 row)
+
+-- TODO This should only return 1 row, as the path should already exist.
+-- However, we need to fix the variable reuse in MERGE. Until then,
+-- this will always return 5 rows due to 'x' above not being the same node.
+SELECT * FROM cypher('cypher_merge', $$ MATCH p=()-[:B]->(:C)-[:E]->(:C)<-[:F]-(:I) RETURN p $$) AS (p agtype);
+ p
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710705, "label": "", "properties": {}}::vertex, {"id": 2814749767106561, "label": "B", "end_id": 3096224743817217, "start_id": 281474976710705, "properties": {}}::edge, {"id": 3096224743817217, "label": "C", "properties": {}}::vertex, {"id": 3377699720527873, "label": "E", "end_id": 3096224743817218, "start_id": 3096224743817217, "properties": {}}::edge, {"id": 3096224743817218, "label": "C", "properties": {}}::vertex, {"id": 3659174697238529, "label": "F", "end_id": 3096224743817218, "start_id": 3940649673949185, "properties": {}}::edge, {"id": 3940649673949185, "label": "I", "properties": {}}::vertex]::path
+ [{"id": 281474976710706, "label": "", "properties": {}}::vertex, {"id": 2814749767106562, "label": "B", "end_id": 3096224743817219, "start_id": 281474976710706, "properties": {}}::edge, {"id": 3096224743817219, "label": "C", "properties": {}}::vertex, {"id": 3377699720527874, "label": "E", "end_id": 3096224743817220, "start_id": 3096224743817219, "properties": {}}::edge, {"id": 3096224743817220, "label": "C", "properties": {}}::vertex, {"id": 3659174697238530, "label": "F", "end_id": 3096224743817220, "start_id": 3940649673949186, "properties": {}}::edge, {"id": 3940649673949186, "label": "I", "properties": {}}::vertex]::path
+ [{"id": 281474976710707, "label": "", "properties": {}}::vertex, {"id": 2814749767106563, "label": "B", "end_id": 3096224743817221, "start_id": 281474976710707, "properties": {}}::edge, {"id": 3096224743817221, "label": "C", "properties": {}}::vertex, {"id": 3377699720527875, "label": "E", "end_id": 3096224743817222, "start_id": 3096224743817221, "properties": {}}::edge, {"id": 3096224743817222, "label": "C", "properties": {}}::vertex, {"id": 3659174697238531, "label": "F", "end_id": 3096224743817222, "start_id": 3940649673949187, "properties": {}}::edge, {"id": 3940649673949187, "label": "I", "properties": {}}::vertex]::path
+ [{"id": 281474976710708, "label": "", "properties": {}}::vertex, {"id": 2814749767106564, "label": "B", "end_id": 3096224743817223, "start_id": 281474976710708, "properties": {}}::edge, {"id": 3096224743817223, "label": "C", "properties": {}}::vertex, {"id": 3377699720527876, "label": "E", "end_id": 3096224743817224, "start_id": 3096224743817223, "properties": {}}::edge, {"id": 3096224743817224, "label": "C", "properties": {}}::vertex, {"id": 3659174697238532, "label": "F", "end_id": 3096224743817224, "start_id": 3940649673949188, "properties": {}}::edge, {"id": 3940649673949188, "label": "I", "properties": {}}::vertex]::path
+ [{"id": 281474976710709, "label": "", "properties": {}}::vertex, {"id": 2814749767106565, "label": "B", "end_id": 3096224743817225, "start_id": 281474976710709, "properties": {}}::edge, {"id": 3096224743817225, "label": "C", "properties": {}}::vertex, {"id": 3377699720527877, "label": "E", "end_id": 3096224743817226, "start_id": 3096224743817225, "properties": {}}::edge, {"id": 3096224743817226, "label": "C", "properties": {}}::vertex, {"id": 3659174697238533, "label": "F", "end_id": 3096224743817226, "start_id": 3940649673949189, "properties": {}}::edge, {"id": 3940649673949189, "label": "I", "properties": {}}::vertex]::path
+(5 rows)
+
--clean up
SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
a
@@ -911,7 +958,7 @@ SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtyp
* Clean up graph
*/
SELECT drop_graph('cypher_merge', true);
-NOTICE: drop cascades to 9 other objects
+NOTICE: drop cascades to 14 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
@@ -921,6 +968,11 @@ drop cascades to table cypher_merge."Person"
drop cascades to table cypher_merge."City"
drop cascades to table cypher_merge."BORN_IN"
drop cascades to table cypher_merge.node
+drop cascades to table cypher_merge."B"
+drop cascades to table cypher_merge."C"
+drop cascades to table cypher_merge."E"
+drop cascades to table cypher_merge."F"
+drop cascades to table cypher_merge."I"
NOTICE: graph "cypher_merge" has been dropped
drop_graph
------------
diff --git a/regress/sql/cypher_merge.sql b/regress/sql/cypher_merge.sql
index 8c127f637..1fe3ed707 100644
--- a/regress/sql/cypher_merge.sql
+++ b/regress/sql/cypher_merge.sql
@@ -479,6 +479,22 @@ SELECT * FROM cypher('cypher_merge', $$ MATCH (n:node) RETURN n $$) AS (n agtype
SELECT * FROM cypher('cypher_merge', $$ MERGE (n:node {name: 'Jason'}) SET n.name = 'Lisa', n.age = 23, n.gender = 'Female' RETURN n $$) AS (n agtype);
SELECT * FROM cypher('cypher_merge', $$ MATCH (n:node) RETURN n $$) AS (n agtype);
+--
+-- Complex MERGE w/wo RETURN values
+--
+-- These should each create a path, if it doesn't already exist.
+-- TODO Until the issue with variable reuse of 'x' in MERGE is corrected,
+-- these commands will each create a new path.
+SELECT * FROM cypher('cypher_merge', $$ MERGE ()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) $$) AS (x agtype);
+SELECT * FROM cypher('cypher_merge', $$ MERGE ()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) RETURN x $$) AS (x agtype);
+SELECT * FROM cypher('cypher_merge', $$ MERGE p=()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) $$) AS (p agtype);
+SELECT * FROM cypher('cypher_merge', $$ MERGE p=()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) RETURN p $$) AS (p agtype);
+SELECT * FROM cypher('cypher_merge', $$ MERGE p=()-[:B]->(x:C)-[:E]->(x:C)<-[f:F]-(y:I) RETURN p $$) AS (p agtype);
+-- TODO This should only return 1 row, as the path should already exist.
+-- However, we need to fix the variable reuse in MERGE. Until then,
+-- this will always return 5 rows due to 'x' above not being the same node.
+SELECT * FROM cypher('cypher_merge', $$ MATCH p=()-[:B]->(:C)-[:E]->(:C)<-[:F]-(:I) RETURN p $$) AS (p agtype);
+
--clean up
SELECT * FROM cypher('cypher_merge', $$MATCH (n) DETACH DELETE n $$) AS (a agtype);
diff --git a/src/backend/executor/cypher_merge.c b/src/backend/executor/cypher_merge.c
index b0247d01f..b42408b75 100644
--- a/src/backend/executor/cypher_merge.c
+++ b/src/backend/executor/cypher_merge.c
@@ -230,11 +230,27 @@ static void process_path(cypher_merge_custom_scan_state *css)
ExprContext *econtext = css->css.ss.ps.ps_ExprContext;
TupleTableSlot *scantuple = econtext->ecxt_scantuple;
Datum result;
+ int tuple_position = path->path_attr_num - 1;
+ bool debug_flag = false;
- result = make_path(css->path_values);
+ /*
+ * We need to make sure that the tuple_position is within the
+ * boundaries of the tuple's number of attributes. Otherwise, it
+ * will corrupt memory. The cases where it doesn't fit within are
+ * usually due to a variable that is specified but there isn't a RETURN
+ * clause. In these cases we just don't bother to store the
+ * value.
+ */
+ if (!debug_flag &&
+ (tuple_position < scantuple->tts_tupleDescriptor->natts ||
+ scantuple->tts_tupleDescriptor->natts != 1))
+ {
+ result = make_path(css->path_values);
- scantuple->tts_values[path->path_attr_num - 1] = result;
- scantuple->tts_isnull[path->path_attr_num - 1] = false;
+ /* store the result */
+ scantuple->tts_values[tuple_position] = result;
+ scantuple->tts_isnull[tuple_position] = false;
+ }
}
}
@@ -739,25 +755,25 @@ static Datum merge_vertex(cypher_merge_custom_scan_state *css,
*/
if (CYPHER_TARGET_NODE_IS_VARIABLE(node->flags))
{
- bool debug = false;
+ bool debug_flag = false;
int tuple_position = node->tuple_position - 1;
/*
- * Generate an error message if the tuple position is
- * out-of-bounds and allow for debugging.
+ * We need to make sure that the tuple_position is within the
+ * boundaries of the tuple's number of attributes. Otherwise, it
+ * will corrupt memory. The cases where it doesn't fall within
+ * are usually due to a variable that is specified but there
+ * isn't a RETURN clause. In these cases we just don't bother to
+ * store the value.
*/
- if (!debug && (tuple_position >=
- scanTupleSlot->tts_tupleDescriptor->natts))
+ if (!debug_flag &&
+ (tuple_position < scanTupleSlot->tts_tupleDescriptor->natts ||
+ scanTupleSlot->tts_tupleDescriptor->natts != 1))
{
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("merge_vertex: invalid tuple position")));
-
+ /* store the result */
+ scanTupleSlot->tts_values[tuple_position] = result;
+ scanTupleSlot->tts_isnull[tuple_position] = false;
}
-
- /* store the result */
- scanTupleSlot->tts_values[tuple_position] = result;
- scanTupleSlot->tts_isnull[tuple_position] = false;
}
}
}
@@ -955,25 +971,25 @@ static void merge_edge(cypher_merge_custom_scan_state *css,
if (CYPHER_TARGET_NODE_IS_VARIABLE(node->flags))
{
TupleTableSlot *scantuple = econtext->ecxt_scantuple;
- bool debug = false;
+ bool debug_flag = false;
int tuple_position = node->tuple_position - 1;
/*
- * Generate an error message if the tuple position is
- * out-of-bounds and allow for debugging.
+ * We need to make sure that the tuple_position is within the
+ * boundaries of the tuple's number of attributes. Otherwise, it
+ * will corrupt memory. The cases where it doesn't fall within are
+ * usually due to a variable that is specified but there isn't a
+ * RETURN clause. In these cases we just don't bother to store the
+ * value.
*/
- if (!debug && (tuple_position >=
- scantuple->tts_tupleDescriptor->natts))
+ if (!debug_flag &&
+ (tuple_position < scantuple->tts_tupleDescriptor->natts ||
+ scantuple->tts_tupleDescriptor->natts != 1))
{
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("merge_edge: invalid tuple position")));
-
+ /* store the result */
+ scantuple->tts_values[tuple_position] = result;
+ scantuple->tts_isnull[tuple_position] = false;
}
-
- /* store the result */
- scantuple->tts_values[tuple_position] = result;
- scantuple->tts_isnull[tuple_position] = false;
}
}
}