From 6ca0e68c3973aabd1b381fe6d5332ea818314b0a Mon Sep 17 00:00:00 2001
From: AyoubKAZ <kazarayoub2004@gmail.com>
Date: Wed, 5 Aug 2026 16:04:04 +0200
Subject: [PATCH v1 4/4] Add support for label conjunction (&) in SQL/PGQ

The SQL/PGQ standard allows label expressions to use boolean operators,
such as conjunction of labels: MATCH (a IS label1 & label2). Previously, only
disjunction (|) was supported in graph element patterns.

This commit adds support for implicit label conjunction: a path factor appearing with different label expressions in multiple graph element patterns, which needs to be conjucted as if it was written with an explicit "&" which is not yet supported in grammar.

Author: Ayoub Kazar <ayoub.kazar@data-bene.io>
---
 src/backend/rewrite/rewriteGraphTable.c   | 96 ++++++++++++++++-------
 src/test/regress/expected/graph_table.out | 16 +++-
 src/test/regress/sql/graph_table.sql      |  4 +-
 3 files changed, 83 insertions(+), 33 deletions(-)

diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c
index 35404563bc3..a07ff860643 100644
--- a/src/backend/rewrite/rewriteGraphTable.c
+++ b/src/backend/rewrite/rewriteGraphTable.c
@@ -218,7 +218,7 @@ generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern)
 
 				/*
 				 * If both the element patterns have label expressions, they
-				 * need to be conjuncted, which is not supported right now.
+				 * need to be conjuncted.
 				 *
 				 * However, an empty label expression means all labels.
 				 * Conjunction of any label expression with all labels is the
@@ -231,10 +231,10 @@ generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern)
 					other->has_empty_labelexpr = gep->has_empty_labelexpr;
 				}
 				else if (!gep->has_empty_labelexpr && !equal(other->labelexpr, gep->labelexpr))
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("element patterns with same variable name \"%s\" but different label expressions are not supported",
-									gep->variable)));
+					other->labelexpr = (Node *) makeBoolExpr(AND_EXPR,
+															 list_make2(other->labelexpr,
+																		gep->labelexpr),
+															 -1);
 
 				/*
 				 * If two element patterns have the same variable name, they
@@ -870,49 +870,85 @@ get_path_elements_from_labelexpr(struct path_factor *pf, Node *labelexpr)
 	}
 	else if (IsA(labelexpr, BoolExpr))
 	{
-		BoolExpr   *be = castNode(BoolExpr, pf->labelexpr);
+		BoolExpr   *be = castNode(BoolExpr, labelexpr);
 		List	   *label_exprs = be->args;
 
-		/*
-		 * We only support label disjunction. So we just collect the distinct
-		 * elements merging element label OIDs of the elements with same OID.
-		 */
-		Assert(be->boolop == OR_EXPR);
+		Assert(be->boolop == OR_EXPR || be->boolop == AND_EXPR);
 
