From 177842b956b716af67c4f2acd9381f29371f9386 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Wed, 24 Dec 2025 23:44:00 +0900
Subject: [PATCH] SQL/PGQ: Add LABELS() graph element function

Implement LABELS() function that returns all labels of a graph element
as text[]. This follows the SQL/PGQ standard for graph element functions.

Implementation uses a subquery structure that wraps each element table
with a virtual __labels__ column containing the element's label array.
This design enables the query optimizer to:

- Prune Append branches when filtering by specific labels
  (e.g., WHERE 'Person' = ANY(LABELS(v)) scans only Person table)
- Constant-fold label arrays for single-label elements
- Eliminate scans entirely for non-matching label filters

Key changes:
- Add GraphLabelsRef node type for LABELS() in parse tree
- Transform LABELS(var) to Var referencing __labels__ column
- Wrap element tables in subqueries with __labels__ column
- Add optimizer pruning tests for shared labels

Known limitation:
Column-level privileges are not properly checked when accessing
properties through GRAPH_TABLE with this subquery structure.
This needs further investigation.

Test status: graph_table (PASS), graph_table_rls (PASS),
             privileges (FAIL - column-level permission bypass)
---
 src/backend/nodes/nodeFuncs.c             |   9 +
 src/backend/parser/parse_collate.c        |   1 +
 src/backend/parser/parse_expr.c           |   6 +
 src/backend/parser/parse_graphtable.c     |  64 +++++++
 src/backend/rewrite/rewriteGraphTable.c   | 212 +++++++++++++++++++---
 src/backend/utils/adt/ruleutils.c         |   8 +
 src/include/nodes/primnodes.h             |  10 +
 src/include/parser/parse_graphtable.h     |   2 +
 src/test/regress/expected/graph_table.out | 211 +++++++++++++++++++++
 src/test/regress/sql/graph_table.sql      |  98 ++++++++++
 10 files changed, 597 insertions(+), 24 deletions(-)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 907808b09b8..a004be4380d 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -287,6 +287,9 @@ exprType(const Node *expr)
 		case T_GraphPropertyRef:
 			type = ((const GraphPropertyRef *) expr)->typeId;
 			break;
+		case T_GraphLabelsRef:
+			type = TEXTARRAYOID;
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
 			type = InvalidOid;	/* keep compiler quiet */
@@ -541,6 +544,8 @@ exprTypmod(const Node *expr)
 			return exprTypmod((Node *) ((const PlaceHolderVar *) expr)->phexpr);
 		case T_GraphPropertyRef:
 			return ((const GraphPropertyRef *) expr)->typmod;
+		case T_GraphLabelsRef:
+			return -1;
 		default:
 			break;
 	}
@@ -1066,6 +1071,9 @@ exprCollation(const Node *expr)
 		case T_GraphPropertyRef:
 			coll = ((const GraphPropertyRef *) expr)->collation;
 			break;
+		case T_GraphLabelsRef:
+			coll = DEFAULT_COLLATION_OID;
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
 			coll = InvalidOid;	/* keep compiler quiet */
@@ -2133,6 +2141,7 @@ expression_tree_walker_impl(Node *node,
 		case T_SortGroupClause:
 		case T_CTESearchClause:
 		case T_GraphPropertyRef:
+		case T_GraphLabelsRef:
 		case T_MergeSupportFunc:
 			/* primitive node types with no expression subnodes */
 			break;
diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c
index 8f912065a01..9aaf01bf58b 100644
--- a/src/backend/parser/parse_collate.c
+++ b/src/backend/parser/parse_collate.c
@@ -547,6 +547,7 @@ assign_collations_walker(Node *node, assign_collations_context *context)
 		case T_SetToDefault:
 		case T_CurrentOfExpr:
 		case T_GraphPropertyRef:
+		case T_GraphLabelsRef:
 
 			/*
 			 * General case for childless expression nodes.  These should
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 7efa776cb9b..594f0419f63 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1448,6 +1448,12 @@ transformFuncCall(ParseState *pstate, FuncCall *fn)
 	Node	   *last_srf = pstate->p_last_srf;
 	List	   *targs;
 	ListCell   *args;
+	Node	   *result;
+
+	/* Check for graph functions like LABELS() in GRAPH_TABLE context */
+	result = transformGraphTableFuncCall(pstate, fn);
+	if (result != NULL)
+		return result;
 
 	/* Transform the list of arguments ... */
 	targs = NIL;
diff --git a/src/backend/parser/parse_graphtable.c b/src/backend/parser/parse_graphtable.c
index a8769a67b6a..5bd71eae536 100644
--- a/src/backend/parser/parse_graphtable.c
+++ b/src/backend/parser/parse_graphtable.c
@@ -82,6 +82,70 @@ transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref)
 	return NULL;
 }
 
