From 935b326657b0448ffa845d2ad9148aa72dd07015 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sun, 30 Aug 2026 07:20:31 +0900
Subject: [PATCH v51 2/9] Row pattern recognition patch (parse/analysis).

---
 src/backend/nodes/copyfuncs.c        |  27 ++
 src/backend/nodes/outfuncs.c         |  47 ++
 src/backend/nodes/queryjumblefuncs.c |  36 ++
 src/backend/nodes/readfuncs.c        |  80 ++++
 src/backend/parser/Makefile          |   1 +
 src/backend/parser/README            |   1 +
 src/backend/parser/meson.build       |   1 +
 src/backend/parser/parse_agg.c       |   9 +-
 src/backend/parser/parse_clause.c    |   7 +
 src/backend/parser/parse_cte.c       |  54 +++
 src/backend/parser/parse_expr.c      | 103 +++++
 src/backend/parser/parse_func.c      | 218 ++++++++-
 src/backend/parser/parse_rpr.c       | 631 +++++++++++++++++++++++++++
 src/include/nodes/primnodes.h        |  59 +++
 src/include/parser/parse_rpr.h       |  22 +
 15 files changed, 1286 insertions(+), 10 deletions(-)
 create mode 100644 src/backend/parser/parse_rpr.c
 create mode 100644 src/include/parser/parse_rpr.h

diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ff22a04abe5..17d45930d7b 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -16,6 +16,7 @@
 #include "postgres.h"
 
 #include "miscadmin.h"
+#include "nodes/plannodes.h"
 #include "utils/datum.h"
 
 
@@ -166,6 +167,32 @@ _copyBitmapset(const Bitmapset *from)
 	return bms_copy(from);
 }
 
+static RPRPattern *
+_copyRPRPattern(const RPRPattern *from)
+{
+	RPRPattern *newnode = makeNode(RPRPattern);
+
+	COPY_SCALAR_FIELD(numVars);
+	COPY_SCALAR_FIELD(maxDepth);
+	COPY_SCALAR_FIELD(numElements);
+
+	/* Deep copy the varNames array (DEFINE clause is required) */
+	Assert(from->numVars > 0);
+	newnode->varNames = palloc0_array(char *, from->numVars);
+	for (int i = 0; i < from->numVars; i++)
+		newnode->varNames[i] = pstrdup(from->varNames[i]);
+
+	/* Deep copy the elements array (always has at least one element + FIN) */
+	Assert(from->numElements >= 2);
+	newnode->elements = palloc_array(RPRPatternElement, from->numElements);
+	memcpy(newnode->elements, from->elements,
+		   from->numElements * sizeof(RPRPatternElement));
+
+	COPY_SCALAR_FIELD(isAbsorbable);
+
+	return newnode;
+}
+
 
 /*
  * copyObjectImpl -- implementation of copyObject(); see nodes/nodes.h
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 953c5797c5d..0b145329cbf 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -23,6 +23,7 @@
 #include "nodes/bitmapset.h"
 #include "nodes/nodes.h"
 #include "nodes/pg_list.h"
+#include "nodes/plannodes.h"
 #include "utils/datum.h"
 
 /* State flag that determines how nodeToStringInternal() should treat location fields */
@@ -727,6 +728,52 @@ _outA_Const(StringInfo str, const A_Const *node)
 	WRITE_LOCATION_FIELD(location);
 }
 
+static void
+_outRPRPattern(StringInfo str, const RPRPattern *node)
+{
+	WRITE_NODE_TYPE("RPRPATTERN");
+
+	WRITE_INT_FIELD(numVars);
+	WRITE_INT_FIELD(maxDepth);
+	WRITE_INT_FIELD(numElements);
+
+	/* Write varNames array as list of strings */
+	appendStringInfoString(str, " :varNames");
+	if (node->numVars > 0 && node->varNames != NULL)
+	{
+		appendStringInfoString(str, " (");
+		for (int i = 0; i < node->numVars; i++)
+		{
+			if (i > 0)
+				appendStringInfoChar(str, ' ');
+			outToken(str, node->varNames[i]);
+		}
+		appendStringInfoChar(str, ')');
+	}
+	else
+		appendStringInfoString(str, " <>");
+
+	/* Write elements array (makeRPRPattern guarantees numElements >= 2) */
+	appendStringInfoString(str, " :elements");
+	Assert(node->numElements > 0 && node->elements != NULL);
+	appendStringInfoChar(str, ' ');
+	for (int i = 0; i < node->numElements; i++)
+	{
+		const RPRPatternElement *elem = &node->elements[i];
+
+		appendStringInfo(str, "(%d %d %u %d %d %d %d)",
+						 (int) elem->varId,
+						 (int) elem->depth,
+						 (unsigned) elem->flags,
+						 (int) elem->min,
+						 (int) elem->max,
+						 (int) elem->next,
+						 (int) elem->jump);
+	}
+
+	WRITE_BOOL_FIELD(isAbsorbable);
+}
+
 
 /*
  * outNode -
diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c
index 094d2872644..299cad5152c 100644
--- a/src/backend/nodes/queryjumblefuncs.c
+++ b/src/backend/nodes/queryjumblefuncs.c
@@ -79,6 +79,9 @@ static void _jumbleVariableSetStmt(JumbleState *jstate, Node *node);
 static void _jumbleRangeTblEntry_eref(JumbleState *jstate,
 									  RangeTblEntry *rte,
 									  Alias *expr);
+static void _jumbleWindowClause_defineClause(JumbleState *jstate,
+											 WindowClause *wc,
+											 List *defineClause);
 
 /*
  * Given a possibly multi-statement source string, confine our attention to the
@@ -774,6 +777,39 @@ _jumbleRangeTblEntry_eref(JumbleState *jstate,
 	JUMBLE_STRING(aliasname);
 }
 
+/*
+ * Custom query jumble function for WindowClause.defineClause.
+ *
+ * The DEFINE clause is stored as a list of TargetEntry, one per row pattern
+ * variable, whose resname is the variable being defined.  TargetEntry.resname
+ * is marked as query_jumble_ignore, however in a DEFINE clause, the resname
+ * is semantically significant: it binds the condition to a PATTERN variable.
+ * "DEFINE A AS p > 50, B AS p < 50" and "DEFINE B AS p > 50, A AS p < 50"
+ * are different queries and must get different query ids. Therefore jumble
+ * TargetEntry.resname is necessary for WindowClause.defineClause.
+ */
+static void
+_jumbleWindowClause_defineClause(JumbleState *jstate,
+								 WindowClause *wc,
+								 List *defineClause)
+{
+	/*
+	 * Jumble the list as the generated code would, so that a window without a
+	 * DEFINE clause keeps its query id.
+	 */
+	_jumbleNode(jstate, (Node *) defineClause);
+
+	/* Then add the variable names, which TargetEntry.resname hides. */
+	foreach_node(TargetEntry, tle, defineClause)
+	{
+		if (tle->resname)
+			AppendJumble(jstate, (const unsigned char *) tle->resname,
+						 strlen(tle->resname) + 1);
+		else
+			AppendJumbleNull(jstate);
+	}
+}
+
 /*
  * CompLocation: comparator for qsorting LocationLen structs by location
  */
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index c07661334ea..d390073e344 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -28,6 +28,7 @@
 
 #include "miscadmin.h"
 #include "nodes/bitmapset.h"