-		path_elements = NIL;
-		foreach_ptr(Node, label_expr, label_exprs)
+		if (be->boolop == OR_EXPR)
 		{
-			List	   *node_path_elements = get_path_elements_from_labelexpr(pf, label_expr);
-
-			if (path_elements == NIL)
-				path_elements = node_path_elements;
-			else
+			/*
+			 * Label disjunction: collect all distinct elements across all
+			 * sub-expressions, merging elem_label_oids for elements that
+			 * appear in more than one sub-expression.
+			 */
+			path_elements = NIL;
+			foreach_ptr(Node, label_expr, label_exprs)
 			{
-				foreach_ptr(struct path_element, npe, node_path_elements)
-				{
-					struct path_element *found = NULL;
+				List	   *node_path_elements = get_path_elements_from_labelexpr(pf, label_expr);
 
-					foreach_ptr(struct path_element, pe, path_elements)
+				if (path_elements == NIL)
+					path_elements = node_path_elements;
+				else
+				{
+					foreach_ptr(struct path_element, npe, node_path_elements)
 					{
-						if (npe->elemoid == pe->elemoid)
+						struct path_element *found = NULL;
+
+						foreach_ptr(struct path_element, pe, path_elements)
 						{
-							pe->elem_label_oids = list_concat(pe->elem_label_oids,
-															  npe->elem_label_oids);
-							found = pe;
-							break;
+							if (npe->elemoid == pe->elemoid)
+							{
+								pe->elem_label_oids = list_concat(pe->elem_label_oids,
+																  npe->elem_label_oids);
+								found = pe;
+								break;
+							}
 						}
+
+						if (!found)
+							path_elements = lappend(path_elements, npe);
 					}
+				}
+			}
+		}
+		else
+		{
+			List	   *left_elems;
+			List	   *right_elems;
+			List	   *intersection = NIL;
 
-					if (!found)
-						path_elements = lappend(path_elements, npe);
+			/*
+			 * Label conjunction (implicit, from same-variable element
+			 * patterns with different label expressions): recurse into each
+			 * child of the binary AND tree and keep only elements that appear
+			 * in both sides.  Merge elem_label_oids so that property
+			 * resolution can use labels from both sides of the conjunction.
+			 */
+			left_elems = get_path_elements_from_labelexpr(pf, linitial(label_exprs));
+			right_elems = get_path_elements_from_labelexpr(pf, lsecond(label_exprs));
+
+			foreach_ptr(struct path_element, pe, left_elems)
+			{
+				foreach_ptr(struct path_element, npe, right_elems)
+				{
+					if (pe->elemoid == npe->elemoid)
+					{
+						pe->elem_label_oids = list_concat(pe->elem_label_oids,
+														  npe->elem_label_oids);
+						intersection = lappend(intersection, pe);
+						break;
+					}
 				}
 			}
+			path_elements = intersection;
 		}
+
 	}
 	else
 	{
 		path_elements = NIL;	/* Keep compiler quiet */
-		elog(ERROR, "unsupported label expression node: %d", (int) nodeTag(pf->labelexpr));
+		elog(ERROR, "unsupported label expression node: %d", (int) nodeTag(labelexpr));
 	}
 
 	return path_elements;
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
index 082b509f95d..a0194424150 100644
--- a/src/test/regress/expected/graph_table.out
+++ b/src/test/regress/expected/graph_table.out
@@ -646,8 +646,20 @@ SELECT * FROM GRAPH_TABLE (g1 MATCH (a WHERE a.vprop1 between 20 and 2000)->(b W
 -- labels and elements kinds of element patterns with the same variable name
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS l1)-[a IS l1]->(b IS l1) COLUMNS (a.elname AS aename, b.elname AS bename)) ORDER BY 1, 2; -- error
 ERROR:  element patterns with same variable name "a" but different element pattern types
-SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a IS vl2) WHERE a.vname <> b.vname COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through;  -- error
-ERROR:  element patterns with same variable name "a" but different label expressions are not supported
+-- Implicit conjunction
+SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a IS vl2) WHERE a.vname <> b.vname COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through;
+ self | through | self_p1 | through_p1 
+------+---------+---------+------------
+(0 rows)
+
+SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl2)->(b)->(a IS vl3) COLUMNS (a.vname AS self, b.vname AS through, a.vprop2 AS vl2_prop, a.vprop1 AS vl3_prop)) ORDER BY self, through;
+ self | through | vl2_prop | vl3_prop 
+------+---------+----------+----------
+ v21  | v12     |     1100 |     1010
+ v22  | v32     |     1200 |     1020
+ v23  | v13     |     1300 |     1030
+(3 rows)
+
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through;
  self | through | self_p1 | through_p1 
 ------+---------+---------+------------
diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql
index e37ce9a4d3d..12eede01019 100644
--- a/src/test/regress/sql/graph_table.sql
+++ b/src/test/regress/sql/graph_table.sql
@@ -378,7 +378,9 @@ SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b WHERE b.vprop1 > 20)->(a WHERE a.vpr
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a WHERE a.vprop1 between 20 and 2000)->(b WHERE b.vprop1 > 20)->(a WHERE a.vprop1 between 20 and 2000) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through;
 -- labels and elements kinds of element patterns with the same variable name
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS l1)-[a IS l1]->(b IS l1) COLUMNS (a.elname AS aename, b.elname AS bename)) ORDER BY 1, 2; -- error
-SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a IS vl2) WHERE a.vname <> b.vname COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through;  -- error
+-- Implicit conjunction
+SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a IS vl2) WHERE a.vname <> b.vname COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through;
+SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl2)->(b)->(a IS vl3) COLUMNS (a.vname AS self, b.vname AS through, a.vprop2 AS vl2_prop, a.vprop1 AS vl3_prop)) ORDER BY self, through;
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through;
 SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b)->(a IS vl1) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through;
 
-- 
2.34.1