+/*
+ * Transform a graph function call like LABELS(v) inside GRAPH_TABLE.
+ * Returns NULL if this is not a recognized graph function.
+ */
+Node *
+transformGraphTableFuncCall(ParseState *pstate, FuncCall *fn)
+{
+	GraphTableParseState *gpstate = pstate->p_graph_table_pstate;
+	char	   *funcname;
+
+	if (!gpstate)
+		return NULL;
+
+	if (list_length(fn->funcname) != 1)
+		return NULL;
+
+	funcname = strVal(linitial(fn->funcname));
+
+	if (pg_strcasecmp(funcname, "labels") == 0)
+	{
+		Node	   *arg;
+		ColumnRef  *cref;
+		char	   *elvarname;
+		GraphLabelsRef *glr;
+
+		if (list_length(fn->args) != 1)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("LABELS() requires exactly one argument"),
+					parser_errposition(pstate, fn->location));
+
+		arg = linitial(fn->args);
+
+		if (!IsA(arg, ColumnRef))
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("LABELS() argument must be an element variable"),
+					parser_errposition(pstate, fn->location));
+
+		cref = (ColumnRef *) arg;
+		if (list_length(cref->fields) != 1)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("LABELS() argument must be an element variable"),
+					parser_errposition(pstate, cref->location));
+
+		elvarname = strVal(linitial(cref->fields));
+
+		if (!list_member(gpstate->variables, linitial(cref->fields)))
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_COLUMN),
+					errmsg("element variable \"%s\" does not exist", elvarname),
+					parser_errposition(pstate, cref->location));
+
+		glr = makeNode(GraphLabelsRef);
+		glr->elvarname = pstrdup(elvarname);
+		glr->location = fn->location;
+
+		return (Node *) glr;
+	}
+
+	return NULL;
+}
+
 /*
  * Transform a label expression.
  */
diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c
index 3b319639b81..aab2ed1769f 100644
--- a/src/backend/rewrite/rewriteGraphTable.c
+++ b/src/backend/rewrite/rewriteGraphTable.c
@@ -15,6 +15,7 @@
 
 #include "access/table.h"
 #include "access/htup_details.h"
+#include "catalog/pg_collation.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_propgraph_element.h"
 #include "catalog/pg_propgraph_element_label.h"
@@ -38,8 +39,10 @@
 #include "rewrite/rewriteManip.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
+#include "utils/rel.h"
 #include "utils/ruleutils.h"
 #include "utils/syscache.h"
 
@@ -86,12 +89,16 @@ struct path_element
 	/* Source and destination conditions for an edge element. */
 	List	   *src_quals;
 	List	   *dest_quals;
+	/* Attribute number of __labels__ column in subquery RTE */
+	AttrNumber	labels_attnum;
 };
 
 static Node *replace_property_refs(Oid propgraphid, Node *node, const List *mappings);
 static List *build_edge_vertex_link_quals(HeapTuple edgetup, int edgerti, int refrti, Oid refid, AttrNumber catalog_key_attnum, AttrNumber catalog_ref_attnum, AttrNumber catalog_eqop_attnum);
 static List *generate_queries_for_path_pattern(RangeTblEntry *rte, List *element_patterns);
 static Query *generate_query_for_graph_path(RangeTblEntry *rte, List *path);
+static Const *build_labels_const(Oid elemoid);
+static RangeTblEntry *create_element_subquery_rte(struct path_element *pe, Oid reloid, AttrNumber *labels_attnum);
 static Node *generate_setop_from_pathqueries(List *pathqueries, List **rtable, List **targetlist);
 static List *generate_queries_for_path_pattern_recurse(RangeTblEntry *rte, List *pathqueries, List *cur_path, List *path_pattern_lists, int elempos);
 static Query *generate_query_for_empty_path_pattern(RangeTblEntry *rte);