+#include "nodes/plannodes.h"
 #include "nodes/readfuncs.h"
 
 
@@ -567,6 +568,85 @@ _readExtensibleNode(ReadNodeContext *ctx)
 	READ_DONE();
 }
 
+static RPRPattern *
+_readRPRPattern(ReadNodeContext *ctx)
+{
+	READ_LOCALS(RPRPattern);
+
+	READ_INT_FIELD(numVars);
+	READ_INT_FIELD(maxDepth);
+	READ_INT_FIELD(numElements);
+
+	/* Read varNames array */
+	token = pg_strtok(ctx, &length);	/* skip :varNames */
+	token = pg_strtok(ctx, &length);	/* get '(' or '<>' */
+	if (local_node->numVars > 0 && token[0] == '(')
+	{
+		local_node->varNames = palloc_array(char *, local_node->numVars);
+		for (int i = 0; i < local_node->numVars; i++)
+		{
+			token = pg_strtok(ctx, &length);
+			local_node->varNames[i] = debackslash(token, length);
+		}
+		token = pg_strtok(ctx, &length);	/* skip ')' */
+	}
+	else
+	{
+		local_node->varNames = NULL;
+	}
+
+	/* Read elements array */
+	token = pg_strtok(ctx, &length);	/* skip :elements */
+	token = pg_strtok(ctx, &length);	/* get '(' */
+	/* out always emits the array (makeRPRPattern guarantees numElements >= 2) */
+	Assert(local_node->numElements > 0 && token[0] == '(');
+	local_node->elements = palloc0_array(RPRPatternElement, local_node->numElements);
+	for (int i = 0; i < local_node->numElements; i++)
+	{
+		RPRPatternElement *elem = &local_node->elements[i];
+		int			varId,
+					flags,
+					depth,
+					min,
+					max,
+					next,
+					jump;
+
+		/* Parse "(varId depth flags min max next jump)" */
+		token = pg_strtok(ctx, &length);
+		varId = atoi(token);
+		token = pg_strtok(ctx, &length);
+		depth = atoi(token);
+		token = pg_strtok(ctx, &length);
+		flags = atoi(token);
+		token = pg_strtok(ctx, &length);
+		min = atoi(token);
+		token = pg_strtok(ctx, &length);
+		max = atoi(token);
+		token = pg_strtok(ctx, &length);
+		next = atoi(token);
+		token = pg_strtok(ctx, &length);
+		jump = atoi(token);
+		token = pg_strtok(ctx, &length);	/* skip ')' */
+
+		elem->varId = (RPRVarId) varId;
+		elem->flags = (RPRElemFlags) flags;
+		elem->depth = (RPRDepth) depth;
+		elem->min = (RPRQuantity) min;
+		elem->max = (RPRQuantity) max;
+		elem->next = (RPRElemIdx) next;
+		elem->jump = (RPRElemIdx) jump;
+
+		/* Read next element's '(' or end */
+		if (i < local_node->numElements - 1)
+			token = pg_strtok(ctx, &length);	/* get '(' */
+	}
+
+	READ_BOOL_FIELD(isAbsorbable);
+
+	READ_DONE();
+}
+
 
 /*
  * parseNodeString
diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile
index 8b5a4af6bf2..51e6b1adfb8 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -30,6 +30,7 @@ OBJS = \
 	parse_oper.o \
 	parse_param.o \
 	parse_relation.o \
+	parse_rpr.o \
 	parse_target.o \
 	parse_type.o \
 	parse_utilcmd.o \
diff --git a/src/backend/parser/README b/src/backend/parser/README
index e26eb437a9f..22a5e91c8cf 100644
--- a/src/backend/parser/README
+++ b/src/backend/parser/README
@@ -26,6 +26,7 @@ parse_node.c	create nodes for various structures
 parse_oper.c	handle operators in expressions
 parse_param.c	handle Params (for the cases used in the core backend)
 parse_relation.c support routines for tables and column handling
+parse_rpr.c	handle Row Pattern Recognition
 parse_target.c	handle the result list of the query
 parse_type.c	support routines for data type handling
 parse_utilcmd.c	parse analysis for utility commands (done at execution time)
diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build
index 86c09b29ec2..82fe86e10db 100644
--- a/src/backend/parser/meson.build
+++ b/src/backend/parser/meson.build
@@ -17,6 +17,7 @@ backend_sources += files(
   'parse_oper.c',
   'parse_param.c',
   'parse_relation.c',
+  'parse_rpr.c',
   'parse_target.c',
   'parse_type.c',
   'parse_utilcmd.c',
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 754a20507d0..34797048253 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -597,7 +597,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 				err = _("aggregate functions are not allowed in property definition expressions");
 			else
 				err = _("grouping operations are not allowed in property definition expressions");
+			break;
 
+		case EXPR_KIND_RPR_DEFINE:
+			errkind = true;
 			break;
 
 			/*
@@ -1045,6 +1048,9 @@ 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_RPR_DEFINE:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -1125,7 +1131,8 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 				equal(refwin->orderClause, windef->orderClause) &&
 				refwin->frameOptions == windef->frameOptions &&
 				equal(refwin->startOffset, windef->startOffset) &&
-				equal(refwin->endOffset, windef->endOffset))
+				equal(refwin->endOffset, windef->endOffset) &&
+				equal(refwin->rpCommonSyntax, windef->rpCommonSyntax))
 			{
 				/* found a duplicate window specification */
 				wfunc->winref = winref;
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 67de2733d8a..6b02cb531a1 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -39,6 +39,7 @@
 #include "parser/parse_graphtable.h"
 #include "parser/parse_oper.h"
 #include "parser/parse_relation.h"
+#include "parser/parse_rpr.h"
 #include "parser/parse_target.h"
 #include "parser/parse_type.h"
 #include "parser/parser.h"
