From db93ea38da965fa2503ef528520076378da70cc7 Mon Sep 17 00:00:00 2001
From: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Date: Thu, 28 May 2026 02:45:58 +0530
Subject: [PATCH v20260601 6/7] Empty label expression in view definition

A view definition depends on all the graph labels that are explicitly
mentioned in the graph patterns in it so as to avoid it being rendered
invalid when any of the labels is dropped. But when a view definition
contains an empty label expression, we do not create any dependency
between the view and the labels that the empty label expression resolves
to. Resolve an empty label expression during transformation phase so
that we can create dependency between those labels and the view.

This will further help to avoid invalidation of a view containing
all-properties references when we support it.

An empty label expression may resolve to an empty set of labels if there
are not labels associated with the elements matching the kind of element
pattern containing the label expression. We do not have a syntax level
support for a label expression containing no labels. Hence we can not
dump a view containing such an empty label expression. Throw an error
when a query contains such an empty label expression. There are possibly
no real usecases which use such queries.

Author: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Discussion: https://www.postgresql.org/message-id/CAExHW5twGP5Zuk4Zch4kz8XDrSpckWQipMs=ysAj8GmqNa2FCQ@mail.gmail.com
---
 src/backend/parser/parse_graphtable.c         | 110 +++++++++++++++++-
 src/backend/rewrite/rewriteGraphTable.c       |  78 ++++---------
 src/include/nodes/parsenodes.h                |   8 ++
 .../expected/create_property_graph.out        |   1 +
 src/test/regress/expected/graph_table.out     |  29 ++++-
 .../regress/sql/create_property_graph.sql     |   1 +
 src/test/regress/sql/graph_table.sql          |  16 ++-
 7 files changed, 178 insertions(+), 65 deletions(-)

diff --git a/src/backend/parser/parse_graphtable.c b/src/backend/parser/parse_graphtable.c
index 73fbfb541f7..5c9da75c8cf 100644
--- a/src/backend/parser/parse_graphtable.c
+++ b/src/backend/parser/parse_graphtable.c
@@ -18,6 +18,8 @@
 #include "access/genam.h"
 #include "access/htup_details.h"
 #include "access/table.h"
+#include "catalog/pg_propgraph_element.h"
+#include "catalog/pg_propgraph_element_label.h"
 #include "catalog/pg_propgraph_label.h"
 #include "catalog/pg_propgraph_property.h"
 #include "miscadmin.h"
@@ -151,6 +153,51 @@ transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref)
 	return NULL;
 }
 
+/*
+ * Given the OID of a label and the kind of graph element pattern, return true if
+ * there exists at least one element matching the given kind associated with the
+ * label. Otherwise return false.
+ */
+static bool
+label_has_elements_of_kind(Oid labelid, GraphElementPatternKind gepkind)
+{
+	Relation	rel;
+	SysScanDesc scan;
+	ScanKeyData key[1];
+	HeapTuple	tup;
+	bool		result = false;
+
+	rel = table_open(PropgraphElementLabelRelationId, AccessShareLock);
+	ScanKeyInit(&key[0],
+				Anum_pg_propgraph_element_label_pgellabelid,
+				BTEqualStrategyNumber,
+				F_OIDEQ, ObjectIdGetDatum(labelid));
+	scan = systable_beginscan(rel, PropgraphElementLabelLabelIndexId,
+							  true, NULL, 1, key);
+	while (!result && HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_propgraph_element_label element_label = (Form_pg_propgraph_element_label) GETSTRUCT(tup);
+		Oid			element_oid = element_label->pgelelid;
+		HeapTuple	element_tup = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(element_oid));
+		Form_pg_propgraph_element element_form;
+
+		if (!HeapTupleIsValid(element_tup))
+			elog(ERROR, "cache lookup failed for property graph element %u", element_oid);
+
+		element_form = (Form_pg_propgraph_element) GETSTRUCT(element_tup);
+
+		if ((element_form->pgekind == PGEKIND_VERTEX && gepkind == VERTEX_PATTERN) ||
+			(element_form->pgekind == PGEKIND_EDGE && IS_EDGE_PATTERN(gepkind)))
+			result = true;
+
+		ReleaseSysCache(element_tup);
+	}
+
+	systable_endscan(scan);
+	table_close(rel, AccessShareLock);
+	return result;
+}
+
 /*
  * Transform a label expression.
  *
@@ -161,14 +208,66 @@ transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref)
  * GraphLabelRef nodes corresponding to the names of the labels appearing in the
  * expression. If any label name cannot be resolved to a label in the property
  * graph, an error is raised.
+ *
+ * An empty label expression is treated as a special case. According to section
+ * 9.2 "Contextual inference of a set of labels" subclause 2.a.ii of SQL/PGQ
+ * standard, element pattern which does not have a label expression is
+ * considered to have label expression equivalent to '%|!%' which is set of all
+ * labels which have at least one element of the given element kind associated with it.
  */
 static Node *
