From 669ba54b976e8d4a572b98087fec90782171f8dd Mon Sep 17 00:00:00 2001
From: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Date: Mon, 15 Dec 2025 18:24:47 +0530
Subject: [PATCH v20251217 2/2] Edge in any direction bug

When an edge table connects vertices from the same table, an edge in any
direction pattern, e.g. ()-[]-(), was matching vertices only in one direction.
The qual for such a path pattern need to have two sets of quals, one for each
direction, disjuncted. The disjunction makes sure that an edge in either
direction is selected.

Also rebase on ecb553ae8211e3d135e0c9d42b90cc22be51d27c.

Author: Ashutosh Bapat
Reported-by: Peter Eisentraut
---
 src/backend/rewrite/rewriteGraphTable.c   | 77 ++++++++++++-----------
 src/test/regress/expected/graph_table.out | 30 ++++++++-
 src/test/regress/sql/graph_table.sql      |  8 ++-
 3 files changed, 78 insertions(+), 37 deletions(-)

diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c
index 06bcb982774..3b319639b81 100644
--- a/src/backend/rewrite/rewriteGraphTable.c
+++ b/src/backend/rewrite/rewriteGraphTable.c
@@ -276,9 +276,8 @@ generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern)
 		 *
 		 * Edge pointing in any direction is treated similar to that pointing
 		 * in right direction here.  When constructing a query in
-		 * generate_query_for_graph_path(), we will swap source and
-		 * destination elements if the edge element turns out to be an edge
-		 * pointing in the left direction.
+		 * generate_query_for_graph_path(), we will try links in both the
+		 * directions.
 		 *
 		 * If multiple edge patterns share the same variable name, they
 		 * constrain the adjacent vertex patterns since an edge can connect
@@ -426,8 +425,7 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 		{
 			struct path_element *src_pe;
 			struct path_element *dest_pe;
-			List	   *src_quals;
-			List	   *dest_quals;
+			Expr	   *edge_qual = NULL;
 
 			Assert(pf->src_pf && pf->dest_pf);
 			src_pe = list_nth(graph_path, pf->src_pf->factorpos);
@@ -437,41 +435,50 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 			Assert(pf->src_pf == src_pe->path_factor &&
 				   pf->dest_pf == dest_pe->path_factor);
 
+			if (src_pe->elemoid == pe->srcvertexid &&
+				dest_pe->elemoid == pe->destvertexid)
+				edge_qual = makeBoolExpr(AND_EXPR,
+										 list_concat(copyObject(pe->src_quals),
+													 copyObject(pe->dest_quals)),
+										 -1);
+
 			/*
-			 * If the given edge element does not connect the adjacent vertex
-			 * elements in this path, the path is broken. Abandon this path as
-			 * it won't return any rows.
-			 *
-			 * For an edge element pattern pointing in any direction, try
-			 * swapping the source and destination vertex elements.
+			 * An edge pattern in any direction matches edges in both
+			 * directions, try swapping source and destination. When the
+			 * source and destination is the same vertex table, quals
+			 * corresponding to either direction may get satisfied. Hence OR
+			 * the quals corresponding to both the directions.
 			 */
-			if (src_pe->elemoid != pe->srcvertexid ||
-				dest_pe->elemoid != pe->destvertexid)
+			if (pf->kind == EDGE_PATTERN_ANY &&
+				dest_pe->elemoid == pe->srcvertexid &&
+				src_pe->elemoid == pe->destvertexid)
 			{
-				if (pf->kind == EDGE_PATTERN_ANY &&
-					dest_pe->elemoid == pe->srcvertexid &&
-					src_pe->elemoid == pe->destvertexid)
-				{
-					dest_quals = copyObject(pe->src_quals);
-					src_quals = copyObject(pe->dest_quals);
-
-					/* Swap the source and destination varnos in the quals. */
-					ChangeVarNodes((Node *) dest_quals, pe->path_factor->src_pf->factorpos + 1,
-								   pe->path_factor->dest_pf->factorpos + 1, 0);
-					ChangeVarNodes((Node *) src_quals, pe->path_factor->dest_pf->factorpos + 1,
-								   pe->path_factor->src_pf->factorpos + 1, 0);
-				}
+				List	   *src_quals = copyObject(pe->dest_quals);
+				List	   *dest_quals = copyObject(pe->src_quals);
+				Expr	   *rev_edge_qual;
+
+				/* Swap the source and destination varnos in the quals. */
+				ChangeVarNodes((Node *) dest_quals, pe->path_factor->src_pf->factorpos + 1,
+							   pe->path_factor->dest_pf->factorpos + 1, 0);
+				ChangeVarNodes((Node *) src_quals, pe->path_factor->dest_pf->factorpos + 1,
+							   pe->path_factor->src_pf->factorpos + 1, 0);
+
+				rev_edge_qual = makeBoolExpr(AND_EXPR, list_concat(src_quals, dest_quals), -1);
+				if (edge_qual)
+					edge_qual = makeBoolExpr(OR_EXPR, list_make2(edge_qual, rev_edge_qual), -1);
 				else
-					return NULL;
-			}
-			else
-			{
-				src_quals = copyObject(pe->src_quals);
-				dest_quals = copyObject(pe->dest_quals);
+					edge_qual = rev_edge_qual;
 			}
 
-			qual_exprs = list_concat(qual_exprs, src_quals);
-			qual_exprs = list_concat(qual_exprs, dest_quals);
+			/*
+			 * If the given edge element does not connect the adjacent vertex
+			 * elements in this path, the path is broken. Abandon this path as
+			 * it won't return any rows.
+			 */
+			if (edge_qual == NULL)
+				return NULL;
+
+			qual_exprs = lappend(qual_exprs, edge_qual);
 		}
 		else
 			Assert(!pe->src_quals && !pe->dest_quals);
