From 5476fb8e5ce11f8d15d3de2deb1a17248624c196 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih.pg@gmail.com>
Date: Sat, 15 Aug 2026 15:08:13 -0400
Subject: [PATCH v7 1/1] Rework GRAPH_TABLE aggregate/window/SRF rejection
 using ParseExprKind.

Commit f58567105 disallowed aggregates, window functions, and
set-returning functions in a GRAPH_TABLE COLUMNS list by inspecting
the parse state after transforming the list.  This way is not capable
of reporting a parse location for the misplaced construct; worse, the
patch missed checking for aggregates etc. in GRAPH_TABLE WHERE.

Instead give the COLUMNS list and the graph pattern WHERE clause their
own ParseExprKind values and enforce the restriction within the
parser's transformation functions check_agglevels_and_constraints(),
transformWindowFuncCall(), and check_srf_call_placement().  (This
reverts the code changes of f58567105, though we keep the test cases
and add some more.)  This is more consistent with how the parser
implements other misplaced-construct checks, and it allows delivery
of better error messages.

Subqueries are likewise disallowed within a GRAPH_TABLE COLUMNS list or
graph pattern WHERE clause; transformSubLink now rejects a SubLink under
these ParseExprKinds.

The parsing check rejects only aggregates having level zero.
This is intentional: an outer-level aggregate is effectively a
constant within the subquery containing GRAPH_TABLE, so there's no
reason not to allow it.  The case did not work before because
GraphPropertyRef did not carry query-level information explicitly and
the rewrite path did not consistently adjust already-outer references
before nesting the expression under a generated subquery.

Rejecting only level-zero aggregates therefore requires
GraphPropertyRef to carry an explicit levelsup field and for
check_agg_arguments_walker to account for GraphPropertyRef nodes
directly.  Since GraphPropertyRef is not a Var, teach the relevant
query-level walkers to treat it as a Var-like node where needed, and
prepare GRAPH_TABLE expressions for the added query level before
replace_property_refs() resolves property refs.

Note that this changes the SQLSTATE for a rejected aggregate from
ERRCODE_FEATURE_NOT_SUPPORTED to ERRCODE_GROUPING_ERROR, consistent
with how misplaced aggregates are rejected in other cases.

Author: Sami Imseih <samimseih@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/CAA5RZ0tvdYODLQvYwVzAxUPe5=E3vSe8Zy7TvrQq+syvGKpHSQ@mail.gmail.com
Backpatch-through: 19
---
 src/backend/optimizer/util/var.c          | 22 ++++++-
 src/backend/parser/parse_agg.c            | 25 ++++++++
 src/backend/parser/parse_clause.c         | 47 +-------------
 src/backend/parser/parse_expr.c           | 10 +++
 src/backend/parser/parse_func.c           |  4 ++
 src/backend/parser/parse_graphtable.c     | 10 +--
 src/backend/rewrite/rewriteGraphTable.c   | 52 ++++++++--------
 src/backend/rewrite/rewriteManip.c        | 13 ++++
 src/include/nodes/primnodes.h             | 15 +++++
 src/include/parser/parse_node.h           |  2 +
 src/test/regress/expected/graph_table.out | 75 +++++++++++++++++++++--
 src/test/regress/sql/graph_table.sql      | 14 ++++-
 12 files changed, 202 insertions(+), 87 deletions(-)

diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c
index 907a255c36f..929cb740874 100644
--- a/src/backend/optimizer/util/var.c
+++ b/src/backend/optimizer/util/var.c
@@ -6,7 +6,9 @@
  * Note: for most purposes, PlaceHolderVar is considered a Var too,
  * even if its contained expression is variable-free.  Also, CurrentOfExpr
  * is treated as a Var for purposes of determining whether an expression
- * contains variables.
+ * contains variables.  GraphPropertyRef carries a levelsup and is likewise
+ * treated as a Var when determining which query level a reference belongs to
+ * (see primnodes.h).
  *
  *
  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
@@ -462,6 +464,12 @@ contain_vars_of_level_walker(Node *node, int *sublevels_up)
 			return true;		/* abort tree traversal and return true */
 		return false;
 	}