-transformLabelExpr(GraphTableParseState *gpstate, Node *labelexpr)
+transformLabelExpr(GraphTableParseState *gpstate, Node *labelexpr, GraphElementPatternKind gepkind)
 {
 	Node	   *result;
 
-	if (labelexpr == NULL)
-		return NULL;
+	if (!labelexpr)
+	{
+		Relation	rel;
+		SysScanDesc scan;
+		ScanKeyData key[1];
+		HeapTuple	tup;
+		List	   *args = NIL;
+
+		rel = table_open(PropgraphLabelRelationId, AccessShareLock);
+		ScanKeyInit(&key[0],
+					Anum_pg_propgraph_label_pglpgid,
+					BTEqualStrategyNumber,
+					F_OIDEQ, ObjectIdGetDatum(gpstate->graphid));
+		scan = systable_beginscan(rel, PropgraphLabelGraphNameIndexId,
+								  true, NULL, 1, key);
+		while (HeapTupleIsValid(tup = systable_getnext(scan)))
+		{
+			Form_pg_propgraph_label label = (Form_pg_propgraph_label) GETSTRUCT(tup);
+			GraphLabelRef *lref;
+
+			if (!label_has_elements_of_kind(label->oid, gepkind))
+				continue;
+
+			lref = makeNode(GraphLabelRef);
+			lref->labelid = label->oid;
+			lref->location = -1;
+			args = lappend(args, lref);
+		}
+		systable_endscan(scan);
+		table_close(rel, AccessShareLock);
+
+		/*
+		 * If there are no labels with elements of the given kind, the set of
+		 * labels that this label expression resolves to is empty. There is no
+		 * way to represent an empty set of labels as a label expression since
+		 * we do not support label conjunction as well as negation. So we can
+		 * not dump a view containing such a label expression. Hence prohibit
+		 * it for now.
+		 */
+		if (!args)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("empty label expression does not resolve to any label"));
+
+		result = (Node *) makeBoolExpr(OR_EXPR, args, -1);
+		return result;
+
+	}
 
 	check_stack_depth();
 
@@ -208,7 +307,7 @@ transformLabelExpr(GraphTableParseState *gpstate, Node *labelexpr)
 				{
 					Node	   *arg = (Node *) lfirst(lc);
 
-					arg = transformLabelExpr(gpstate, arg);
+					arg = transformLabelExpr(gpstate, arg, gepkind);
 					args = lappend(args, arg);
 				}
 
@@ -249,7 +348,8 @@ transformGraphElementPattern(ParseState *pstate, GraphElementPattern *gep)
 
 	gpstate->cur_gep = gep;
 
-	gep->labelexpr = transformLabelExpr(gpstate, gep->labelexpr);
+	gep->has_empty_labelexpr = !gep->labelexpr;
+	gep->labelexpr = transformLabelExpr(gpstate, gep->labelexpr, gep->kind);
 
 	gep->whereClause = transformExpr(pstate, gep->whereClause, EXPR_KIND_WHERE);
 
diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c
index 33d4e866d74..2292ca6dcee 100644
--- a/src/backend/rewrite/rewriteGraphTable.c
+++ b/src/backend/rewrite/rewriteGraphTable.c
@@ -58,6 +58,8 @@ struct path_factor
 {
 	GraphElementPatternKind kind;
 	const char *variable;
+	bool		has_empty_labelexpr;	/* Copied from the corresponding
+										 * GraphElementPattern */
 	Node	   *labelexpr;
 	Node	   *whereClause;
 	int			factorpos;		/* Position of this path factor in the list of
@@ -221,9 +223,12 @@ generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern)
 				 * expression itself. Hence if only one of the two element
 				 * patterns has a label expression use that expression.
 				 */
-				if (!other->labelexpr)
+				if (other->has_empty_labelexpr)
+				{
 					other->labelexpr = gep->labelexpr;
-				else if (gep->labelexpr && !equal(other->labelexpr, gep->labelexpr))
+					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",
@@ -250,6 +255,7 @@ generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern)
 			pf = palloc0_object(struct path_factor);
 			pf->factorpos = factorpos++;
 			pf->kind = gep->kind;
+			pf->has_empty_labelexpr = gep->has_empty_labelexpr;
 			pf->labelexpr = gep->labelexpr;
 			pf->variable = gep->variable;
 			pf->whereClause = gep->whereClause;
@@ -823,37 +829,9 @@ get_labels_for_expr(Oid propgraphid, Node *labelexpr)
 {
 	List	   *label_oids;
 
-	if (!labelexpr)
-	{
-		Relation	rel;
-		SysScanDesc scan;
-		ScanKeyData key[1];
-		HeapTuple	tup;
-
-		/*
-		 * According to section 9.2 "Contextual inference of a set of labels"
-		 * subclause 2.a.ii of SQL/PGQ standard, element pattern which does
-		 * not have a label expression is considered to have label expression
-		 * equivalent to '%|!%' which is set of all labels.
-		 */
-		label_oids = NIL;
-		rel = table_open(PropgraphLabelRelationId, AccessShareLock);
-		ScanKeyInit(&key[0],
-					Anum_pg_propgraph_label_pglpgid,
-					BTEqualStrategyNumber,
-					F_OIDEQ, ObjectIdGetDatum(propgraphid));
-		scan = systable_beginscan(rel, PropgraphLabelGraphNameIndexId,
-								  true, NULL, 1, key);
-		while (HeapTupleIsValid(tup = systable_getnext(scan)))
-		{
-			Form_pg_propgraph_label label = (Form_pg_propgraph_label) GETSTRUCT(tup);
+	Assert(labelexpr);
 
-			label_oids = lappend_oid(label_oids, label->oid);
-		}
-		systable_endscan(scan);
-		table_close(rel, AccessShareLock);
-	}
-	else if (IsA(labelexpr, GraphLabelRef))
+	if (IsA(labelexpr, GraphLabelRef))
 	{
 		GraphLabelRef *glr = castNode(GraphLabelRef, labelexpr);
 
@@ -903,7 +881,6 @@ get_path_elements_for_path_factor(Oid propgraphid, struct path_factor *pf)
 	List	   *elem_oids_seen = NIL;
 	List	   *pf_elem_oids = NIL;
 	List	   *path_elements = NIL;
-	List	   *unresolved_labels = NIL;
 	Relation	rel;
 	SysScanDesc scan;
 	ScanKeyData key[1];
@@ -970,33 +947,26 @@ get_path_elements_for_path_factor(Oid propgraphid, struct path_factor *pf)
 		{
 			/*
 			 * We did not find any qualified element associated with this
-			 * label. The label or its properties can not be associated with
-			 * the given element pattern. Throw an error if the label was
-			 * explicitly specified in the element pattern. Otherwise remember
-			 * it for later use.
+			 * label. Throw an error.
+			 *
+			 * An empty label expression is replaced by all labels that are
+			 * associated with at least one element of the required kind. We
+			 * should not reach here in that case.
 			 */
-			if (!pf->labelexpr)
-				unresolved_labels = lappend_oid(unresolved_labels, labeloid);
-			else
-				ereport(ERROR,
-						(errcode(ERRCODE_UNDEFINED_OBJECT),
-						 errmsg("no property graph element of type \"%s\" has label \"%s\" associated with it in property graph \"%s\"",
-								pf->kind == VERTEX_PATTERN ? "vertex" : "edge",
-								get_propgraph_label_name(labeloid),
-								get_rel_name(propgraphid))));
+			Assert(!pf->has_empty_labelexpr);
+
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("no property graph element of type \"%s\" has label \"%s\" associated with it in property graph \"%s\"",
+							pf->kind == VERTEX_PATTERN ? "vertex" : "edge",
+							get_propgraph_label_name(labeloid),
+							get_rel_name(propgraphid))));
 		}
 
 		systable_endscan(scan);
 	}
 	table_close(rel, AccessShareLock);
-
-	/*
-	 * Remove the labels which were not explicitly mentioned in the label
-	 * expression but do not have any qualified elements associated with them.
-	 * Properties associated with such labels may not be referenced. See
-	 * replace_property_refs_mutator() for more details.
-	 */
-	pf->labeloids = list_difference_oid(label_oids, unresolved_labels);
+	pf->labeloids = label_oids;
 
 	return path_elements;
 }
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 91377a6cde3..71040261375 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1048,7 +1048,15 @@ typedef struct GraphElementPattern
 	NodeTag		type;
 	GraphElementPatternKind kind;
 	const char *variable;
