v47-0002-Row-pattern-recognition-patch-parse-analysis.patch

application/octet-stream

Filename: v47-0002-Row-pattern-recognition-patch-parse-analysis.patch
Type: application/octet-stream
Part: 1
Message: Re: Row pattern recognition
From 73ccc0a14771ca64110aeea5303d134d94fadb42 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 2/9] Row pattern recognition patch (parse/analysis).

---
 src/backend/nodes/copyfuncs.c     |  27 ++
 src/backend/nodes/equalfuncs.c    |  35 ++
 src/backend/nodes/outfuncs.c      |  51 +++
 src/backend/nodes/readfuncs.c     |  85 +++++
 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 |  12 +-
 src/backend/parser/parse_expr.c   |  42 +++
 src/backend/parser/parse_func.c   |  86 ++++-
 src/backend/parser/parse_rpr.c    | 594 ++++++++++++++++++++++++++++++
 src/include/nodes/primnodes.h     |  54 +++
 src/include/parser/parse_clause.h |   3 +
 src/include/parser/parse_rpr.h    |  22 ++
 15 files changed, 1017 insertions(+), 6 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..e67ad39bdb8 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(from->numVars * sizeof(char *));
+	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(from->numElements * sizeof(RPRPatternElement));
+	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/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3d1a1adf86e..328199918b8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "miscadmin.h"
+#include "nodes/plannodes.h"
 #include "utils/datum.h"
 
 
@@ -149,6 +150,40 @@ _equalBitmapset(const Bitmapset *a, const Bitmapset *b)
 	return bms_equal(a, b);
 }
 
+static bool
+_equalRPRPattern(const RPRPattern *a, const RPRPattern *b)
+{
+	COMPARE_SCALAR_FIELD(numVars);
+	COMPARE_SCALAR_FIELD(maxDepth);
+	COMPARE_SCALAR_FIELD(numElements);
+
+	/* Compare varNames array */
+	if (a->numVars > 0)
+	{
+		if (a->varNames == NULL || b->varNames == NULL)
+			return false;
+		for (int i = 0; i < a->numVars; i++)
+		{
+			if (strcmp(a->varNames[i], b->varNames[i]) != 0)
+				return false;
+		}
+	}
+
+	/* Compare elements array */
+	if (a->numElements > 0)
+	{
+		if (a->elements == NULL || b->elements == NULL)
+			return false;
+		if (memcmp(a->elements, b->elements,
+				   a->numElements * sizeof(RPRPatternElement)) != 0)
+			return false;
+	}
+
+	COMPARE_SCALAR_FIELD(isAbsorbable);
+
+	return true;
+}
+
 /*
  * Lists are handled specially
  */
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 953c5797c5d..e6ea9ce22d9 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,56 @@ _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 */
+	appendStringInfoString(str, " :elements");
+	if (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);
+		}
+	}
+	else
+		appendStringInfoString(str, " <>");
+
+	WRITE_BOOL_FIELD(isAbsorbable);
+}
+
 
 /*
  * outNode -
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index b6b2ce6c792..5bbde5bcad2 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,90 @@ _readExtensibleNode(void)
 	READ_DONE();
 }
 
+static RPRPattern *
+_readRPRPattern(void)
+{
+	READ_LOCALS(RPRPattern);
+
+	READ_INT_FIELD(numVars);
+	READ_INT_FIELD(maxDepth);
+	READ_INT_FIELD(numElements);
+
+	/* Read varNames array */
+	token = pg_strtok(&length); /* skip :varNames */
+	token = pg_strtok(&length); /* get '(' or '<>' */
+	if (local_node->numVars > 0 && token[0] == '(')
+	{
+		local_node->varNames = palloc(local_node->numVars * sizeof(char *));
+		for (int i = 0; i < local_node->numVars; i++)
+		{
+			token = pg_strtok(&length);
+			local_node->varNames[i] = debackslash(token, length);
+		}
+		token = pg_strtok(&length); /* skip ')' */
+	}
+	else
+	{
+		local_node->varNames = NULL;
+	}
+
+	/* Read elements array */
+	token = pg_strtok(&length); /* skip :elements */
+	token = pg_strtok(&length); /* get '(' or '<>' */
+	if (local_node->numElements > 0 && token[0] == '(')
+	{
+		local_node->elements = palloc0(local_node->numElements * sizeof(RPRPatternElement));
+		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(&length);
+			varId = atoi(token);
+			token = pg_strtok(&length);
+			depth = atoi(token);
+			token = pg_strtok(&length);
+			flags = atoi(token);
+			token = pg_strtok(&length);
+			min = atoi(token);
+			token = pg_strtok(&length);
+			max = atoi(token);
+			token = pg_strtok(&length);
+			next = atoi(token);
+			token = pg_strtok(&length);
+			jump = atoi(token);
+			token = pg_strtok(&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(&length); /* get '(' */
+		}
+	}
+	else
+	{
+		local_node->elements = NULL;
+	}
+
+	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 acb933392de..b16e54d6e31 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 4270c2382c4..6c443a31e79 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"
@@ -88,8 +89,6 @@ static void checkExprIsVarFree(ParseState *pstate, Node *n,
 							   const char *constructName);
 static TargetEntry *findTargetlistEntrySQL92(ParseState *pstate, Node *node,
 											 List **tlist, ParseExprKind exprKind);