@@ -3005,6 +3006,8 @@ transformWindowDefinitions(ParseState *pstate,
 		 * And prepare the new WindowClause.
 		 */
 		wc = makeNode(WindowClause);
+		wc->rpSkipTo = ST_NONE; /* ST_NONE marks this as a non-RPR window;
+								 * overridden by transformRPR() if RPR is used */
 		wc->name = windef->name;
 		wc->refname = windef->refname;
 
@@ -3133,6 +3136,10 @@ transformWindowDefinitions(ParseState *pstate,
 											 rangeopfamily, rangeopcintype,
 											 &wc->endInRangeFunc,
 											 windef->endOffset);
+
+		/* Process Row Pattern Recognition related clauses */
+		transformRPR(pstate, wc, windef, targetlist);
+
 		wc->winref = winref;
 
 		result = lappend(result, wc);
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index 73e86b7422f..0d80b8b00c0 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -96,6 +96,14 @@ static void checkWellFormedRecursion(CteState *cstate);
 static bool checkWellFormedRecursionWalker(Node *node, CteState *cstate);
 static void checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate);
 
+/* Recursive-WITH RPR rejection */
+typedef struct
+{
+	ParseLoc	location;		/* location of first RPR window, or -1 */
+} ContainRPRContext;
+
+static bool contain_rpr_walker(Node *node, void *context);
+
 
 /*
  * transformWithClause -
@@ -164,6 +172,28 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 		CteState	cstate;
 		int			i;
 
+		/*
+		 * Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every <with list
+		 * element> in a WITH RECURSIVE clause is "potentially recursive" and
+		 * shall not contain a <row pattern common syntax>.  (PostgreSQL does
+		 * not implement <row pattern measures>, so only the common syntax
+		 * needs to be checked.)  ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5
+		 * (R010) restate the prohibition for CREATE RECURSIVE VIEW, which is
+		 * rewritten to WITH RECURSIVE by makeRecursiveViewSelect() and so
+		 * flows through here as well.
+		 */
+		foreach_node(CommonTableExpr, cte, withClause->ctes)
+		{
+			ContainRPRContext ctx;
+
+			ctx.location = -1;
+			if (contain_rpr_walker(cte->ctequery, &ctx))
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("cannot use row pattern recognition in a recursive query"),
+						parser_errposition(pstate, ctx.location));
+		}
+
 		cstate.pstate = pstate;
 		cstate.numitems = list_length(withClause->ctes);
 		cstate.items = palloc0_array(CteItem, cstate.numitems);
@@ -1268,3 +1298,27 @@ checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
 		}
 	}
 }
+
+
+/*
+ * contain_rpr_walker
+ *	  Returns true if the raw parse tree contains any <row pattern common
+ *	  syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.
+ */
+static bool
+contain_rpr_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, WindowDef))
+	{
+		WindowDef  *wd = (WindowDef *) node;
+
+		if (wd->rpCommonSyntax != NULL)
+		{
+			((ContainRPRContext *) context)->location = wd->rpCommonSyntax->location;
+			return true;
+		}
+	}
+	return raw_expression_tree_walker(node, contain_rpr_walker, context);
+}
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 30c889f505f..be0f99381ad 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -579,6 +579,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
+		case EXPR_KIND_RPR_DEFINE:
 			/* okay */
 			break;
 
@@ -627,6 +628,57 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	if (node != NULL)
 		return node;
 
+	/*----------
+	 * Qualified references in DEFINE need a tri-classification:
+	 *
+	 *	  pattern variable qualifier (e.g. UP.price): valid per
+	 *	  ISO/IEC 19075-5 6.15 / 4.16 but not yet implemented --
+	 *	  raise FEATURE_NOT_SUPPORTED.
+	 *
+	 *	  FROM-clause range variable qualifier: prohibited by
+	 *	  ISO/IEC 19075-5 6.5 -- raise SYNTAX_ERROR.
+	 *
+	 *	  any other qualifier (typo, undefined name): fall through and let
+	 *	  normal column resolution produce a sensible error.
+	 *
+	 * The quoted text reflects only the ColumnRef portion; a trailing field
+	 * selection on a composite type (e.g. ".amount" in "(A.items).amount")
+	 * lives in the surrounding A_Indirection node and is not included here.
+	 * That can be revisited when MEASURES support adds indirection-aware
+	 * traversal.
+	 *----------
+	 */
+	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+		list_length(cref->fields) != 1)
+	{
+		char	   *qualifier = strVal(linitial(cref->fields));
+		bool		is_pattern_var = false;
+
+		foreach_node(String, pv, pstate->p_rpr_pattern_vars)
+		{
+			if (strcmp(strVal(pv), qualifier) == 0)
+			{
+				is_pattern_var = true;
+				break;
+			}
+		}
+
+		if (is_pattern_var)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
+		else if (refnameNamespaceItem(pstate, NULL, qualifier,
+									  cref->location, NULL) != NULL)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
+		/* else: unknown qualifier -- fall through to normal resolution */
+	}
+
 	/*----------
 	 * The allowed syntaxes are:
 	 *
@@ -894,6 +946,30 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		}
 	}
 
+	/*
+	 * Restrict column references in a row pattern DEFINE clause.  node is now
+	 * a successfully resolved reference, so reject the two forms RPR does not
+	 * allow: a correlated reference to an outer query's column, and a
+	 * schema/catalog-qualified reference (three or more name parts).  Simple
+	 * two-part qualifiers (pattern or range variable) are handled earlier,
+	 * before resolution.
+	 */
+	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE)
+	{
+		if (IsA(node, Var) && ((Var *) node)->varlevelsup > 0)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot use outer query column in DEFINE clause"),
+					parser_errposition(pstate, cref->location));
+
+		if (list_length(cref->fields) >= 3)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("qualified expression \"%s\" is not allowed in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
+	}
+
 	return node;
 }
 
@@ -1893,6 +1969,31 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			err = _("cannot use subquery in FOR PORTION OF expression");
 			break;
 
+			/*----------
+			 * XXX SQL/RPR (ISO/IEC 19075-5 6.17.4 / 4.18.4; R020 / R010)
+			 * permits a subquery nested in a DEFINE expression provided
+			 * that:
+			 *   (a) the subquery does not itself perform row pattern
+			 *       recognition, and
+			 *   (b) the subquery does not reference a row pattern variable
+			 *       of the outer query.
+			 *
+			 * We reject all subqueries here for now.  Implementing the
+			 * case distinction would mean walking the analyzed subquery
+			 * Query tree for nested RPR window clauses to enforce (a),
+			 * and walking it for ColumnRef qualifiers matching any
+			 * ancestor's p_rpr_pattern_vars to enforce (b).  Both checks
+			 * are doable with the existing infrastructure -- they are
+			 * left as future work, not blocked on any other feature.
+			 * Until then this blanket rejection is intentional
+			 * over-rejection, not a standard fit; it subsumes both (a)
+			 * and (b) by making the subquery itself unreachable.
+			 *----------
+			 */
+		case EXPR_KIND_RPR_DEFINE:
+			err = _("cannot use subquery in DEFINE expression");
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -3255,6 +3356,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "property definition expression";
 		case EXPR_KIND_FOR_PORTION:
 			return "FOR PORTION OF";