@@ -407,7 +414,6 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 	Query	   *path_query = makeNode(Query);
 	List	   *fromlist = NIL;
 	List	   *qual_exprs = NIL;
-	List	   *vars;
 
 	path_query->commandType = CMD_SELECT;
 
@@ -415,8 +421,8 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 	{
 		struct path_factor *pf = pe->path_factor;
 		RangeTblRef *rtr;
-		Relation	rel;
-		ParseNamespaceItem *pni;
+		RangeTblEntry *subrte;
+		AttrNumber	labels_attnum;
 
 		Assert(pf->kind == VERTEX_PATTERN || IS_EDGE_PATTERN(pf->kind));
 
@@ -484,7 +490,11 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 			Assert(!pe->src_quals && !pe->dest_quals);
 
 		/*
-		 * Create RangeTblEntry for this element table.
+		 * Create RangeTblEntry for this element table wrapped in a subquery.
+		 *
+		 * The subquery adds a virtual __labels__ column containing the
+		 * element's labels, allowing LABELS() to return a Var that can be
+		 * optimized by the query planner.
 		 *
 		 * SQL/PGQ standard (Ref. Section 11.19, Access rule 2 and General
 		 * rule 4) does not specify whose access privileges to use when
@@ -493,13 +503,9 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 		 * make property graphs as a hole for unpriviledged data access. This
 		 * is inline with the views being security_invoker by default.
 		 */
-		rel = table_open(pe->reloid, AccessShareLock);
-		pni = addRangeTableEntryForRelation(make_parsestate(NULL), rel, AccessShareLock,
-											NULL, true, false);
-		table_close(rel, NoLock);
-		path_query->rtable = lappend(path_query->rtable, pni->p_rte);
-		path_query->rteperminfos = lappend(path_query->rteperminfos, pni->p_perminfo);
-		pni->p_rte->perminfoindex = list_length(path_query->rteperminfos);
+		subrte = create_element_subquery_rte(pe, pe->reloid, &labels_attnum);
+		pe->labels_attnum = labels_attnum;
+		path_query->rtable = lappend(path_query->rtable, subrte);
 		rtr = makeNode(RangeTblRef);
 		rtr->rtindex = list_length(path_query->rtable);
 		fromlist = lappend(fromlist, rtr);
@@ -541,20 +547,15 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 															graph_path));
 
 	/*
-	 * Mark the columns being accessed in the path query as requiring SELECT
-	 * privilege. Any lateral columns should have been handled when the
-	 * corresponding ColumnRefs were transformed. Ignore those here.
+	 * Note: Permission checking is handled within the subquery RTEs.
+	 * Each element table is wrapped in a subquery that contains the
+	 * RTE_RELATION with its rteperminfos. The permission info should be
+	 * collected by setrefs.c during planning.
+	 *
+	 * TODO: Currently there's an issue where the permission info from
+	 * nested subqueries is not properly collected into finalrteperminfos.
+	 * This needs to be investigated further.
 	 */
-	vars = pull_vars_of_level((Node *) list_make2(qual_exprs, path_query->targetList), 0);
-	foreach_node(Var, var, vars)
-	{
-		RTEPermissionInfo *perminfo = getRTEPermissionInfo(path_query->rteperminfos,
-														   rt_fetch(var->varno, path_query->rtable));
-
-		/* Must offset the attnum to fit in a bitmapset */
-		perminfo->selectedCols = bms_add_member(perminfo->selectedCols,
-												var->varattno - FirstLowInvalidHeapAttributeNumber);
-	}
 
 	return path_query;
 }
@@ -1135,6 +1136,35 @@ replace_property_refs_mutator(Node *node, struct replace_property_refs_context *
 
 		return n;
 	}