-static TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
-											 List **tlist, ParseExprKind exprKind);
 static int	get_matching_location(int sortgroupref,
 								  List *sortgrouprefs, List *exprs);
 static List *resolve_unique_index_expr(ParseState *pstate, InferClause *infer,
@@ -101,7 +100,6 @@ static Node *transformFrameOffset(ParseState *pstate, int frameOptions,
 								  Oid rangeopfamily, Oid rangeopcintype, Oid *inRangeFunc,
 								  Node *clause);
 
-
 /*
  * transformFromClause -
  *	  Process the FROM clause and add items to the query's range table,
@@ -2310,7 +2308,7 @@ findTargetlistEntrySQL92(ParseState *pstate, Node *node, List **tlist,
  * tlist	the target list (passed by reference so we can append to it)
  * exprKind identifies clause type being processed
  */
-static TargetEntry *
+TargetEntry *
 findTargetlistEntrySQL99(ParseState *pstate, Node *node, List **tlist,
 						 ParseExprKind exprKind)
 {
@@ -3033,6 +3031,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;
 
@@ -3161,6 +3161,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_expr.c b/src/backend/parser/parse_expr.c
index c3c7aa29720..f145342e1fb 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,42 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	if (node != NULL)
 		return node;
 
+	/*
+	 * Qualified column references in DEFINE are not supported.  This covers
+	 * both FROM-clause range variables (prohibited by §6.5) and pattern
+	 * variable qualified names (e.g. UP.price), which are valid per §4.16
+	 * but not yet implemented.
+	 */
+	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+		list_length(cref->fields) != 1)
+	{
+		char	   *qualifier = strVal(linitial(cref->fields));
+		ListCell   *lc;
+		bool		is_pattern_var = false;
+
+		foreach(lc, pstate->p_rpr_pattern_vars)
+		{
+			if (strcmp(strVal(lfirst(lc)), qualifier) == 0)
+			{
+				is_pattern_var = true;
+				break;
+			}
+		}
+
+		if (is_pattern_var)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("pattern variable qualified column reference \"%s\" is not supported in DEFINE clause",
+							NameListToString(cref->fields)),
+					 parser_errposition(pstate, cref->location)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("range variable qualified column reference \"%s\" is not allowed in DEFINE clause",
+							NameListToString(cref->fields)),
+					 parser_errposition(pstate, cref->location)));
+	}
+
 	/*----------
 	 * The allowed syntaxes are:
 	 *
@@ -1892,6 +1929,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use subquery in FOR PORTION OF expression");
 			break;
+		case EXPR_KIND_RPR_DEFINE:
+			err = _("cannot use subquery in DEFINE expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3255,6 +3295,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 35ff6427147..1eabcda02a1 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -31,6 +31,7 @@
 #include "parser/parse_target.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
+#include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
@@ -756,8 +757,88 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	if (retset)
 		check_srf_call_placement(pstate, last_srf, location);
 
+	/*
+	 * RPR navigation functions (PREV/NEXT/FIRST/LAST) are only meaningful
+	 * inside a WINDOW DEFINE clause.
+	 *
+	 * Outside DEFINE, these polymorphic placeholders can shadow column access
+	 * via functional notation (e.g., last(f) meaning f.last). For the 1-arg
+	 * form, try column projection first; if that succeeds, use it instead.
+	 * Otherwise, report a clear parser error.
+	 */
+	if (fdresult == FUNCDETAIL_NORMAL &&
+		pstate->p_expr_kind != EXPR_KIND_RPR_DEFINE &&
+		(funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
+		 funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8 ||
+		 funcid == F_FIRST_ANYELEMENT || funcid == F_LAST_ANYELEMENT ||
+		 funcid == F_FIRST_ANYELEMENT_INT8 || funcid == F_LAST_ANYELEMENT_INT8))
+	{
+		/* 1-arg form: try column projection before erroring out */
+		if (nargs == 1 && !agg_star && !agg_distinct && over == NULL &&
+			list_length(funcname) == 1)
+		{
+			Node	   *projection;
+
+			projection = ParseComplexProjection(pstate,
+												strVal(linitial(funcname)),
+												linitial(fargs),
+												location);
+			if (projection)
+				return projection;
+		}
+
+		/* Not a column projection -- report error */
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("cannot use %s outside a DEFINE clause",
+						NameListToString(funcname)),
+				 parser_errposition(pstate, location)));
+	}
+
 	/* build the appropriate output structure */