+		case EXPR_KIND_RPR_DEFINE:
+			return "DEFINE";
 
 			/*
 			 * 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..eec16f5335a 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -48,6 +48,9 @@ static void unify_hypothetical_args(ParseState *pstate,
 									List *fargs, int numAggregatedArgs,
 									Oid *actual_arg_types, Oid *declared_arg_types);
 static Oid	FuncNameAsType(List *funcname);
+static Node *ParseRPRNavCall(ParseState *pstate, List *funcname,
+							 List *fargs, List *argnames, FuncCall *fn,
+							 int location);
 static Node *ParseComplexProjection(ParseState *pstate, const char *funcname,
 									Node *first_arg, int location);
 static Oid	LookupFuncNameInternal(ObjectType objtype, List *funcname,
@@ -121,6 +124,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	int			fgc_flags;
 	char		aggkind = 0;
 	ParseCallbackState pcbstate;
+	bool		could_be_rpr_nav = false;
 
 	/*
 	 * If there's an aggregate filter, transform it using transformWhereClause
@@ -217,6 +221,28 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 		Assert(first_arg != NULL);
 	}
 
+	/*
+	 * Inside an RPR DEFINE clause, an unqualified call to one of the row
+	 * pattern navigation names PREV/NEXT/FIRST/LAST denotes the navigation
+	 * operation, not an ordinary function.  Just note that here; the catalog
+	 * lookup is skipped and the RPRNavExpr is built at the end, after the
+	 * common decoration checks have run (see the could_be_rpr_nav handling
+	 * below).  A schema-qualified call is the explicit way to reach an
+	 * ordinary function of one of these names.
+	 */
+	if (!is_column && !proc_call &&
+		pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+		list_length(funcname) == 1)
+	{
+		const char *name = strVal(linitial(funcname));
+
+		if (strcmp(name, "prev") == 0 ||
+			strcmp(name, "next") == 0 ||
+			strcmp(name, "first") == 0 ||
+			strcmp(name, "last") == 0)
+			could_be_rpr_nav = true;
+	}
+
 	/*
 	 * Decide whether it's legitimate to consider the construct to be a column
 	 * projection.  For that, there has to be a single argument of complex
@@ -265,17 +291,32 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	 * with default arguments.
 	 */
 