+	if (IsA(node, GraphPropertyRef))
+	{
+		if (((GraphPropertyRef *) node)->gprlevelsup == *sublevels_up)
+			return true;		/* abort tree traversal and return true */
+		return false;
+	}
 	if (IsA(node, CurrentOfExpr))
 	{
 		if (*sublevels_up == 0)
@@ -585,6 +593,18 @@ locate_var_of_level_walker(Node *node,
 		}
 		return false;
 	}
+	if (IsA(node, GraphPropertyRef))
+	{
+		GraphPropertyRef *gpr = (GraphPropertyRef *) node;
+
+		if (gpr->gprlevelsup == context->sublevels_up &&
+			gpr->location >= 0)
+		{
+			context->var_location = gpr->location;
+			return true;		/* abort tree traversal and return true */
+		}
+		return false;
+	}
 	if (IsA(node, CurrentOfExpr))
 	{
 		/* since CurrentOfExpr doesn't carry location, nothing we can do */
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 754a20507d0..ebc6dff2d0e 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -600,6 +600,12 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 
 			break;
 
+		case EXPR_KIND_GRAPH_TABLE_COLUMNS:
+		case EXPR_KIND_GRAPH_TABLE_WHERE:
+			errkind = true;
+
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -784,6 +790,21 @@ check_agg_arguments_walker(Node *node,
 		}
 		return false;
 	}
+	if (IsA(node, GraphPropertyRef))
+	{
+		int			gprlevelsup = ((GraphPropertyRef *) node)->gprlevelsup;
+
+		/* convert levelsup to frame of reference of original query */
+		gprlevelsup -= context->sublevels_up;
+		/* ignore local vars of subqueries */
+		if (gprlevelsup >= 0)
+		{
+			if (context->min_varlevel < 0 ||
+				context->min_varlevel > gprlevelsup)
+				context->min_varlevel = gprlevelsup;
+		}
+		return false;
+	}
 	if (IsA(node, Aggref))
 	{
 		int			agglevelsup = ((Aggref *) node)->agglevelsup;
@@ -1045,6 +1066,10 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_FOR_PORTION:
 			err = _("window functions are not allowed in FOR PORTION OF expressions");
 			break;
+		case EXPR_KIND_GRAPH_TABLE_COLUMNS:
+		case EXPR_KIND_GRAPH_TABLE_WHERE:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 67de2733d8a..34a4fa0672b 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -945,10 +945,6 @@ transformRangeGraphTable(ParseState *pstate, RangeGraphTable *rgt)
 	List	   *colnames = NIL;
 	ListCell   *lc;
 	int			resno = 0;
-	bool		saved_hasSublinks;
-	bool		saved_hasAggs;
-	bool		saved_hasWindowFuncs;
-	bool		saved_hasTargetSRFs;
 
 	rel = parserOpenPropGraph(pstate, rgt->graph_name, AccessShareLock);
 
@@ -967,16 +963,6 @@ transformRangeGraphTable(ParseState *pstate, RangeGraphTable *rgt)
 	Assert(!pstate->p_lateral_active);
 	pstate->p_lateral_active = true;
 
-	saved_hasSublinks = pstate->p_hasSubLinks;
-	pstate->p_hasSubLinks = false;
-
-	saved_hasAggs = pstate->p_hasAggs;
-	pstate->p_hasAggs = false;
-	saved_hasWindowFuncs = pstate->p_hasWindowFuncs;
-	pstate->p_hasWindowFuncs = false;
-	saved_hasTargetSRFs = pstate->p_hasTargetSRFs;
-	pstate->p_hasTargetSRFs = false;
-
 	gp = transformGraphPattern(pstate, rgt->graph_pattern);
 
 	/*
@@ -991,7 +977,7 @@ transformRangeGraphTable(ParseState *pstate, RangeGraphTable *rgt)
 		TargetEntry *te;
 		char	   *colname;
 
-		colexpr = transformExpr(pstate, rt->val, EXPR_KIND_SELECT_TARGET);
+		colexpr = transformExpr(pstate, rt->val, EXPR_KIND_GRAPH_TABLE_COLUMNS);
 
 		if (rt->name)
 			colname = rt->name;
@@ -1030,37 +1016,6 @@ transformRangeGraphTable(ParseState *pstate, RangeGraphTable *rgt)
 	pstate->p_graph_table_pstate = NULL;
 	pstate->p_lateral_active = false;
 
-	/*
-	 * If we support subqueries within GRAPH_TABLE, those need to be
-	 * propagated to the queries resulting from rewriting graph table RTE. We
-	 * don't do that right now, hence prohibit it for now.
-	 */
-	if (pstate->p_hasSubLinks)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("subqueries within GRAPH_TABLE reference are not supported")));
-	pstate->p_hasSubLinks = saved_hasSublinks;
-
-	/*
-	 * GRAPH_TABLE cannot yet evaluate aggregate, window, or set-returning
-	 * functions in its COLUMNS list, so prohibit them for now.
-	 */
-	if (pstate->p_hasAggs)
-		ereport(ERROR,
-				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				errmsg("aggregate functions in GRAPH_TABLE COLUMNS are not supported"));
-	if (pstate->p_hasWindowFuncs)
-		ereport(ERROR,
-				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				errmsg("window functions in GRAPH_TABLE COLUMNS are not supported"));
-	if (pstate->p_hasTargetSRFs)
-		ereport(ERROR,
-				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				errmsg("set-returning functions in GRAPH_TABLE COLUMNS are not supported"));
-	pstate->p_hasAggs = saved_hasAggs;
-	pstate->p_hasWindowFuncs = saved_hasWindowFuncs;
-	pstate->p_hasTargetSRFs = saved_hasTargetSRFs;
-
 	return addRangeTableEntryForGraphTable(pstate, graphid, castNode(GraphPattern, gp), columns, colnames, rgt->alias, false, true);
 }
 
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 30c889f505f..d69a0a79f48 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -579,6 +579,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
+		case EXPR_KIND_GRAPH_TABLE_COLUMNS:
+		case EXPR_KIND_GRAPH_TABLE_WHERE:
 			/* okay */
 			break;
 