+
+	/*
+	 * If no label expression is specified, we will replace it with a non-NULL
+	 * expression in transformLabelExpr(). This flag indicates whether the
+	 * label expression was originally empty.
+	 */
+	bool		has_empty_labelexpr;
 	Node	   *labelexpr;
+
 	List	   *subexpr;
 	Node	   *whereClause;
 	List	   *quantifier;
diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out
index 65508223f54..a1a5501dd52 100644
--- a/src/test/regress/expected/create_property_graph.out
+++ b/src/test/regress/expected/create_property_graph.out
@@ -939,6 +939,7 @@ DETAIL:  Table "v2tmp" is a temporary table.
 DROP TABLE g2;  -- error: wrong object type
 ERROR:  "g2" is not a table
 HINT:  Use DROP PROPERTY GRAPH to remove a property graph.
+ALTER PROPERTY GRAPH g1 ADD VERTEX TABLES (t1 KEY (a)); -- to make a valid graph query
 CREATE VIEW vg1 AS SELECT * FROM GRAPH_TABLE(g1 MATCH () COLUMNS (1 AS one));
 DROP PROPERTY GRAPH g1; -- error
 ERROR:  cannot drop property graph g1 because other objects depend on it
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
index a5f9f0ac90a..cfee0626fdb 100644
--- a/src/test/regress/expected/graph_table.out
+++ b/src/test/regress/expected/graph_table.out
@@ -844,6 +844,8 @@ EXECUTE loopstmt;
 (2 rows)
 
 -- inheritance and partitioning
+--
+-- Also test temporary property graphs, keywords NODE and RELATIONSHIP
 CREATE TABLE pv (id int, val int);
 CREATE TABLE cv1 () INHERITS (pv);
 CREATE TABLE cv2 () INHERITS (pv);