+	else if (IsA(node, GraphLabelsRef))
+	{
+		GraphLabelsRef *glr = (GraphLabelsRef *) node;
+		struct path_element *found_mapping = NULL;
+		Var		   *var;
+
+		/* Find the element mapping for this variable */
+		foreach_ptr(struct path_element, m, context->mappings)
+		{
+			if (m->path_factor->variable &&
+				strcmp(glr->elvarname, m->path_factor->variable) == 0)
+			{
+				found_mapping = m;
+				break;
+			}
+		}
+		if (!found_mapping)
+			elog(ERROR, "undefined element variable \"%s\"", glr->elvarname);
+
+		/*
+		 * Return a Var referencing the __labels__ column in the subquery RTE.
+		 * This allows the optimizer to push down predicates involving LABELS().
+		 */
+		var = makeVar(found_mapping->path_factor->factorpos + 1,
+					  found_mapping->labels_attnum,
+					  TEXTARRAYOID, -1, InvalidOid, 0);
+
+		return (Node *) var;
+	}
 
 	return expression_tree_mutator(node, replace_property_refs_mutator, context);
 }
@@ -1327,3 +1357,137 @@ get_element_property_expr(Oid elemoid, Oid propoid, int rtindex)
 
 	return n;
 }
+
+/*
+ * Build a Const node containing an array of label names for the given element.
+ * Returns text[] constant like ARRAY['Person', 'Employee']::text[].
+ */
+static Const *
+build_labels_const(Oid elemoid)
+{
+	List	   *label_names = NIL;
+	ArrayType  *arr;
+	Datum	   *elems;
+	int			nelems;
+	int			i;
+	CatCList   *catlist;
+
+	/* Get all labels for this element using syscache */
+	catlist = SearchSysCacheList1(PROPGRAPHELEMENTLABELELEMENTLABEL,
+								  ObjectIdGetDatum(elemoid));
+
+	for (i = 0; i < catlist->n_members; i++)
+	{
+		HeapTuple	labeltup = &catlist->members[i]->tuple;
+		Form_pg_propgraph_element_label form =
+			(Form_pg_propgraph_element_label) GETSTRUCT(labeltup);
+		char	   *labelname = get_propgraph_label_name(form->pgellabelid);
+
+		label_names = lappend(label_names, makeString(labelname));
+	}
+
+	ReleaseSysCacheList(catlist);
+
+	/* Build ARRAY['label1', 'label2', ...]::text[] */
+	nelems = list_length(label_names);
+	elems = (Datum *) palloc(nelems * sizeof(Datum));
+	i = 0;
+	foreach_ptr(String, s, label_names)
+	{
+		elems[i++] = CStringGetTextDatum(strVal(s));
+	}
+
+	arr = construct_array(elems, nelems, TEXTOID, -1, false, TYPALIGN_INT);
+
+	return makeConst(TEXTARRAYOID, -1, InvalidOid,
+					 -1, PointerGetDatum(arr), false, false);
+}
+
+/*
+ * Create a subquery RTE that wraps the element table and adds a virtual
+ * __labels__ column.
+ *
+ * The subquery is: SELECT *, ARRAY['Label1', 'Label2']::text[] AS __labels__
+ *                  FROM element_table
+ *
+ * This allows LABELS(v) to reference a Var instead of a constant, enabling
+ * the optimizer to push down predicates involving LABELS().
+ */
+static RangeTblEntry *
+create_element_subquery_rte(struct path_element *pe, Oid reloid, AttrNumber *labels_attnum)
+{
+	Query	   *subquery;
+	RangeTblEntry *subrte;
+	RangeTblEntry *relrte;
+	RangeTblRef *rtr;
+	Relation	rel;
+	TupleDesc	tupdesc;
+	int			attno;
+	ParseNamespaceItem *pni;
+	Const	   *labels_const;
+	TargetEntry *labels_te;
+
+	subquery = makeNode(Query);
+	subquery->commandType = CMD_SELECT;
+
+	/* Create RTE for the actual table */
+	rel = table_open(reloid, AccessShareLock);
+	pni = addRangeTableEntryForRelation(make_parsestate(NULL), rel,
+										AccessShareLock, NULL, true, false);
+	relrte = pni->p_rte;
+	tupdesc = RelationGetDescr(rel);
+	table_close(rel, NoLock);
+
+	subquery->rtable = list_make1(relrte);
+	subquery->rteperminfos = list_make1(pni->p_perminfo);
+	relrte->perminfoindex = 1;
+
+	/* Create FromExpr */
+	rtr = makeNode(RangeTblRef);
+	rtr->rtindex = 1;
+	subquery->jointree = makeFromExpr(list_make1(rtr), NULL);
+
+	/* Build targetlist: all columns from the table + __labels__ */
+	subquery->targetList = NIL;
+	attno = 1;
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+		Var		   *var;
+		TargetEntry *te;
+
+		if (attr->attisdropped)
+			continue;
+
+		var = makeVar(1, attr->attnum, attr->atttypid,
+					  attr->atttypmod, attr->attcollation, 0);
+		te = makeTargetEntry((Expr *) var, attno++,
+							 pstrdup(NameStr(attr->attname)), false);
+		subquery->targetList = lappend(subquery->targetList, te);
+	}
+
+	/* Add __labels__ column */
+	labels_const = build_labels_const(pe->elemoid);
+	labels_te = makeTargetEntry((Expr *) labels_const, attno,
+								pstrdup("__labels__"), false);
+	subquery->targetList = lappend(subquery->targetList, labels_te);
+	*labels_attnum = attno;
+
+	/* Create subquery RTE */
+	subrte = makeNode(RangeTblEntry);
+	subrte->rtekind = RTE_SUBQUERY;
+	subrte->subquery = subquery;
+	subrte->lateral = false;
+	subrte->inh = false;
+	subrte->inFromCl = true;
+
+	/* Build column name list for the subquery RTE */
+	subrte->eref = makeAlias("__element__", NIL);
+	foreach_node(TargetEntry, te, subquery->targetList)
+	{
+		subrte->eref->colnames = lappend(subrte->eref->colnames,
+										 makeString(pstrdup(te->resname)));
+	}
+
+	return subrte;
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index bf8a623415c..e03e48f9ec6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -11143,6 +11143,14 @@ get_rule_expr(Node *node, deparse_context *context,
 				break;
 			}
 