-	if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
+	if (fdresult == FUNCDETAIL_NORMAL &&
+		pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+		(funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
+		 funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8 ||
+		 funcid == F_FIRST_ANYELEMENT || funcid == F_LAST_ANYELEMENT ||
+		 funcid == F_FIRST_ANYELEMENT_INT8 || funcid == F_LAST_ANYELEMENT_INT8))
+	{
+		/*
+		 * RPR navigation functions (PREV/NEXT/FIRST/LAST) are compiled into
+		 * EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE opcodes instead of a normal
+		 * function call.  Represent them as RPRNavExpr nodes so that later
+		 * stages can identify them without relying on funcid comparisons.
+		 */
+		RPRNavKind	kind;
+		bool		has_offset;
+		RPRNavExpr *navexpr;
+
+		if (funcid == F_PREV_ANYELEMENT || funcid == F_PREV_ANYELEMENT_INT8)
+			kind = RPR_NAV_PREV;
+		else if (funcid == F_NEXT_ANYELEMENT || funcid == F_NEXT_ANYELEMENT_INT8)
+			kind = RPR_NAV_NEXT;
+		else if (funcid == F_FIRST_ANYELEMENT || funcid == F_FIRST_ANYELEMENT_INT8)
+			kind = RPR_NAV_FIRST;
+		else
+			kind = RPR_NAV_LAST;
+
+		has_offset = (funcid == F_PREV_ANYELEMENT_INT8 ||
+					  funcid == F_NEXT_ANYELEMENT_INT8 ||
+					  funcid == F_FIRST_ANYELEMENT_INT8 ||
+					  funcid == F_LAST_ANYELEMENT_INT8);
+
+		navexpr = makeNode(RPRNavExpr);
+
+		navexpr->kind = kind;
+		navexpr->arg = (Expr *) linitial(fargs);
+		navexpr->offset_arg = has_offset ? (Expr *) lsecond(fargs) : NULL;
+		navexpr->resulttype = rettype;
+		/* resultcollid will be set by parse_collate.c */
+		navexpr->location = location;
+
+		retval = (Node *) navexpr;
+	}
+	else if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		FuncExpr   *funcexpr = makeNode(FuncExpr);
 