@@ -1845,6 +1847,10 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_CYCLE_MARK:
 			/* okay */
 			break;
+		case EXPR_KIND_GRAPH_TABLE_COLUMNS:
+		case EXPR_KIND_GRAPH_TABLE_WHERE:
+			err = _("cannot use subquery in GRAPH_TABLE reference");
+			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
 			err = _("cannot use subquery in check constraint");
@@ -3255,6 +3261,10 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "property definition expression";
 		case EXPR_KIND_FOR_PORTION:
 			return "FOR PORTION OF";
+		case EXPR_KIND_GRAPH_TABLE_COLUMNS:
+			return "GRAPH_TABLE COLUMNS";
+		case EXPR_KIND_GRAPH_TABLE_WHERE:
+			return "GRAPH_TABLE WHERE";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index c87804f5d41..0c72b95e4a5 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2848,6 +2848,10 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
 			break;
+		case EXPR_KIND_GRAPH_TABLE_COLUMNS:
+		case EXPR_KIND_GRAPH_TABLE_WHERE:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_graphtable.c b/src/backend/parser/parse_graphtable.c
index 73fbfb541f7..c8e230379a5 100644
--- a/src/backend/parser/parse_graphtable.c
+++ b/src/backend/parser/parse_graphtable.c
@@ -92,7 +92,7 @@ transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref)
 
 		if (IsA(field1, A_Star) || IsA(field2, A_Star))
 		{
-			if (pstate->p_expr_kind == EXPR_KIND_SELECT_TARGET)
+			if (pstate->p_expr_kind == EXPR_KIND_GRAPH_TABLE_COLUMNS)
 				ereport(ERROR,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("\"*\" is not supported here"),
@@ -106,7 +106,6 @@ transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref)
 
 		elvarname = strVal(field1);
 		propname = strVal(field2);
-
 		if (list_member(gpstate->variables, field1))
 		{
 			GraphPropertyRef *gpr;
@@ -137,6 +136,8 @@ transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref)
 
 			gpr->location = cref->location;
 			gpr->elvarname = elvarname;
+
+			gpr->gprlevelsup = 0;
 			gpr->propid = pgpform->oid;
 			gpr->typeId = pgpform->pgptypid;
 			gpr->typmod = pgpform->pgptypmod;
@@ -250,8 +251,7 @@ transformGraphElementPattern(ParseState *pstate, GraphElementPattern *gep)
 	gpstate->cur_gep = gep;
 
 	gep->labelexpr = transformLabelExpr(gpstate, gep->labelexpr);
-
-	gep->whereClause = transformExpr(pstate, gep->whereClause, EXPR_KIND_WHERE);
+	gep->whereClause = transformExpr(pstate, gep->whereClause, EXPR_KIND_GRAPH_TABLE_WHERE);
 
 	/*
 	 * Assign collations here for the reason mentioned in the prologue of
@@ -387,7 +387,7 @@ transformGraphPattern(ParseState *pstate, GraphPattern *graph_pattern)
 											 transformPathPatternList(pstate, graph_pattern->path_pattern_list));
 
 	graph_pattern->path_pattern_list = path_pattern_list;
-	graph_pattern->whereClause = transformExpr(pstate, graph_pattern->whereClause, EXPR_KIND_WHERE);
+	graph_pattern->whereClause = transformExpr(pstate, graph_pattern->whereClause, EXPR_KIND_GRAPH_TABLE_WHERE);
 	assign_expr_collations(pstate, graph_pattern->whereClause);
 
 	return (Node *) graph_pattern;
diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c
index 0eaf28b3de5..ce32a97b4f2 100644
--- a/src/backend/rewrite/rewriteGraphTable.c
+++ b/src/backend/rewrite/rewriteGraphTable.c
@@ -34,7 +34,6 @@
 #include "parser/parse_oper.h"
 #include "parser/parse_relation.h"
 #include "parser/parsetree.h"
-#include "parser/parse_graphtable.h"
 #include "rewrite/rewriteGraphTable.h"
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
@@ -100,7 +99,8 @@ static Query *generate_query_for_empty_path_pattern(RangeTblEntry *rte);
 static Query *generate_union_from_pathqueries(List **pathqueries);
 static List *get_path_elements_for_path_factor(Oid propgraphid, struct path_factor *pf);
 static bool is_property_associated_with_label(Oid labeloid, Oid propoid);
-static Node *get_element_property_expr(Oid elemoid, Oid propoid, int rtindex);
+static Node *get_element_property_expr(Oid elemoid, Oid propoid, int rtindex,
+									   Index gprlevelsup);
 
 /*
  * Convert GRAPH_TABLE clause into a subquery using relational
@@ -530,9 +530,10 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 
 		if (pf->whereClause)
 		{
-			Node	   *tr;
+			Node	   *tr = copyObject(pf->whereClause);
 
-			tr = replace_property_refs(rte->relid, pf->whereClause, list_make1(pe));
+			IncrementVarSublevelsUp(tr, 1, 0);
+			tr = replace_property_refs(rte->relid, tr, list_make1(pe));
 
 			qual_exprs = lappend(qual_exprs, tr);
 		}
@@ -540,9 +541,10 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 
 	if (rte->graph_pattern->whereClause)
 	{
-		Node	   *path_quals = replace_property_refs(rte->relid,
-													   (Node *) rte->graph_pattern->whereClause,
-													   graph_path);
+		Node	   *path_quals = copyObject(rte->graph_pattern->whereClause);
+
+		IncrementVarSublevelsUp(path_quals, 1, 0);
+		path_quals = replace_property_refs(rte->relid, path_quals, graph_path);
 
 		qual_exprs = lappend(qual_exprs, path_quals);
 	}
@@ -551,9 +553,11 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path)
 										qual_exprs ? (Node *) makeBoolExpr(AND_EXPR, qual_exprs, -1) : NULL);
 
 	/* Construct query targetlist from COLUMNS specification of GRAPH_TABLE. */
+	path_query->targetList = copyObject(rte->graph_table_columns);
+	IncrementVarSublevelsUp((Node *) path_query->targetList, 1, 0);
 	path_query->targetList = castNode(List,
 									  replace_property_refs(rte->relid,
-															(Node *) rte->graph_table_columns,
+															(Node *) path_query->targetList,
 															graph_path));
 
 	/*
@@ -1025,21 +1029,7 @@ replace_property_refs_mutator(Node *node, struct replace_property_refs_context *
 {
 	if (node == NULL)
 		return NULL;
-	if (IsA(node, Var))
-	{
-		Var		   *var = (Var *) node;
-		Var		   *newvar = copyObject(var);
-
-		/*
-		 * If it's already a Var, then it was a lateral reference.  Since we
-		 * are in a subquery after the rewrite, we have to increase the level
-		 * by one.
-		 */
-		newvar->varlevelsup++;
-
-		return (Node *) newvar;
-	}
-	else if (IsA(node, GraphPropertyRef))
+	if (IsA(node, GraphPropertyRef))
 	{
 		GraphPropertyRef *gpr = (GraphPropertyRef *) node;
 		Node	   *n = NULL;
@@ -1063,6 +1053,8 @@ replace_property_refs_mutator(Node *node, struct replace_property_refs_context *
 		 */
 		Assert(found_mapping);
 
+		Assert(gpr->gprlevelsup == 0);
+
 		mapping_factor = found_mapping->path_factor;
 
 		/*
@@ -1093,7 +1085,8 @@ replace_property_refs_mutator(Node *node, struct replace_property_refs_context *
 
 				n = stringToNode(TextDatumGetCString(SysCacheGetAttrNotNull(PROPGRAPHLABELPROP,
 																			tup, Anum_pg_propgraph_label_property_plpexpr)));
-				ChangeVarNodes(n, 1, mapping_factor->factorpos + 1, 0);
+				ChangeVarNodes(n, 1, mapping_factor->factorpos + 1,
+							   gpr->gprlevelsup);
 
 				ReleaseSysCache(tup);
 			}
@@ -1132,8 +1125,10 @@ replace_property_refs_mutator(Node *node, struct replace_property_refs_context *
 				 * SQL/PGQ standard section 6.5 Property Reference, General
 				 * Rule 2.b.
 				 */
-				n = get_element_property_expr(found_mapping->elemoid, gpr->propid,
-											  mapping_factor->factorpos + 1);
+				n = get_element_property_expr(found_mapping->elemoid,
+											  gpr->propid,
+											  mapping_factor->factorpos + 1,
+											  gpr->gprlevelsup);
 
 				if (!n)
 					n = (Node *) makeNullConst(gpr->typeId, gpr->typmod, gpr->collation);
@@ -1304,7 +1299,8 @@ is_property_associated_with_label(Oid labeloid, Oid propoid)
  * NULL.
  */
 static Node *
-get_element_property_expr(Oid elemoid, Oid propoid, int rtindex)
+get_element_property_expr(Oid elemoid, Oid propoid, int rtindex,
+						  Index gprlevelsup)
 {
 	Relation	rel;
 	SysScanDesc scan;
@@ -1331,7 +1327,7 @@ get_element_property_expr(Oid elemoid, Oid propoid, int rtindex)
 			continue;
 		n = stringToNode(TextDatumGetCString(SysCacheGetAttrNotNull(PROPGRAPHLABELPROP,
 																	proptup, Anum_pg_propgraph_label_property_plpexpr)));
-		ChangeVarNodes(n, 1, rtindex, 0);
+		ChangeVarNodes(n, 1, rtindex, gprlevelsup);
 
 		ReleaseSysCache(proptup);
 		break;
diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c
index 3653f00d383..f147d64c0c3 100644
--- a/src/backend/rewrite/rewriteManip.c
+++ b/src/backend/rewrite/rewriteManip.c
@@ -815,6 +815,19 @@ IncrementVarSublevelsUp_walker(Node *node,
 			var->varlevelsup += context->delta_sublevels_up;
 		return false;			/* done here */
 	}
+	if (IsA(node, GraphPropertyRef))
+	{
+		/*
+		 * Unlike a Var, we must not adjust a GraphPropertyRef's level here.
+		 * This walker runs over a GRAPH_TABLE COLUMNS/WHERE expression before
+		 * replace_property_refs() resolves each GraphPropertyRef to a Var
+		 * local to the generated path query, setting its final level via
+		 * ChangeVarNodes().  As we don't yet allow correlated subqueries
+		 * inside a GRAPH_TABLE reference, gprlevelsup is always 0; assert it.
+		 */
+		Assert(((GraphPropertyRef *) node)->gprlevelsup == 0);
+		return false;			/* done here */
+	}
 	if (IsA(node, CurrentOfExpr))
 	{
 		/* this should not happen */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 5a636d1f179..aa7c7841b2b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2196,11 +2196,26 @@ typedef struct GraphLabelRef
 
 /*
  * GraphPropertyRef - property reference inside GRAPH_TABLE clause
+ *
+ * A GraphPropertyRef is generated during transformation and resolved away by
+ * the rewriter, so only a few places need to know about it.  It carries a
+ * levelsup and is treated like a Var by contain_vars_of_level() and
+ * locate_var_of_level(); IncrementVarSublevelsUp() leaves it alone, since
+ * replace_property_refs() passes its gprlevelsup through when resolving it to
+ * a Var.
+ * We don't yet allow correlated subqueries inside a GRAPH_TABLE reference, so
+ * gprlevelsup is always 0 today.
  */
 typedef struct GraphPropertyRef
 {
 	Expr		xpr;
 	const char *elvarname;
+
+	/*
+	 * would be > 0 for an outer-query reference, but see above: always 0
+	 * today
+	 */
+	Index		gprlevelsup;
 	Oid			propid;
 	Oid			typeId;
 	int32		typmod;
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f7f4ba6c2a8..34f061df0fa 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -84,6 +84,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
 	EXPR_KIND_PROPGRAPH_PROPERTY,	/* derived property expression */
+	EXPR_KIND_GRAPH_TABLE_COLUMNS,	/* GRAPH_TABLE COLUMNS list item */
+	EXPR_KIND_GRAPH_TABLE_WHERE,	/* WHERE in a GRAPH_TABLE */
 } ParseExprKind;
 
 
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
index 2e862a82ba0..eb2bd1b0220 100644
--- a/src/test/regress/expected/graph_table.out
+++ b/src/test/regress/expected/graph_table.out
@@ -471,13 +471,72 @@ SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.* IS NOT NULL)-[
 ERROR:  "*" not allowed here
 LINE 1: ...M GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.* IS NOT...
                                                              ^
--- aggregate, window, and set-returning functions are not supported in COLUMNS
+-- aggregate, grouping, window, and set-returning functions are not allowed
+-- in the COLUMNS list or the graph pattern WHERE, except for outer-level aggs
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (count(*) AS num));
-ERROR:  aggregate functions in GRAPH_TABLE COLUMNS are not supported
+ERROR:  aggregate functions are not allowed in GRAPH_TABLE COLUMNS
+LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (count(*) A...
+                                                             ^
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (GROUPING(c.customer_id) AS g));
+ERROR:  grouping operations are not allowed in GRAPH_TABLE COLUMNS
+LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (GROUPING(c...
+                                                             ^
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (row_number() OVER () AS rn));
-ERROR:  window functions in GRAPH_TABLE COLUMNS are not supported
+ERROR:  window functions are not allowed in GRAPH_TABLE COLUMNS
+LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (row_number...
+                                                             ^
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (generate_series(1, 2) AS gs));
-ERROR:  set-returning functions in GRAPH_TABLE COLUMNS are not supported
+ERROR:  set-returning functions are not allowed in GRAPH_TABLE COLUMNS
+LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (generate_s...
+                                                             ^
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE count(c.customer_id) > 0) COLUMNS (c.name AS nm));
+ERROR:  aggregate functions are not allowed in GRAPH_TABLE WHERE
+LINE 1: ...M GRAPH_TABLE (myshop MATCH (c IS customers WHERE count(c.cu...
+                                                             ^
+SELECT count(*) FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE 1 IN (c.customer_id, 1000)) COLUMNS (c.name AS nm)) t;
+ count 
+-------
+     1
+(1 row)
+
+SELECT EXISTS(SELECT num FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (count(o.customer_id) AS num)) t) FROM customers o;
+ exists 
+--------
+ t
+(1 row)
+
+SELECT EXISTS(SELECT nm FROM GRAPH_TABLE (myshop MATCH (c IS customers) WHERE count(o.customer_id) > 0 COLUMNS (c.name AS nm)) t) FROM customers o;
+ exists 
+--------
+ t
+(1 row)
+
+SELECT EXISTS(SELECT nm FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE count(o.customer_id) > 0) COLUMNS (c.name AS nm)) t) FROM customers o;
+ exists 
+--------
+ t
+(1 row)
+
+SELECT EXISTS(SELECT nm FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE count(c.customer_id + o.customer_id) > 0) COLUMNS (c.name AS nm)) t) FROM customers o;
+ERROR:  aggregate functions are not allowed in GRAPH_TABLE WHERE
+LINE 1: ...M GRAPH_TABLE (myshop MATCH (c IS customers WHERE count(c.cu...
+                                                             ^
+SELECT EXISTS(SELECT nm FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE GROUPING(o.customer_id) = 1) COLUMNS (c.name AS nm)) t) FROM customers o GROUP BY customer_id;
+ exists 
+--------
+ f
+ f
+ f
+(3 rows)
+
+-- a property in a GRAPH_TABLE subquery below the aggregate is not local to it,
+-- so the aggregate is allowed
+SELECT count(o.customer_id + (SELECT count(n) FROM GRAPH_TABLE (g1 MATCH (src) COLUMNS (src.vprop1 AS n)) x)) FROM customers o;
+ count 
+-------
+     3
+(1 row)
+
 -- consecutive element patterns with same kind
 SELECT * FROM GRAPH_TABLE (g1 MATCH ()() COLUMNS (1 as one));
 ERROR:  adjacent vertex patterns are not supported
@@ -1118,9 +1177,13 @@ SELECT * FROM customers co WHERE co.customer_id = (SELECT customer_id FROM GRAPH
 
 -- query within graph table
 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));