-	setup_parser_errposition_callback(&pcbstate, pstate, location);
+	if (!could_be_rpr_nav)
+	{
+		setup_parser_errposition_callback(&pcbstate, pstate, location);
+
+		fdresult = func_get_detail(funcname, fargs, argnames, nargs,
+								   actual_arg_types,
+								   !func_variadic, true, proc_call,
+								   &fgc_flags,
+								   &funcid, &rettype, &retset,
+								   &nvargs, &vatype,
+								   &declared_arg_types, &argdefaults);
 
-	fdresult = func_get_detail(funcname, fargs, argnames, nargs,
-							   actual_arg_types,
-							   !func_variadic, true, proc_call,
-							   &fgc_flags,
-							   &funcid, &rettype, &retset,
-							   &nvargs, &vatype,
-							   &declared_arg_types, &argdefaults);
+		cancel_parser_errposition_callback(&pcbstate);
+	}
+	else
+	{
+		/*
+		 * A recognized navigation name skips catalog lookup entirely.  Treat
+		 * it as an ordinary function so the common wrong-kind-of-routine and
+		 * decoration checks below run with the existing messages, then route
+		 * to ParseRPRNavCall to build the RPRNavExpr.
+		 */
+		Assert(!proc_call);
 
-	cancel_parser_errposition_callback(&pcbstate);
+		fdresult = FUNCDETAIL_NORMAL;
+	}
 
 	/*
 	 * Check for various wrong-kind-of-routine cases.
@@ -656,6 +697,15 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 					 parser_errposition(pstate, location)));
 	}
 
+	/*
+	 * A recognized navigation name has now passed the common decoration and
+	 * wrong-kind checks above; build the RPRNavExpr.  No fallback to function
+	 * resolution ever happens here.
+	 */
+	if (could_be_rpr_nav)
+		return ParseRPRNavCall(pstate, funcname, fargs, argnames, fn,
+							   location);
+
 	/*
 	 * If there are default arguments, we have to include their types in
 	 * actual_arg_types for the purpose of checking generic type consistency.
@@ -2089,6 +2139,153 @@ FuncNameAsType(List *funcname)
 	return result;
 }
 
+/*
+ * ParseRPRNavCall
+ *		Recognize a row pattern navigation operation in a DEFINE clause.
+ *
+ * Inside an EXPR_KIND_RPR_DEFINE clause an unqualified call to one of the
+ * names PREV/NEXT/FIRST/LAST denotes the corresponding row pattern navigation
+ * operation (ISO/IEC 19075-5 Subclause 5.6), not an ordinary function call.
+ * The name is matched here, before any catalog lookup, with no fallback to
+ * function resolution: once it matches, decoration and argument-count
+ * violations are dedicated errors rather than letting an ordinary function of
+ * the same name take over.  A schema-qualified call (the caller restricts us
+ * to unqualified names) is the documented way to reach such a function
+ * instead.
+ *
+ * The caller routes here only after the name has matched one of the four
+ * navigation names and the common decoration/wrong-kind checks in
+ * ParseFuncOrColumn have run, so this always returns an RPRNavExpr.
+ */
+static Node *
+ParseRPRNavCall(ParseState *pstate, List *funcname, List *fargs,
+				List *argnames, FuncCall *fn, int location)
+{
+	const char *name = strVal(linitial(funcname));
+	RPRNavKind	kind;
+	const char *navname;
+	int			nargs = list_length(fargs);
+	Node	   *arg;
+	RPRNavExpr *navexpr;
+
+	/* match the parser-downcased identifier; otherwise not a navigation name */
+	if (strcmp(name, "prev") == 0)
+	{
+		kind = RPR_NAV_PREV;
+		navname = "PREV";
+	}
+	else if (strcmp(name, "next") == 0)
+	{
+		kind = RPR_NAV_NEXT;
+		navname = "NEXT";
+	}
+	else if (strcmp(name, "first") == 0)
+	{
+		kind = RPR_NAV_FIRST;
+		navname = "FIRST";
+	}
+	else if (strcmp(name, "last") == 0)
+	{
+		kind = RPR_NAV_LAST;
+		navname = "LAST";
+	}
+	else
+	{
+		/* the caller only routes here after matching one of the four names */
+		pg_unreachable();
+		return NULL;
+	}
+
+	/*
+	 * Once the name matches we never fall back to function resolution, so any
+	 * decoration that does not make sense for a navigation operation is a
+	 * hard error.  The aggregate/window decorations (agg_star, DISTINCT,
+	 * WITHIN GROUP, ORDER BY, FILTER, OVER, RESPECT/IGNORE NULLS) are already
+	 * rejected by the common path in ParseFuncOrColumn, which treated the
+	 * recognized name as an ordinary function; what remains are the
+	 * decorations that path accepts for a plain function but a navigation
+	 * operation must still reject.
+	 */
+	if (fn->func_variadic)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("cannot use VARIADIC with row pattern navigation function %s",
+						navname),
+				 parser_errposition(pstate, location)));
+	if (argnames != NIL)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("cannot use named arguments with row pattern navigation function %s",
+						navname),
+				 parser_errposition(pstate, location)));
+	/* takes a value expression and an optional offset */
+	if (nargs == 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("too few arguments for row pattern navigation function %s",
+						navname),
+				 errdetail("%s takes a value expression and an optional offset argument.",
+						   navname),
+				 parser_errposition(pstate, location)));
+	if (nargs > 2)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("too many arguments for row pattern navigation function %s",
+						navname),
+				 errdetail("%s takes a value expression and an optional offset argument.",
+						   navname),
+				 parser_errposition(pstate, location)));
+
+	/*
+	 * Resolve a still-unknown first argument to text, the same way the
+	 * anycompatible family does.  A navigation operation is not a polymorphic
+	 * function, so the old "could not determine polymorphic type" error does
+	 * not apply; an unknown literal cannot contain a column reference, so the
+	 * walker still rejects it later.
+	 */
+	arg = linitial(fargs);
+	if (exprType(arg) == UNKNOWNOID)
+		arg = coerce_to_common_type(pstate, arg, TEXTOID, navname);
+
+	navexpr = makeNode(RPRNavExpr);
+	navexpr->kind = kind;
+	navexpr->arg = (Expr *) arg;
+	navexpr->navno = -1;		/* assigned while planning */
+
+	/* an explicit offset is coerced to int8, which the executor reads */
+	if (nargs == 2)
+	{
+		Node	   *offset = lsecond(fargs);
+		Oid			offtype = exprType(offset);
+
+		if (offtype != INT8OID)
+		{
+			Node	   *newoffset;
+
+			newoffset = coerce_to_target_type(pstate, offset, offtype,
+											  INT8OID, -1, COERCION_IMPLICIT,
+											  COERCE_IMPLICIT_CAST, -1);
+			if (newoffset == NULL)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("offset argument of %s must be type %s, not type %s",
+								navname, "bigint", format_type_be(offtype)),
+						 parser_errposition(pstate, exprLocation(offset))));
+			offset = newoffset;
+		}
+		navexpr->offset_arg = (Expr *) offset;
+	}
+	else
+		navexpr->offset_arg = NULL;
+
+	/* compound_offset_arg stays NULL; define_walker flattening fills it in */
+	navexpr->resulttype = exprType(arg);
+	/* resultcollid will be set by parse_collate.c */
+	navexpr->location = location;
+
+	return (Node *) navexpr;
+}
+
 /*
  * ParseComplexProjection -
  *	  handles function calls with a single argument that is of complex type.
@@ -2848,6 +3045,9 @@ 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_RPR_DEFINE:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
new file mode 100644
index 00000000000..6292cd0547f
--- /dev/null
+++ b/src/backend/parser/parse_rpr.c
@@ -0,0 +1,631 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_rpr.c
+ *	  Handle Row Pattern Recognition clauses in parser.
+ *
+ * This file transforms RPR-related clauses from raw parse tree to planner
+ * structures during query analysis:
+ *   - Validates frame options (ROWS only, must start at CURRENT ROW, no
+ *     EXCLUDE, and CURRENT ROW is not accepted as the frame end)
+ *   - Validates PATTERN variable count (max RPR_VARID_MAX + 1)
+ *   - Transforms DEFINE clause
+ *   - Stores the PATTERN parse tree and the AFTER MATCH SKIP TO flag
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/parser/parse_rpr.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
+#include "optimizer/rpr.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_rpr.h"
+#include "parser/parse_target.h"
+
+/* DEFINE clause walker context -- see define_walker for usage. */
+typedef enum
+{
+	DEFINE_PHASE_BODY,			/* top-level DEFINE expression */
+	DEFINE_PHASE_NAV_ARG,		/* inside an outer nav's arg subtree */
+	DEFINE_PHASE_NAV_OFFSET,	/* inside an outer nav's offset_arg /
+								 * compound_offset_arg */
+} DefinePhase;
+
+typedef struct
+{
+	ParseState *pstate;
+	DefinePhase phase;
+	int			nav_count;		/* RPRNavExpr nodes seen in current nav.arg */
+	bool		has_column_ref; /* Var seen in current nav scope */
+	RPRNavKind	inner_kind;		/* kind of first nested nav in current arg */
+} DefineWalkCtx;
+
+/* Forward declarations */
+static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+									   List **varNames);
+static List *transformDefineClause(ParseState *pstate, WindowDef *windef,
+								   List **targetlist);
+static bool define_walker(Node *node, void *context);
+
+/*
+ * transformRPR
+ *		Process Row Pattern Recognition related clauses.
+ *
+ * Validates and transforms RPR clauses from parse tree to planner structures:
+ *   - Validates frame options (ROWS only, must start at CURRENT ROW, no
+ *     EXCLUDE, and CURRENT ROW is not accepted as the frame end)
+ *   - Set AFTER MATCH SKIP TO flag
+ *   - Transforms DEFINE clause into TargetEntry list
+ *   - Stores PATTERN parse tree for deparsing (optimization happens in planner)
+ *
+ * Returns early if windef has no rpCommonSyntax (non-RPR window).
+ */
+void
+transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+			 List **targetlist)
+{
+	/* Window definition must exist when called */
+	Assert(windef != NULL);
+
+	/*
+	 * Row Pattern Common Syntax clause exists?
+	 */
+	if (windef->rpCommonSyntax == NULL)
+		return;
+
+	/* Check Frame options */
+
+	/* Frame type must be "ROW" */
+	if (wc->frameOptions & FRAMEOPTION_GROUPS)
+		ereport(ERROR,
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
+				errhint("Use ROWS instead."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
+	if (wc->frameOptions & FRAMEOPTION_RANGE)
+		ereport(ERROR,
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use FRAME option RANGE with row pattern recognition"),
+				errhint("Use ROWS instead."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
+
+	/* Frame must start at current row */
+	if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
+	{
+		const char *frameType = "ROWS";
+		const char *startBound = "unknown";
+
+		/* Determine current start bound */
+		if (wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING)
+			startBound = "UNBOUNDED PRECEDING";
+		else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING)
+			startBound = "offset PRECEDING";
+		else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING)
+			startBound = "offset FOLLOWING";
+
+		/* At least one valid frame start option should be set */
+		Assert((wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) ||
+			   (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) ||
+			   (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING));
+
+		ereport(ERROR,
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
+				errdetail("Current frame starts with %s.", startBound),
+				errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
+				parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location));
+	}
+
+	/* EXCLUDE options are not permitted */
+	if ((wc->frameOptions & FRAMEOPTION_EXCLUSION) != 0)
+	{
+		const char *excludeType = "EXCLUDE";
+
+		/* Determine which EXCLUDE option was used */
+		if (wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW)
+			excludeType = "EXCLUDE CURRENT ROW";
+		else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP)
+			excludeType = "EXCLUDE GROUP";
+		else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES)
+			excludeType = "EXCLUDE TIES";
+
+		/* At least one valid exclude option should be set */
+		Assert((wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW) ||
+			   (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP) ||
+			   (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES));
+
+		ereport(ERROR,
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use EXCLUDE options with row pattern recognition"),
+				errdetail("Frame definition includes %s.", excludeType),
+				errhint("Remove the EXCLUDE clause from the window definition."),
+				parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location));
+	}
+
+	/*
+	 * The standard allows only UNBOUNDED FOLLOWING or a positive offset
+	 * FOLLOWING as the frame end.  The equivalent 0 FOLLOWING spelling is
+	 * caught at runtime in calculate_frame_offsets().
+	 */
+	if (wc->frameOptions & FRAMEOPTION_END_CURRENT_ROW)
+		ereport(ERROR,
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use CURRENT ROW as frame end with row pattern recognition"),
+				errhint("Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
+
+	/* Assign AFTER MATCH SKIP TO flag */
+	wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
+
+	/* Transform DEFINE clause into list of TargetEntry's */
+	wc->defineClause = transformDefineClause(pstate, windef, targetlist);
+
+	/* Store PATTERN parse tree for deparsing */
+	wc->rpPattern = windef->rpCommonSyntax->rpPattern;
+}
+
+/*
+ * validateRPRPatternVarCount
+ *		Validate that PATTERN variable count fits the varId range.
+ *
+ * Recursively traverses the pattern tree, collecting unique variable names.
+ * Throws an error if the number of unique variables would require a varId
+ * greater than RPR_VARID_MAX.
+ *
+ * varNames collects the unique PATTERN variable names, which is what
+ * transformColumnRef checks via p_rpr_pattern_vars to identify pattern
+ * variable qualifiers.  Cross-checking DEFINE variable names against this
+ * list is the caller's responsibility, since it only needs to run once.
+ */
+static void
+validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+						   List **varNames)
+{
+	/* Pattern node must exist - parser always provides non-NULL root */
+	Assert(node != NULL);
+
+	/*
+	 * trailing_alt is a transient grammar flag; splitRPRTrailingAlt must have
+	 * cleared it on every node before the pattern reaches parse analysis.
+	 */
+	Assert(!node->trailing_alt);
+
+	check_stack_depth();
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			/* Add variable name if not already in list */
+			{
+				bool		found = false;
+
+				foreach_node(String, varname, *varNames)
+				{
+					if (strcmp(strVal(varname), node->varName) == 0)
+					{
+						found = true;
+						break;
+					}
+				}
+				if (!found)
+				{
+					/*
+					 * Check against RPR_VARID_MAX before adding.  varId
+					 * values run 0 to RPR_VARID_MAX inclusive, so the next
+					 * varId to be assigned (the current list length) must not
+					 * exceed it.
+					 */
+					if (list_length(*varNames) > RPR_VARID_MAX)
+						ereport(ERROR,
+								errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+								errmsg("too many row pattern variables"),
+								errdetail("The maximum number of row pattern variables is %d.", RPR_VARID_MAX + 1),
+								parser_errposition(pstate,
+												   exprLocation((Node *) node)));
+
+					*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
+				}
+			}
+			break;
+
+		case RPR_PATTERN_SEQ:
+		case RPR_PATTERN_ALT:
+		case RPR_PATTERN_GROUP:
+			/* Recurse into children */
+			foreach_node(RPRPatternNode, child, node->children)
+			{
+				validateRPRPatternVarCount(pstate, child, varNames);
+			}
+			break;
+	}
+}
+
+/*
+ * transformDefineClause
+ *		Process DEFINE clause and transform ResTarget into list of TargetEntry.
+ *
+ * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
+ * Variables in DEFINE but not in PATTERN are rejected as an error.
+ *
+ * XXX Pattern variable qualified expressions in DEFINE (e.g. "A.price")
+ * are not yet supported.  Currently rejected by transformColumnRef in
+ * parse_expr.c via the p_rpr_pattern_vars check.
+ */
+static List *
+transformDefineClause(ParseState *pstate, WindowDef *windef,
+					  List **targetlist)
+{
+	List	   *defineClause = NIL;
+	List	   *patternVarNames = NIL;
+
+	/*
+	 * The grammar builds an RPCommonSyntax only for a window specification
+	 * that carries DEFINE, so the list is never empty here.
+	 */
+	Assert(windef->rpCommonSyntax->rpDefs != NULL);
+
+	/*
+	 * Validate PATTERN variable count and collect the PATTERN variable names
+	 * for transformColumnRef.
+	 */
+	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
+							   &patternVarNames);
+	pstate->p_rpr_pattern_vars = patternVarNames;
+
+	/*
+	 * Reject any DEFINE variable whose name does not appear in PATTERN.  This
+	 * cross-check only needs to run once, so it lives here in the caller
+	 * rather than in the recursive validateRPRPatternVarCount().
+	 */
+	foreach_node(ResTarget, rt, windef->rpCommonSyntax->rpDefs)
+	{
+		bool		found = false;
+
+		foreach_node(String, varname, patternVarNames)
+		{
+			if (strcmp(strVal(varname), rt->name) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+		if (!found)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+						   rt->name),
+					parser_errposition(pstate, rt->location));
+	}
+
+	/*
+	 * Check for duplicate row pattern definition variables.  The standard
+	 * requires that no two row pattern definition variable names shall be
+	 * equivalent.  Report the error at the later (duplicate) definition.
+	 */
+	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
+	{
+		foreach_node(ResTarget, prior, windef->rpCommonSyntax->rpDefs)
+		{
+			if (prior == restarget)
+				break;
+			if (strcmp(prior->name, restarget->name) == 0)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("DEFINE variable \"%s\" appears more than once",
+							   restarget->name),
+						parser_errposition(pstate,
+										   exprLocation((Node *) restarget)));
+		}
+	}
+
+	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
+	{
+		TargetEntry *teDefine;
+		Node	   *expr;
+		List	   *vars;
+
+		/*
+		 * Transform the DEFINE expression and coerce it to boolean.  We must
+		 * NOT add the whole expression to the query targetlist, because it
+		 * may contain RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only
+		 * be evaluated inside the owning WindowAgg.  Coercing here, before
+		 * pull_var_clause, keeps pull_var_clause operating on the final
+		 * expression form and surfaces a type mismatch before the targetlist
+		 * is touched.
+		 */
+		expr = transformExpr(pstate, restarget->val,
+							 EXPR_KIND_RPR_DEFINE);
+		expr = coerce_to_boolean(pstate, expr, "DEFINE");
+
+		/* Build the defineClause entry directly from the transformed expr */
+		teDefine = makeTargetEntry((Expr *) expr,
+								   list_length(defineClause) + 1,
+								   pstrdup(restarget->name),
+								   true);
+
+		/* build transformed DEFINE clause (list of TargetEntry) */
+		defineClause = lappend(defineClause, teDefine);
+
+		/*
+		 * Pull out Var nodes from the transformed expression and ensure each
+		 * one is present in the targetlist.  This is needed so the planner
+		 * propagates the referenced columns through the plan tree, making
+		 * them available to the WindowAgg's DEFINE evaluation.
+		 */
+		vars = pull_var_clause(expr, 0);
+		foreach_node(Var, var, vars)
+		{
+			bool		found = false;
+
+			foreach_node(TargetEntry, tle, *targetlist)
+			{
+				if (equal(tle->expr, var))
+				{
+					found = true;
+					break;
+				}
+			}
+			if (!found)
+			{
+				TargetEntry *newtle;
+
+				newtle = makeTargetEntry((Expr *) copyObject(var),
+										 (AttrNumber) pstate->p_next_resno++,
+										 NULL,
+										 true);
+				*targetlist = lappend(*targetlist, newtle);
+			}
+		}
+		list_free(vars);
+	}
+	pstate->p_rpr_pattern_vars = NIL;
+
+	/*
+	 * Validate DEFINE expressions: nested PREV/NEXT, column references,
+	 * compound flatten -- all in a single walk per variable.
+	 */
+	foreach_ptr(TargetEntry, te, defineClause)
+	{
+		DefineWalkCtx ctx;
+
+		ctx.pstate = pstate;
+		ctx.phase = DEFINE_PHASE_BODY;
+		ctx.nav_count = 0;
+		ctx.has_column_ref = false;
+		ctx.inner_kind = 0;
+		(void) define_walker((Node *) te->expr, &ctx);
+	}
+
+	/* mark column origins */
+	markTargetListOrigins(pstate, defineClause);
+
+	/* mark all nodes in the DEFINE clause tree with collation information */
+	assign_expr_collations(pstate, (Node *) defineClause);
+
+	return defineClause;
+}
+
+/*
+ * define_walker
+ *		Single-pass DEFINE clause validator.  At each node, enforces:
+ *
+ *		  [1] for each outer RPRNavExpr (PHASE_BODY -> PHASE_NAV_ARG):
+ *			  - nav.arg must contain at least one column reference
+ *			  - PREV/NEXT wrapping FIRST/LAST is flattened in place
+ *				to a compound kind (PREV_FIRST, PREV_LAST, NEXT_FIRST,
+ *				NEXT_LAST)
+ *			  - an inner navigation that is not nav.arg itself is
+ *				rejected as not being a direct argument
+ *			  - any other nesting is rejected (FIRST(PREV()),
+ *				PREV(PREV()), FIRST(FIRST()), three-or-more deep)
+ *		  [2] for each nav offset (PHASE_NAV_OFFSET):
+ *			  - must be a run-time constant (no column references)
+ *			  - must not contain a row pattern navigation operation
+ *
+ * Entering an outer nav, the walker walks nav.arg in PHASE_NAV_ARG to collect
+ * nesting and column-ref state, flattens a compound form or raises a nesting
+ * error, then walks the post-flatten offset(s) in PHASE_NAV_OFFSET.  A
+ * compound form's inner offset is walked in both passes: PHASE_NAV_ARG only
+ * asks whether nav.arg as a whole holds a column reference, so the offset is
+ * walked again to catch one it would have leaked.
+ *
+ * Var sightings feed the column-ref rule for the enclosing nav scope;
+ * RPRNavExpr sightings inside PHASE_NAV_ARG feed the nesting decision.
+ * The phases themselves are described where DefinePhase is declared.
+ */
+static bool
+define_walker(Node *node, void *context)
+{
+	DefineWalkCtx *ctx = (DefineWalkCtx *) context;
+
+	if (node == NULL)
+		return false;
+
+	/* Var sighting feeds the column-ref rule for the enclosing nav scope. */
+	if (IsA(node, Var) &&
+		(ctx->phase == DEFINE_PHASE_NAV_ARG ||
+		 ctx->phase == DEFINE_PHASE_NAV_OFFSET))
+		ctx->has_column_ref = true;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		RPRNavExpr *nav = (RPRNavExpr *) node;
+
+		if (ctx->phase == DEFINE_PHASE_NAV_ARG)
+		{
+			/*
+			 * Nested nav inside an outer nav.arg: record for the outer's
+			 * compound / nesting decision, then keep recursing so deeper Vars
+			 * are still observed.
+			 */
+			if (ctx->nav_count == 0)
+				ctx->inner_kind = nav->kind;
+			ctx->nav_count++;
+			return expression_tree_walker(node, define_walker, ctx);
+		}
+		else if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
+		{
+			/*
+			 * A navigation offset must be a run-time constant, so it cannot
+			 * contain a navigation operation.
+			 */
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("row pattern navigation offset cannot contain a row pattern navigation operation"),
+					errdetail("A navigation offset must be a run-time constant."),
+					parser_errposition(ctx->pstate, nav->location));
+		}
+		else
+		{
+			/*
+			 * PHASE_BODY: this is an outer nav at top level.  Walk arg first
+			 * to collect nesting / column-ref state, then validate and (for
+			 * compound forms) flatten, then walk offset(s).
+			 */
+			DefineWalkCtx saved = *ctx;
+			bool		outer_phys = (nav->kind == RPR_NAV_PREV ||
+									  nav->kind == RPR_NAV_NEXT);
+			bool		flattened = false;
+
+			ctx->phase = DEFINE_PHASE_NAV_ARG;
+			ctx->nav_count = 0;
+			ctx->has_column_ref = false;
+			ctx->inner_kind = 0;
+			(void) define_walker((Node *) nav->arg, ctx);
+
+			if (ctx->nav_count > 0)
+			{
+				bool		inner_phys = (ctx->inner_kind == RPR_NAV_PREV ||
+										  ctx->inner_kind == RPR_NAV_NEXT);
+
+				if (outer_phys && !inner_phys)
+				{
+					RPRNavExpr *inner;
+
+					/* Reject an inner nav that is not the whole argument */
+					if (!IsA(nav->arg, RPRNavExpr))
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+								errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								parser_errposition(ctx->pstate, nav->location));
+
+					/* Reject triple-or-deeper nesting; siblings caught above */
+					if (ctx->nav_count > 1)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("cannot nest row pattern navigation more than two levels deep"),
+								errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								parser_errposition(ctx->pstate, nav->location));
+
+					inner = (RPRNavExpr *) nav->arg;
+
+					if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
+						nav->kind = RPR_NAV_PREV_FIRST;
+					else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
+						nav->kind = RPR_NAV_PREV_LAST;
+					else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
+						nav->kind = RPR_NAV_NEXT_FIRST;
+					else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
+						nav->kind = RPR_NAV_NEXT_LAST;
+
+					nav->compound_offset_arg = nav->offset_arg;
+					nav->offset_arg = inner->offset_arg;
+					nav->arg = inner->arg;
+					flattened = true;
+
+					/*
+					 * The flattened argument must include a column reference,
+					 * just like the simple-nav case below.
+					 */
+					if (!ctx->has_column_ref)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("argument of row pattern navigation operation must include at least one column reference"),
+								parser_errposition(ctx->pstate, nav->location));
+				}
+				else if (!outer_phys && inner_phys)
+					ereport(ERROR,
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
+				else if (outer_phys && inner_phys)
+					ereport(ERROR,
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
+			}
+			else if (!ctx->has_column_ref)
+			{
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("argument of row pattern navigation operation must include at least one column reference"),
+						parser_errposition(ctx->pstate, nav->location));
+			}
+
+			/*
+			 * Walk offset arg(s) in PHASE_NAV_OFFSET to enforce the
+			 * constant-offset rule.  For compound forms, both the inner
+			 * (post-flatten nav->offset_arg) and outer (compound_offset_arg)
+			 * offsets must be constants; the inner's column-ref status was
+			 * not separately tracked during the PHASE_NAV_ARG walk (which
+			 * only checks that nav.arg as a whole has at least one Var), so
+			 * it is re-walked here to catch column references the inner
+			 * offset would have leaked.
+			 */
+			ctx->phase = DEFINE_PHASE_NAV_OFFSET;
+
+			if (nav->offset_arg != NULL)
+			{
+				ctx->has_column_ref = false;
+				(void) define_walker((Node *) nav->offset_arg, ctx);
+				if (ctx->has_column_ref)
+					ereport(ERROR,
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("row pattern navigation offset must be a run-time constant"),
+							parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg)));
+			}
+			if (flattened && nav->compound_offset_arg != NULL)
+			{
+				ctx->has_column_ref = false;
+				(void) define_walker((Node *) nav->compound_offset_arg, ctx);
+				if (ctx->has_column_ref)
+					ereport(ERROR,
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("row pattern navigation offset must be a run-time constant"),
+							parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg)));
+			}
+
+			*ctx = saved;
+			return false;
+		}
+	}
+
+	return expression_tree_walker(node, define_walker, ctx);
+}
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 5a636d1f179..77e27ae669e 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -644,6 +644,65 @@ typedef struct WindowFuncRunCondition
 	Expr	   *arg;
 } WindowFuncRunCondition;
 