@@ -525,7 +532,7 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 	}
 
 	path_query->jointree = makeFromExpr(fromlist,
-										(Node *) makeBoolExpr(AND_EXPR, qual_exprs, -1));
+										qual_exprs ? (Node *) makeBoolExpr(AND_EXPR, qual_exprs, -1) : NULL);
 
 	/* Construct query targetlist from COLUMNS specification of GRAPH_TABLE. */
 	path_query->targetList = castNode(List,
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
index ee1424127ea..ecac6096720 100644
--- a/src/test/regress/expected/graph_table.out
+++ b/src/test/regress/expected/graph_table.out
@@ -553,7 +553,7 @@ ALTER PROPERTY GRAPH g1
     ADD EDGE TABLES (
         e3_3 KEY (src_id, dest_id)
             SOURCE KEY (src_id) REFERENCES v3 (id)
-            DESTINATION KEY (src_id) REFERENCES v3 (id)
+            DESTINATION KEY (dest_id) REFERENCES v3 (id)
             LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2)
             LABEL l1 PROPERTIES (ename AS elname)
     );
@@ -566,6 +566,19 @@ SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS se
 
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(c)-[b]->(d) COLUMNS (a.vname AS aname, b.ename AS bname, c.vname AS cname, d.vname AS dname)); --error
 ERROR:  An edge can not connect more than two vertexes even in a cyclic pattern.
+-- the looping edge should be reported only once even when edge pattern with any direction is used
+SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[c]-(a) COLUMNS (a.vname AS self, c.ename AS loop_name));
+ self | loop_name 
+------+-----------
+ v33  | e331
+(1 row)
+
+SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-(a) COLUMNS (a.vname AS self));
+ self 
+------
+ v33
+(1 row)
+
 -- test collation specified in the expression
 INSERT INTO e3_3 VALUES (2003, 2003, 'E331', 10011);
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)) ORDER BY loop_name COLLATE "C" ASC;
@@ -802,6 +815,21 @@ SELECT * FROM GRAPH_TABLE (g4 MATCH (s IS ptnv)-[e IS ptne]->(d IS ptnv) COLUMNS
   30 | 300 |  10
 (3 rows)
 
+-- edges from the same vertex in both directions connecting to other vertexes in the same table
+SELECT * FROM GRAPH_TABLE (g4 MATCH (s)-[e]-(d) WHERE s.id = 3 COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3;
+ val | val | val 
+-----+-----+-----
+  30 | 200 |  20
+  30 | 300 |  10
+(2 rows)
+
+SELECT * FROM GRAPH_TABLE (g4 MATCH (s WHERE s.id = 3)-[e]-(d) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3;
+ val | val | val 
+-----+-----+-----
+  30 | 200 |  20
+  30 | 300 |  10
+(2 rows)
+
 CREATE VIEW customers_us AS SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));
 SELECT pg_get_viewdef('customers_us'::regclass);
                                                                           pg_get_viewdef                                                                           
diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql
index afafeb2efa9..412da394aab 100644
--- a/src/test/regress/sql/graph_table.sql
+++ b/src/test/regress/sql/graph_table.sql
@@ -356,7 +356,7 @@ ALTER PROPERTY GRAPH g1
     ADD EDGE TABLES (
         e3_3 KEY (src_id, dest_id)
             SOURCE KEY (src_id) REFERENCES v3 (id)
-            DESTINATION KEY (src_id) REFERENCES v3 (id)
+            DESTINATION KEY (dest_id) REFERENCES v3 (id)
             LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2)
             LABEL l1 PROPERTIES (ename AS elname)
     );
@@ -364,6 +364,9 @@ ALTER PROPERTY GRAPH g1
 INSERT INTO e3_3 VALUES (2003, 2003, 'e331', 10010);
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name));
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(c)-[b]->(d) COLUMNS (a.vname AS aname, b.ename AS bname, c.vname AS cname, d.vname AS dname)); --error
+-- the looping edge should be reported only once even when edge pattern with any direction is used
+SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[c]-(a) COLUMNS (a.vname AS self, c.ename AS loop_name));
+SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-(a) COLUMNS (a.vname AS self));
 
 -- test collation specified in the expression
 INSERT INTO e3_3 VALUES (2003, 2003, 'E331', 10011);
@@ -480,6 +483,9 @@ CREATE PROPERTY GRAPH g4
             DESTINATION KEY (dest) REFERENCES ptnv(id)
     );
 SELECT * FROM GRAPH_TABLE (g4 MATCH (s IS ptnv)-[e IS ptne]->(d IS ptnv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3;
+-- edges from the same vertex in both directions connecting to other vertexes in the same table
+SELECT * FROM GRAPH_TABLE (g4 MATCH (s)-[e]-(d) WHERE s.id = 3 COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3;
+SELECT * FROM GRAPH_TABLE (g4 MATCH (s WHERE s.id = 3)-[e]-(d) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3;
 
 CREATE VIEW customers_us AS SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));
 
-- 
2.34.1