-ERROR:  subqueries within GRAPH_TABLE reference are not supported
+ERROR:  cannot use subquery in GRAPH_TABLE reference
+LINE 1: ..._TABLE (g1 MATCH (src)->(dest) WHERE src.vprop1 > (SELECT ma...
+                                                             ^
 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
+ERROR:  cannot use subquery in GRAPH_TABLE reference
+LINE 1: ...MATCH (src)->(dest) WHERE out_degree(src.vname) > (SELECT ma...
+                                                             ^
 -- GRAPH_TABLE subquery in HAVING clause (tests expression mutator)
 SELECT src.vname, count(*) FROM v1 AS src
   GROUP BY src.vname
diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql
index 21e70015f6e..240288ce962 100644
--- a/src/test/regress/sql/graph_table.sql
+++ b/src/test/regress/sql/graph_table.sql
@@ -306,10 +306,22 @@ SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS el1 | vl1)-[conn]->(dest) COLUMNS (c
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.*));
 -- star anywhere else is not allowed as a property reference
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.* IS NOT NULL)-[IS customer_orders]->(o IS orders) COLUMNS (c.name));
--- aggregate, window, and set-returning functions are not supported in COLUMNS
+-- aggregate, grouping, window, and set-returning functions are not allowed
+-- in the COLUMNS list or the graph pattern WHERE, except for outer-level aggs
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (count(*) AS num));
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (GROUPING(c.customer_id) AS g));
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (row_number() OVER () AS rn));
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (generate_series(1, 2) AS gs));
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE count(c.customer_id) > 0) COLUMNS (c.name AS nm));
+SELECT count(*) FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE 1 IN (c.customer_id, 1000)) COLUMNS (c.name AS nm)) t;
+SELECT EXISTS(SELECT num FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (count(o.customer_id) AS num)) t) FROM customers o;
+SELECT EXISTS(SELECT nm FROM GRAPH_TABLE (myshop MATCH (c IS customers) WHERE count(o.customer_id) > 0 COLUMNS (c.name AS nm)) t) FROM customers o;
+SELECT EXISTS(SELECT nm FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE count(o.customer_id) > 0) COLUMNS (c.name AS nm)) t) FROM customers o;
+SELECT EXISTS(SELECT nm FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE count(c.customer_id + o.customer_id) > 0) COLUMNS (c.name AS nm)) t) FROM customers o;
+SELECT EXISTS(SELECT nm FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE GROUPING(o.customer_id) = 1) COLUMNS (c.name AS nm)) t) FROM customers o GROUP BY customer_id;
+-- a property in a GRAPH_TABLE subquery below the aggregate is not local to it,
+-- so the aggregate is allowed
+SELECT count(o.customer_id + (SELECT count(n) FROM GRAPH_TABLE (g1 MATCH (src) COLUMNS (src.vprop1 AS n)) x)) FROM customers o;
 -- consecutive element patterns with same kind
 SELECT * FROM GRAPH_TABLE (g1 MATCH ()() COLUMNS (1 as one));
 SELECT * FROM GRAPH_TABLE (g1 MATCH -> COLUMNS (1 AS one));
-- 
2.47.3