+		case T_GraphLabelsRef:
+			{
+				GraphLabelsRef *glr = (GraphLabelsRef *) node;
+
+				appendStringInfo(buf, "LABELS(%s)", quote_identifier(glr->elvarname));
+				break;
+			}
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			break;
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index e9e2d6c1504..b658bb2be8c 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2201,6 +2201,16 @@ typedef struct GraphPropertyRef
 	ParseLoc	location;
 } GraphPropertyRef;
 
+/*
+ * GraphLabelsRef - LABELS(element_variable) inside GRAPH_TABLE clause
+ */
+typedef struct GraphLabelsRef
+{
+	Expr		xpr;
+	const char *elvarname;		/* element variable name */
+	ParseLoc	location;
+} GraphLabelsRef;
+
 /*--------------------
  * TargetEntry -
  *	   a target entry (used in query target lists)
diff --git a/src/include/parser/parse_graphtable.h b/src/include/parser/parse_graphtable.h
index 4cefd5acf9d..51d2e72b4a6 100644
--- a/src/include/parser/parse_graphtable.h
+++ b/src/include/parser/parse_graphtable.h
@@ -19,6 +19,8 @@
 
 extern Node *transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref);
 
+extern Node *transformGraphTableFuncCall(ParseState *pstate, FuncCall *fn);
+
 extern Node *transformGraphPattern(ParseState *pstate, GraphPattern *graph_pattern);
 
 #endif							/* PARSE_GRAPHTABLE_H */
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
index a4df7464d79..a27322c5573 100644
--- a/src/test/regress/expected/graph_table.out
+++ b/src/test/regress/expected/graph_table.out
@@ -928,4 +928,215 @@ SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE src.vprop1 >
 ERROR:  subqueries within GRAPH_TABLE reference are not supported
 SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE out_degree(src.vname) > (SELECT max(out_degree(nname)) FROM GRAPH_TABLE (g1 MATCH (node) COLUMNS (node.vname AS nname))) COLUMNS(src.vname AS sname, dest.vname AS dname));
 ERROR:  subqueries within GRAPH_TABLE reference are not supported