@@ -873,7 +875,6 @@ SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe]->(d IS pv) COLUMNS (s.va
   30 | 300 |  10
 (3 rows)
 
--- temporary property graph
 CREATE TEMPORARY PROPERTY GRAPH gtmp
     VERTEX TABLES (
         pv KEY (id)
@@ -900,8 +901,12 @@ CREATE TABLE ptne1 PARTITION OF ptne FOR VALUES IN (1, 2);
 CREATE TABLE ptne2 PARTITION OF ptne FOR VALUES IN (3);
 INSERT INTO ptne VALUES (1, 1, 2, 100), (2, 2, 3, 200), (3, 3, 1, 300);
 CREATE PROPERTY GRAPH g4
-    VERTEX TABLES (ptnv)
-    EDGE TABLES (
+    VERTEX TABLES (ptnv);
+-- empty label expression which resolves to no labels
+SELECT * FROM GRAPH_TABLE (g4 MATCH (s is ptnv)-[e]-(d is ptnv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; -- error
+ERROR:  empty label expression does not resolve to any label
+ALTER PROPERTY GRAPH g4
+    ADD EDGE TABLES (
         ptne
             SOURCE KEY (src) REFERENCES ptnv(id)
             DESTINATION KEY (dest) REFERENCES ptnv(id)
@@ -957,6 +962,17 @@ ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE products
 ERROR:  cannot drop property price of property graph myshop because other objects depend on it
 DETAIL:  view customers_us depends on property price of property graph myshop
 HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- Empty label expression creates a dependency between the view and the set of
+-- labels it resolves to
+CREATE VIEW v_empty_label AS SELECT * FROM GRAPH_TABLE (g1 MATCH (v WHERE v.vprop1 = 10) COLUMNS (v.elname));
+ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v1 DROP LABEL vl1; -- error
+ERROR:  cannot drop label vl1 of property graph g1 because other objects depend on it
+DETAIL:  view v_empty_label depends on label vl1 of property graph g1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v2 DROP LABEL vl2; -- error
+ERROR:  cannot drop label vl2 of property graph g1 because other objects depend on it
+DETAIL:  view v_empty_label depends on label vl2 of property graph g1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
 -- ruleutils reverse parsing
 SELECT pg_get_viewdef('customers_us'::regclass);
                                                                                                                                                  pg_get_viewdef                                                                                                                                                 
@@ -970,6 +986,13 @@ SELECT pg_get_viewdef('customers_us'::regclass);
    ORDER BY g.customer_name, g.product_name;
 (1 row)
 
+SELECT pg_get_viewdef('v_empty_label'::regclass);
+                                              pg_get_viewdef                                              
+----------------------------------------------------------------------------------------------------------
+  SELECT elname                                                                                          +
+    FROM GRAPH_TABLE (g1 MATCH (v IS l1|vl1|vl2|vl3 WHERE (v.vprop1 = 10)) COLUMNS (v.elname AS elname));
+(1 row)
+
 -- test view/graph nesting
 CREATE VIEW customers_view AS SELECT customer_id, 'redacted' || customer_id AS name_redacted, address FROM customers;
 SELECT * FROM customers;
diff --git a/src/test/regress/sql/create_property_graph.sql b/src/test/regress/sql/create_property_graph.sql
index e8b3b3ff94c..0c3aa81e9ca 100644
--- a/src/test/regress/sql/create_property_graph.sql
+++ b/src/test/regress/sql/create_property_graph.sql
@@ -364,6 +364,7 @@ ALTER PROPERTY GRAPH g1
 -- DROP, ALTER SET SCHEMA, ALTER PROPERTY GRAPH RENAME TO
 
 DROP TABLE g2;  -- error: wrong object type
+ALTER PROPERTY GRAPH g1 ADD VERTEX TABLES (t1 KEY (a)); -- to make a valid graph query
 CREATE VIEW vg1 AS SELECT * FROM GRAPH_TABLE(g1 MATCH () COLUMNS (1 AS one));
 DROP PROPERTY GRAPH g1; -- error
 ALTER PROPERTY GRAPH g1 SET SCHEMA create_property_graph_tests_2;
diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql
index 80576b2f9f4..c4cb7999857 100644
--- a/src/test/regress/sql/graph_table.sql
+++ b/src/test/regress/sql/graph_table.sql
@@ -471,6 +471,8 @@ ALTER PROPERTY GRAPH g1 ALTER EDGE TABLE e3_3 ALTER LABEL l2 ADD PROPERTIES ((en
 EXECUTE loopstmt;
 
 -- inheritance and partitioning
+--
+-- Also test temporary property graphs, keywords NODE and RELATIONSHIP
 CREATE TABLE pv (id int, val int);
 CREATE TABLE cv1 () INHERITS (pv);
 CREATE TABLE cv2 () INHERITS (pv);
@@ -493,7 +495,6 @@ CREATE PROPERTY GRAPH g3
             DESTINATION KEY(dest) REFERENCES pv(id)
     );
 SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe]->(d IS pv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3;
--- temporary property graph
 CREATE TEMPORARY PROPERTY GRAPH gtmp
     VERTEX TABLES (
         pv KEY (id)
@@ -514,8 +515,11 @@ CREATE TABLE ptne1 PARTITION OF ptne FOR VALUES IN (1, 2);
 CREATE TABLE ptne2 PARTITION OF ptne FOR VALUES IN (3);
 INSERT INTO ptne VALUES (1, 1, 2, 100), (2, 2, 3, 200), (3, 3, 1, 300);
 CREATE PROPERTY GRAPH g4
-    VERTEX TABLES (ptnv)
-    EDGE TABLES (
+    VERTEX TABLES (ptnv);
+-- empty label expression which resolves to no labels
+SELECT * FROM GRAPH_TABLE (g4 MATCH (s is ptnv)-[e]-(d is ptnv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; -- error
+ALTER PROPERTY GRAPH g4
+    ADD EDGE TABLES (
         ptne
             SOURCE KEY (src) REFERENCES ptnv(id)
             DESTINATION KEY (dest) REFERENCES ptnv(id)
@@ -545,8 +549,14 @@ ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE customers
     ALTER LABEL customers DROP PROPERTIES (address);  -- error
 ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE products
     ALTER LABEL products DROP PROPERTIES (price);  -- error
+-- Empty label expression creates a dependency between the view and the set of
+-- labels it resolves to
+CREATE VIEW v_empty_label AS SELECT * FROM GRAPH_TABLE (g1 MATCH (v WHERE v.vprop1 = 10) COLUMNS (v.elname));
+ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v1 DROP LABEL vl1; -- error
+ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v2 DROP LABEL vl2; -- error
 -- ruleutils reverse parsing
 SELECT pg_get_viewdef('customers_us'::regclass);
+SELECT pg_get_viewdef('v_empty_label'::regclass);
 
 -- test view/graph nesting
 
-- 
2.34.1

