v20251215-0002-Edge-in-any-direction-bug.patch

text/x-patch

Filename: v20251215-0002-Edge-in-any-direction-bug.patch
Type: text/x-patch
Part: 0
Message: Re: SQL Property Graph Queries (SQL/PGQ)
From 469b2c0f42895ef21557ae13b097496332fdafef 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 v20251215 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.

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