+-- LABELS() function tests
+-- basic LABELS() in COLUMNS clause
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (c.name, LABELS(c) AS lbls)) ORDER BY 1;
+   name    |    lbls     
+-----------+-------------
+ customer1 | {customers}
+ customer2 | {customers}
+ customer3 | {customers}
+(3 rows)
+
+-- LABELS() for vertices and edges
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[e IS customer_orders]->(o IS orders) COLUMNS (c.name, LABELS(c) AS clbls, LABELS(e) AS elbls, LABELS(o) AS olbls)) ORDER BY 1;
+   name    |    clbls    |            elbls             |     olbls      
+-----------+-------------+------------------------------+----------------
+ customer1 | {customers} | {customer_orders,cust_lists} | {orders,lists}
+ customer2 | {customers} | {customer_orders,cust_lists} | {orders,lists}
+(2 rows)
+
+-- LABELS() in WHERE clause
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c) WHERE 'customers' = ANY(LABELS(c)) COLUMNS (c.name)) ORDER BY 1;
+   name    
+-----------
+ customer1
+ customer2
+ customer3
+(3 rows)
+
+-- LABELS() with array functions
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c) WHERE array_length(LABELS(c), 1) >= 1 COLUMNS (c.name, LABELS(c) AS lbls)) ORDER BY 1;
+   name    |       lbls        
+-----------+-------------------
+ customer1 | {customers}
+ customer2 | {customers}
+ customer3 | {customers}
+ product1  | {products}
+ product2  | {products}
+ product3  | {products}
+           | {lists,wishlists}
+           | {lists,wishlists}
+           | {lists,wishlists}
+           | {orders,lists}
+           | {orders,lists}
+           | {orders,lists}
+(12 rows)
+
+-- LABELS() error: undefined variable
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS(undefined_var)));
+ERROR:  element variable "undefined_var" does not exist
+LINE 1: ...LE (myshop MATCH (c IS customers) COLUMNS (LABELS(undefined_...
+                                                             ^
+-- LABELS() error: no argument
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS()));
+ERROR:  LABELS() requires exactly one argument
+LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS()))...
+                                                             ^
+-- LABELS() error: too many arguments
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS(c, c)));
+ERROR:  LABELS() requires exactly one argument
+LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS(c, ...
+                                                             ^
+-- LABELS() error: non-variable argument
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS(123)));
+ERROR:  LABELS() argument must be an element variable
+LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS(123...
+                                                             ^
+-- LABELS() with shared labels (optimizer pruning tests)
+-- The 'lists' label is shared by both 'orders' and 'wishlists' tables
+-- LABELS() filtering inside MATCH WHERE clause
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'orders' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+);
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on graph_table_tests.orders
+   Output: '{orders,lists}'::text[], orders.order_id
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'wishlists' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+);
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Seq Scan on graph_table_tests.wishlists
+   Output: '{lists,wishlists}'::text[], wishlists.wishlist_id
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'lists' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+);
+                             QUERY PLAN                             
+--------------------------------------------------------------------
+ Append
+   ->  Seq Scan on graph_table_tests.orders
+         Output: '{orders,lists}'::text[], orders.order_id
+   ->  Seq Scan on graph_table_tests.wishlists
+         Output: '{lists,wishlists}'::text[], wishlists.wishlist_id
+(5 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'nonexistent' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+);
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Result
+   Output: "graph_table".lbls, "graph_table".node_id
+   Replaces: Scan on graph_table
+   One-Time Filter: false
+(4 rows)
+
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'orders' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) ORDER BY node_id;
+      lbls      | node_id 
+----------------+---------
+ {orders,lists} |       1
+ {orders,lists} |       2
+ {orders,lists} |       3
+(3 rows)
+
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'wishlists' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) ORDER BY node_id;
+       lbls        | node_id 
+-------------------+---------
+ {lists,wishlists} |       1
+ {lists,wishlists} |       2
+ {lists,wishlists} |       3
+(3 rows)
+
+-- LABELS() filtering outside GRAPH_TABLE (in outer WHERE clause)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'orders' = ANY(lbls);
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on graph_table_tests.orders
+   Output: '{orders,lists}'::text[], orders.order_id
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'wishlists' = ANY(lbls);
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Seq Scan on graph_table_tests.wishlists
+   Output: '{lists,wishlists}'::text[], wishlists.wishlist_id
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'lists' = ANY(lbls);
+                             QUERY PLAN                             
+--------------------------------------------------------------------
+ Append
+   ->  Seq Scan on graph_table_tests.orders
+         Output: '{orders,lists}'::text[], orders.order_id
+   ->  Seq Scan on graph_table_tests.wishlists
+         Output: '{lists,wishlists}'::text[], wishlists.wishlist_id
+(5 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'nonexistent' = ANY(lbls);
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Result
+   Output: "graph_table".lbls, "graph_table".node_id
+   Replaces: Scan on graph_table
+   One-Time Filter: false
+(4 rows)
+
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'orders' = ANY(lbls) ORDER BY node_id;
+      lbls      | node_id 
+----------------+---------
+ {orders,lists} |       1
+ {orders,lists} |       2
+ {orders,lists} |       3
+(3 rows)
+
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'wishlists' = ANY(lbls) ORDER BY node_id;
+       lbls        | node_id 
+-------------------+---------
+ {lists,wishlists} |       1
+ {lists,wishlists} |       2
+ {lists,wishlists} |       3
+(3 rows)
+
 -- leave the objects behind for pg_upgrade/pg_dump tests
diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql
index 7521c3e5c1d..8f1cf447313 100644
--- a/src/test/regress/sql/graph_table.sql
+++ b/src/test/regress/sql/graph_table.sql
@@ -545,4 +545,102 @@ SELECT * FROM customers co WHERE co.customer_id = (SELECT customer_id FROM GRAPH
 SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE src.vprop1 > (SELECT max(v1.vprop1) FROM v1) COLUMNS(src.vname AS sname, dest.vname AS dname));
 SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE out_degree(src.vname) > (SELECT max(out_degree(nname)) FROM GRAPH_TABLE (g1 MATCH (node) COLUMNS (node.vname AS nname))) COLUMNS(src.vname AS sname, dest.vname AS dname));
 