@@ -2789,6 +2870,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..f56b7db5bc8
--- /dev/null
+++ b/src/backend/parser/parse_rpr.c
@@ -0,0 +1,594 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 (must start at CURRENT ROW, no EXCLUDE)
+ *   - Validates PATTERN variable count (max RPR_VARID_MAX)
+ *   - Transforms DEFINE clause into TargetEntry list
+ *   - Stores PATTERN/SKIP TO/INITIAL clauses for planner
+ *
+ * Pattern optimization and compilation to NFA bytecode happens later
+ * in the planner (see optimizer/plan/rpr.c).
+ *
+ * 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"
+
+/* Forward declarations */
+static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+									   List *rpDefs, List **varNames);
+static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
+								   WindowDef *windef, List **targetlist);
+static void check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate);
+static bool check_rpr_nav_nesting_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 (must start at CURRENT ROW, no EXCLUDE)
+ *   - Stores AFTER MATCH SKIP TO clause
+ *   - Stores SEEK/INITIAL clause
+ *   - Transforms DEFINE clause into TargetEntry list
+ *   - Stores PATTERN AST 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)));
+	}
+
+	/* Transform AFTER MATCH SKIP TO clause */
+	wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
+
+	/* Transform SEEK or INITIAL clause */
+	wc->initial = windef->rpCommonSyntax->initial;
+
+	/* Transform DEFINE clause into list of TargetEntry's */
+	wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist);
+
+	/* Store PATTERN AST for deparsing */
+	wc->rpPattern = windef->rpCommonSyntax->rpPattern;
+}
+
+/*
+ * validateRPRPatternVarCount
+ *		Validate that PATTERN variables don't exceed RPR_VARID_MAX.
+ *
+ * Recursively traverses the pattern tree, collecting unique variable names.
+ * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
+ *
+ * If rpDefs is non-NULL, DEFINE variable names are also collected into
+ * varNames so that transformColumnRef can distinguish pattern variable
+ * qualifiers from FROM-clause range variables.
+ *
+ * varNames is both input and output: existing names are preserved, new ones added.
+ */
+static void
+validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+						   List *rpDefs, List **varNames)
+{
+	ListCell   *lc;
+
+	/* Pattern node must exist - parser always provides non-NULL root */
+	Assert(node != NULL);
+
+	check_stack_depth();
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			/* Add variable name if not already in list */
+			{
+				bool		found = false;
+
+				foreach(lc, *varNames)
+				{
+					if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+					{
+						found = true;
+						break;
+					}
+				}
+				if (!found)
+				{
+					/* Check against RPR_VARID_MAX before adding */
+					if (list_length(*varNames) >= RPR_VARID_MAX)
+						ereport(ERROR,
+								(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+								 errmsg("too many pattern variables"),
+								 errdetail("Maximum is %d.", RPR_VARID_MAX),
+								 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(lc, node->children)
+			{
+				validateRPRPatternVarCount(pstate, (RPRPatternNode *) lfirst(lc),
+										   NULL, varNames);
+			}
+			break;
+	}
+
+	/*
+	 * After the top-level call, also collect DEFINE variable names that are
+	 * not already in the list.  This is only done once at the outermost
+	 * recursion level, detected by rpDefs being non-NULL (recursive calls
+	 * pass NULL).
+	 */
+	if (rpDefs)
+	{
+		foreach(lc, rpDefs)
+		{
+			ResTarget  *rt = (ResTarget *) lfirst(lc);
+			ListCell   *lc2;
+			bool		found = false;
+
+			foreach(lc2, *varNames)
+			{
+				if (strcmp(strVal(lfirst(lc2)), 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)));
+		}
+	}
+}
+
+/*
+ * transformDefineClause
+ *		Process DEFINE clause and transform ResTarget into list of TargetEntry.
+ *
+ * First:
+ *   1. Validates PATTERN variable count and collects RPR variable names
+ *
+ * Then for each DEFINE variable:
+ *   2. Checks for duplicate variable names in DEFINE clause
+ *   3. Transforms expression via transformExpr() and ensures referenced
+ *      Var nodes are present in the query targetlist (via pull_var_clause)
+ *   4. Creates defineClause entry with proper resname (pattern variable name)
+ *   5. Coerces expressions to boolean type
+ *   6. Marks column origins and assigns collation information
+ *
+ * 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 column references 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, WindowClause *wc, WindowDef *windef,
+					  List **targetlist)
+{
+	ListCell   *lc,
+			   *l;
+	ResTarget  *restarget,
+			   *r;
+	List	   *restargets;
+	List	   *defineClause = NIL;
+	char	   *name;
+	List	   *patternVarNames = NIL;
+
+	/*
+	 * If Row Definition Common Syntax exists, DEFINE clause must exist. (the
+	 * raw parser should have already checked it.)
+	 */
+	Assert(windef->rpCommonSyntax->rpDefs != NULL);
+
+	/*
+	 * Validate PATTERN variable count and collect all RPR variable names
+	 * (PATTERN + DEFINE) for use in transformColumnRef.
+	 */
+	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
+							   windef->rpCommonSyntax->rpDefs,
+							   &patternVarNames);
+	pstate->p_rpr_pattern_vars = patternVarNames;
+
+	/*
+	 * Check for duplicate row pattern definition variables.  The standard
+	 * requires that no two row pattern definition variable names shall be
+	 * equivalent.
+	 */
+	restargets = NIL;
+	foreach(lc, windef->rpCommonSyntax->rpDefs)
+	{
+		TargetEntry *teDefine;
+
+		restarget = (ResTarget *) lfirst(lc);
+		name = restarget->name;
+
+		foreach(l, restargets)
+		{
+			char	   *n;
+
+			r = (ResTarget *) lfirst(l);
+			n = r->name;
+
+			if (!strcmp(n, name))
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("DEFINE variable \"%s\" appears more than once",
+								name),
+						 parser_errposition(pstate, exprLocation((Node *) r))));
+		}
+
+		restargets = lappend(restargets, restarget);
+
+		/*
+		 * Transform the DEFINE expression.  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.
+		 *
+		 * Instead, we transform the expression directly and only ensure that
+		 * the individual Var nodes it references are present in the
+		 * targetlist, so the planner can propagate the referenced columns.
+		 */
+		{
+			Node	   *expr;
+			List	   *vars;
+			ListCell   *lc2;
+
+			expr = transformExpr(pstate, restarget->val,
+								 EXPR_KIND_RPR_DEFINE);
+
+			/*
+			 * 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(lc2, vars)
+			{
+				Var		   *var = (Var *) lfirst(lc2);
+				bool		found = false;
+				ListCell   *tl;
+
+				foreach(tl, *targetlist)
+				{
+					TargetEntry *tle = (TargetEntry *) lfirst(tl);
+
+					if (IsA(tle->expr, Var) &&
+						((Var *) tle->expr)->varno == var->varno &&
+						((Var *) tle->expr)->varattno == var->varattno)
+					{
+						found = true;
+						break;
+					}
+				}
+				if (!found)
+				{
+					TargetEntry *newtle;
+
+					newtle = makeTargetEntry((Expr *) copyObject(var),
+											 list_length(*targetlist) + 1,
+											 NULL,
+											 true);
+					*targetlist = lappend(*targetlist, newtle);
+				}
+			}
+			list_free(vars);
+
+			/* Build the defineClause entry directly from the transformed expr */
+			teDefine = makeTargetEntry((Expr *) expr,
+									   list_length(defineClause) + 1,
+									   pstrdup(name),
+									   true);
+		}
+
+		/* build transformed DEFINE clause (list of TargetEntry) */
+		defineClause = lappend(defineClause, teDefine);
+	}
+	list_free(restargets);
+	pstate->p_rpr_pattern_vars = NIL;
+
+	/*
+	 * Make sure that the row pattern definition search condition is a boolean
+	 * expression.
+	 */
+	foreach_ptr(TargetEntry, te, defineClause)
+		te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
+
+	/* check for nested PREV/NEXT and missing column references */
+	foreach_ptr(TargetEntry, te, defineClause)
+		(void) check_rpr_nav_nesting_walker((Node *) te->expr, pstate);
+
+	/* 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;
+}
+
+/*
+ * check_rpr_nav_expr
+ *		Validate a single RPRNavExpr node by walking its arg and offset_arg
+ *		subtrees in a single pass each.  Check for illegal nesting, missing
+ *		column references, and non-constant offset expressions.
+ *
+ * Nesting rules (SQL standard 5.6.4):
+ *   - PREV/NEXT wrapping FIRST/LAST: allowed (compound navigation)
+ *   - FIRST/LAST wrapping PREV/NEXT: prohibited
+ *   - Same-category nesting (PREV inside PREV, FIRST inside FIRST, etc.):
+ *     prohibited
+ */
+typedef struct
+{
+	int			nav_count;		/* number of RPRNavExpr nodes found */
+	bool		has_column_ref; /* Var found */
+	RPRNavKind	inner_kind;		/* kind of first (outermost) nested RPRNavExpr */
+} NavCheckResult;
+
+static bool
+nav_check_walker(Node *node, void *context)
+{
+	NavCheckResult *result = (NavCheckResult *) context;
+
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+	{
+		if (result->nav_count == 0)
+			result->inner_kind = ((RPRNavExpr *) node)->kind;
+		result->nav_count++;
+	}
+	if (IsA(node, Var))
+		result->has_column_ref = true;
+
+	return expression_tree_walker(node, nav_check_walker, context);
+}
+
+static void
+check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
+{
+	NavCheckResult result;
+	bool		outer_is_physical = (nav->kind == RPR_NAV_PREV ||
+									 nav->kind == RPR_NAV_NEXT);
+
+	/* Check arg subtree: nesting + column reference in one walk */
+	memset(&result, 0, sizeof(result));
+	(void) nav_check_walker((Node *) nav->arg, &result);
+
+	if (result.nav_count > 0)
+	{
+		bool		inner_is_physical = (result.inner_kind == RPR_NAV_PREV ||
+										 result.inner_kind == RPR_NAV_NEXT);
+
+		if (outer_is_physical && !inner_is_physical)
+		{
+			/*
+			 * PREV/NEXT wrapping FIRST/LAST: compound navigation per SQL
+			 * standard 5.6.4.  Flatten the nested RPRNavExpr into a single
+			 * compound node.  The inner RPRNavExpr must be the direct arg of
+			 * the outer; expressions like PREV(val + FIRST(v)) are not valid
+			 * compound navigation.
+			 */
+			RPRNavExpr *inner;
+
+			/* Reject triple-or-deeper nesting (e.g. PREV(FIRST(PREV(x)))) */
+			if (result.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(pstate, nav->location)));
+
+			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(pstate, nav->location)));
+			inner = (RPRNavExpr *) nav->arg;
+
+			/* Determine compound kind */
+			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;
+
+			/* Move outer offset to compound_offset_arg */
+			nav->compound_offset_arg = nav->offset_arg;
+
+			/* Move inner offset and arg up */
+			nav->offset_arg = inner->offset_arg;
+			nav->arg = inner->arg;
+
+			/* No further nesting check needed - already validated */
+			return;
+		}
+		else if (!outer_is_physical && inner_is_physical)
+		{
+			/* FIRST/LAST wrapping PREV/NEXT: prohibited by standard */
+			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(pstate, nav->location)));
+		}
+		else if (outer_is_physical && inner_is_physical)
+		{
+			/* PREV/NEXT wrapping PREV/NEXT: prohibited */
+			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(pstate, nav->location)));
+		}
+		else
+		{
+			/* FIRST/LAST wrapping FIRST/LAST: prohibited */
+			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(pstate, nav->location)));
+		}
+	}
+	if (!result.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(pstate, nav->location)));
+
+	/* Check offset_arg: column ref + volatile in one walk */
+	if (nav->offset_arg != NULL)
+	{
+		memset(&result, 0, sizeof(result));
+		(void) nav_check_walker((Node *) nav->offset_arg, &result);
+
+		if (result.has_column_ref ||
+			contain_volatile_functions((Node *) nav->offset_arg))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("row pattern navigation offset must be a run-time constant"),
+					 parser_errposition(pstate, nav->location)));
+	}
+}
+
+/*
+ * check_rpr_nav_nesting_walker
+ *		Walk the DEFINE clause expression tree and validate each RPRNavExpr.
+ */
+static bool
+check_rpr_nav_nesting_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+	{
+		check_rpr_nav_expr((RPRNavExpr *) node, (ParseState *) context);
+		/* don't recurse into arg; nesting already checked above */
+		return false;
+	}
+	return expression_tree_walker(node, check_rpr_nav_nesting_walker, context);
+}
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 7977ee24783..656c552b0a8 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -648,6 +648,60 @@ 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,
+	RPR_NAV_NEXT,
+	RPR_NAV_FIRST,
+	RPR_NAV_LAST,
+	/* compound: outer(inner(arg)) */
+	RPR_NAV_PREV_FIRST,
+	RPR_NAV_PREV_LAST,
+	RPR_NAV_NEXT_FIRST,
+	RPR_NAV_NEXT_LAST
+} 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 if simple */
+	Oid			resulttype;		/* result type (same as arg's type) */
+	/* 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_clause.h b/src/include/parser/parse_clause.h
index fe234611007..8aaac881f2b 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -52,6 +52,9 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle,
 extern Index assignSortGroupRef(TargetEntry *tle, List *tlist);
 extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList);
 
+extern TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
+											 List **tlist, ParseExprKind exprKind);
+
 /* functions in parse_jsontable.c */
 extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt);
 
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