v20251225-0001-Add-LABELS-graph-element-function.patch
application/octet-stream
Filename: v20251225-0001-Add-LABELS-graph-element-function.patch
Type: application/octet-stream
Part: 0
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)