+-- LABELS() function tests
+-- basic LABELS() in COLUMNS clause
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (c.name, LABELS(c) AS lbls)) ORDER BY 1;
+
+-- LABELS() for vertices and edges
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[e IS customer_orders]->(o IS orders) COLUMNS (c.name, LABELS(c) AS clbls, LABELS(e) AS elbls, LABELS(o) AS olbls)) ORDER BY 1;
+
+-- LABELS() in WHERE clause
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c) WHERE 'customers' = ANY(LABELS(c)) COLUMNS (c.name)) ORDER BY 1;
+
+-- LABELS() with array functions
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c) WHERE array_length(LABELS(c), 1) >= 1 COLUMNS (c.name, LABELS(c) AS lbls)) ORDER BY 1;
+
+-- LABELS() error: undefined variable
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS(undefined_var)));
+
+-- LABELS() error: no argument
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS()));
+
+-- LABELS() error: too many arguments
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS(c, c)));
+
+-- LABELS() error: non-variable argument
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (LABELS(123)));
+
+-- LABELS() with shared labels (optimizer pruning tests)
+-- The 'lists' label is shared by both 'orders' and 'wishlists' tables
+
+-- LABELS() filtering inside MATCH WHERE clause
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'orders' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'wishlists' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'lists' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'nonexistent' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+);
+
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'orders' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) ORDER BY node_id;
+
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists WHERE 'wishlists' = ANY(LABELS(n)))
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) ORDER BY node_id;
+
+-- LABELS() filtering outside GRAPH_TABLE (in outer WHERE clause)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'orders' = ANY(lbls);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'wishlists' = ANY(lbls);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'lists' = ANY(lbls);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'nonexistent' = ANY(lbls);
+
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'orders' = ANY(lbls) ORDER BY node_id;
+
+SELECT * FROM GRAPH_TABLE (myshop
+    MATCH (n IS lists)
+    COLUMNS (LABELS(n) AS lbls, n.node_id)
+) WHERE 'wishlists' = ANY(lbls) ORDER BY node_id;
+
 -- leave the objects behind for pg_upgrade/pg_dump tests
-- 
2.50.1 (Apple Git-155)