+/*
+ * RPRNavExpr
+ *
+ * Represents a PREV/NEXT/FIRST/LAST navigation call in an RPR DEFINE clause.
+ * At expression compile time this is translated into EEOP_RPR_NAV_SET /
+ * EEOP_RPR_NAV_RESTORE opcodes rather than a normal function call.
+ *
+ * Simple navigation (PREV/NEXT/FIRST/LAST):
+ *   kind:       RPR_NAV_PREV, RPR_NAV_NEXT, RPR_NAV_FIRST, or RPR_NAV_LAST
+ *   arg:        the expression to evaluate against the target row
+ *   offset_arg: optional explicit offset expression (2-arg form); NULL for
+ *               the 1-arg form (implicit offset: 1 for PREV/NEXT, 0 for
+ *               FIRST/LAST)
+ *
+ * Compound navigation (PREV/NEXT wrapping FIRST/LAST):
+ *   kind:              RPR_NAV_PREV_FIRST, PREV_LAST, NEXT_FIRST, NEXT_LAST
+ *   arg:               the expression to evaluate against the final target row
+ *   offset_arg:        inner offset (FIRST/LAST), NULL = implicit default
+ *   compound_offset_arg: outer offset (PREV/NEXT), NULL = implicit default
+ *
+ * Compound target computation:
+ *   PREV_FIRST: (match_start + inner) - outer
+ *   NEXT_FIRST: (match_start + inner) + outer
+ *   PREV_LAST:  (currentpos  - inner) - outer
+ *   NEXT_LAST:  (currentpos  - inner) + outer
+ */
+typedef enum RPRNavKind
+{
+	RPR_NAV_PREV,				/* offset default: 1 */
+	RPR_NAV_NEXT,				/* offset default: 1 */
+	RPR_NAV_FIRST,				/* offset default: 0 */
+	RPR_NAV_LAST,				/* offset default: 0 */
+	/* compound: outer(inner(arg)) */
+	RPR_NAV_PREV_FIRST,			/* (offset, compound_offset) default: (0, 1) */
+	RPR_NAV_PREV_LAST,			/* (offset, compound_offset) default: (0, 1) */
+	RPR_NAV_NEXT_FIRST,			/* (offset, compound_offset) default: (0, 1) */
+	RPR_NAV_NEXT_LAST,			/* (offset, compound_offset) default: (0, 1) */
+} RPRNavKind;
+
+typedef struct RPRNavExpr
+{
+	Expr		xpr;
+	RPRNavKind	kind;			/* navigation kind */
+	Expr	   *arg;			/* argument expression */
+	Expr	   *offset_arg;		/* offset expression, or NULL for default */
+	Expr	   *compound_offset_arg;	/* outer offset for compound nav, or
+										 * NULL for its default */
+
+	/* unique ID within the WindowAgg; -1 until the planner assigns it */
+	int			navno pg_node_attr(query_jumble_ignore);
+
+	/* result type (same as arg's type) */
+	Oid			resulttype pg_node_attr(query_jumble_ignore);
+	/* OID of collation of result */
+	Oid			resultcollid pg_node_attr(query_jumble_ignore);
+	/* token location, or -1 if unknown */
+	ParseLoc	location;
+} RPRNavExpr;
+
 /*
  * MergeSupportFunc
  *
diff --git a/src/include/parser/parse_rpr.h b/src/include/parser/parse_rpr.h
new file mode 100644
index 00000000000..7fab6f292aa
--- /dev/null
+++ b/src/include/parser/parse_rpr.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_rpr.h
+ *	  handle Row Pattern Recognition in parser
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/parser/parse_rpr.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PARSE_RPR_H
+#define PARSE_RPR_H
+
+#include "parser/parse_node.h"
+
+extern void transformRPR(ParseState *pstate, WindowClause *wc,
+						 WindowDef *windef, List **targetlist);
+
+#endif							/* PARSE_RPR_H */
-- 
2.43.0

