nocfbot-0002-Unify-DEFINE-walkers-reject-volatile.txt

text/plain

Filename: nocfbot-0002-Unify-DEFINE-walkers-reject-volatile.txt
Type: text/plain
Part: 1
Message: Re: Row pattern recognition
From 84cd98072184ec63bb2f79477f03bbe81c659b83 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Mon, 4 May 2026 21:37:59 +0900
Subject: [PATCH 02/11] Unify RPR DEFINE walkers and reject volatile callees

Planner/executor: shared nav_traversal_walker + visit_nav_plan /
visit_nav_exec replace four pre-existing walkers; each DEFINE
variable is walked once per phase.

Parser: single define_walker with phase tag (BODY / NAV_ARG /
NAV_OFFSET) replaces two pre-existing walkers and enforces all rules
in one traversal.  Volatile and NextValueExpr are rejected (RPR's NFA
may re-evaluate predicates during backtracking, making volatile
results non-deterministic; STABLE and IMMUTABLE are accepted).

The constant-offset rule now also catches column references in the
inner offset of compound forms.
---
 src/backend/commands/explain.c                |   8 +-
 src/backend/executor/nodeWindowAgg.c          | 282 +++++-------
 src/backend/optimizer/plan/createplan.c       | 434 +++++++++---------
 src/backend/optimizer/plan/rpr.c              |  34 ++
 src/backend/parser/parse_rpr.c                | 393 ++++++++++------
 src/include/optimizer/rpr.h                   |  22 +
 src/test/regress/expected/rpr.out             |  74 +--
 src/test/regress/expected/rpr_explain.out     |  11 +-
 src/test/regress/expected/rpr_integration.out |  40 +-
 src/test/regress/sql/rpr.sql                  |  35 +-
 src/test/regress/sql/rpr_explain.sql          |   7 +-
 src/test/regress/sql/rpr_integration.sql      |  23 +-
 src/tools/pgindent/typedefs.list              |  10 +-
 13 files changed, 758 insertions(+), 615 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 99de36b57f2..1a754bcdac5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3312,8 +3312,12 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 											es);
 						break;
 					case RPR_NAV_OFFSET_FIXED:
-						ExplainPropertyInteger("Nav Mark Lookahead", NULL,
-											   firstOffset, es);
+						if (firstOffset == INT64_MAX)
+							ExplainPropertyText("Nav Mark Lookahead", "infinite",
+												es);
+						else
+							ExplainPropertyInteger("Nav Mark Lookahead", NULL,
+												   firstOffset, es);
 						break;
 					default:
 						elog(ERROR, "unrecognized RPR nav offset kind: %d",
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 93cb9bbdd11..af2351bccb8 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -246,8 +246,7 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
 static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
 
 /* Forward declarations - navigation offset evaluation */
-static void eval_nav_max_offset(WindowAggState *winstate, List *defineClause);
-static void eval_nav_first_offset(WindowAggState *winstate, List *defineClause);
+static void eval_define_offsets(WindowAggState *winstate, List *defineClause);
 
 /*
  * Not null info bit array consists of 2-bit items
@@ -2579,12 +2578,10 @@ ExecWindowAgg(PlanState *pstate)
 			{
 				int64		firstreach;
 
-				if (winstate->navFirstOffset > -winstate->nfaContext->matchStartRow)
-					firstreach = winstate->nfaContext->matchStartRow
-						+ winstate->navFirstOffset;
-				else
-					firstreach = 0;
-				navmarkpos = Min(navmarkpos, firstreach);
+				if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
+										 winstate->navFirstOffset,
+										 &firstreach))
+					navmarkpos = Min(navmarkpos, Max(firstreach, 0));
 			}
 
 			if (navmarkpos > winstate->nav_winobj->markpos)
@@ -3037,17 +3034,13 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->rpSkipTo = node->rpSkipTo;
 	/* Set up row pattern recognition PATTERN clause (compiled NFA) */
 	winstate->rpPattern = node->rpPattern;
-	/* Set up nav offsets for tuplestore trim */
+	/* Set up nav offsets for tuplestore trim; resolve any NEEDS_EVAL kinds */
 	winstate->navMaxOffsetKind = node->navMaxOffsetKind;
 	winstate->navMaxOffset = node->navMaxOffset;
-	if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
-		eval_nav_max_offset(winstate, node->defineClause);
 	winstate->hasFirstNav = node->hasFirstNav;
 	winstate->navFirstOffsetKind = node->navFirstOffsetKind;
 	winstate->navFirstOffset = node->navFirstOffset;
-	if (winstate->hasFirstNav &&
-		winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
-		eval_nav_first_offset(winstate, node->defineClause);
+	eval_define_offsets(winstate, node->defineClause);
 
 	/* Copy match_start dependency bitmapset for per-context evaluation */
 	winstate->defineMatchStartDependent = bms_copy(node->defineMatchStartDependent);
@@ -3997,42 +3990,64 @@ eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
 typedef struct
 {
 	WindowAggState *winstate;
-	int64		maxOffset;
-	bool		overflow;		/* true if overflow detected */
-} EvalNavMaxContext;
+	int64		maxOffset;		/* max backward-reach offset across all nav
+								 * exprs */
+	bool		maxOverflow;	/* true if backward-reach overflow detected */
+	int64		minFirstOffset; /* min forward-from-match_start offset; may be
+								 * negative (PREV_FIRST: inner - outer < 0) */
+} EvalDefineOffsetsContext;
 
 /*
- * eval_nav_max_offset_walker
- *		Walk expression tree evaluating backward-reach offsets at runtime.
+ * visit_nav_exec
+ *		nav_traversal_walker callback (NavVisitFn) for the executor side.
+ *		At each RPRNavExpr, evaluates the nav's offset expression(s) at
+ *		runtime via eval_nav_offset_helper and accumulates:
+ *
+ *		  - maxOffset (backward reach): PREV, LAST-with-offset, compound
+ *			PREV_LAST (sets maxOverflow on int64 overflow), compound
+ *			NEXT_LAST (= max(inner - outer, 0))
+ *		  - minFirstOffset (forward reach from match_start): FIRST,
+ *			compound PREV_FIRST (= inner - outer, may be negative),
+ *			compound NEXT_FIRST (= inner + outer, clamped to INT64_MAX on
+ *			overflow; always >= 0 so never updates minFirstOffset in practice)
  *
- * Handles simple PREV, LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
+ * Counterpart of visit_nav_plan but using runtime evaluation instead of
+ * Const folding; runs only for offsets the planner marked NEEDS_EVAL.
+ * Match-start dependency is not recomputed here -- the planner's bitmapset
+ * is reused via winstate->defineMatchStartDependent.
  */
-static bool
-eval_nav_max_offset_walker(Node *node, void *ctx)
+static void
+visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 {
-	EvalNavMaxContext *context = (EvalNavMaxContext *) ctx;
-
-	if (node == NULL)
-		return false;
+	EvalDefineOffsetsContext *context = (EvalDefineOffsetsContext *) t->data;
 
-	/* Short-circuit if overflow already detected */
-	if (context->overflow)
-		return false;
+	/*
+	 * Parser guarantee (mirrors visit_nav_plan): nav's direct children are
+	 * never RPRNavExpr -- compound nesting is flattened in place and any
+	 * other nesting is rejected.  Outer-kind dispatch is sufficient.
+	 */
+	Assert(nav->arg == NULL || !IsA(nav->arg, RPRNavExpr));
+	Assert(nav->offset_arg == NULL || !IsA(nav->offset_arg, RPRNavExpr));
+	Assert(nav->compound_offset_arg == NULL ||
+		   !IsA(nav->compound_offset_arg, RPRNavExpr));
 
-	if (IsA(node, RPRNavExpr))
+	/* Backward reach: PREV, LAST-with-offset */
+	if (!context->maxOverflow)
 	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
 		int64		reach = 0;
+		bool		gotReach = false;
 
 		if (nav->kind == RPR_NAV_PREV)
 		{
 			reach = eval_nav_offset_helper(context->winstate,
 										   nav->offset_arg, 1);
+			gotReach = true;
 		}
 		else if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
 		{
 			reach = eval_nav_offset_helper(context->winstate,
 										   nav->offset_arg, 0);
+			gotReach = true;
 		}
 		else if (nav->kind == RPR_NAV_PREV_LAST ||
 				 nav->kind == RPR_NAV_NEXT_LAST)
@@ -4045,168 +4060,123 @@ eval_nav_max_offset_walker(Node *node, void *ctx)
 			if (nav->kind == RPR_NAV_PREV_LAST)
 			{
 				if (pg_add_s64_overflow(inner, outer, &reach))
-				{
-					context->overflow = true;
-					return false;
-				}
+					context->maxOverflow = true;
+				else
+					gotReach = true;
 			}
 			else
-				reach = (inner > outer) ? inner - outer : 0;
+			{
+				reach = Max(inner - outer, 0);
+				gotReach = true;
+			}
 		}
 
-		context->maxOffset = Max(context->maxOffset, reach);
-
-		return false;			/* don't walk into children */
+		if (gotReach)
+			context->maxOffset = Max(context->maxOffset, reach);
 	}
 
-	return expression_tree_walker(node, eval_nav_max_offset_walker, ctx);
-}
-
-/*
- * eval_nav_max_offset
- *		Evaluate non-constant backward-reach offsets at executor init time.
- *
- * Called when the planner set navMaxOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
- * because some offset in PREV, LAST-with-offset, or compound PREV_LAST/
- * NEXT_LAST contains a parameter or non-foldable expression.
- *
- * On overflow, sets navMaxOffsetKind to RPR_NAV_OFFSET_RETAIN_ALL so that
- * tuplestore trim is disabled for backward navigation.
- */
-static void
-eval_nav_max_offset(WindowAggState *winstate, List *defineClause)
-{
-	EvalNavMaxContext ctx;
-	ListCell   *lc;
-
-	ctx.winstate = winstate;
-	ctx.maxOffset = 0;
-	ctx.overflow = false;
-
-	foreach(lc, defineClause)
+	/* Forward reach from match_start: FIRST, compound PREV_FIRST/NEXT_FIRST */
+	if (nav->kind == RPR_NAV_FIRST)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
+		int64		reach;
 
-		eval_nav_max_offset_walker((Node *) te->expr, &ctx);
-	}
-
-	if (ctx.overflow)
-	{
-		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
-		winstate->navMaxOffset = 0;
+		reach = eval_nav_offset_helper(context->winstate,
+									   nav->offset_arg, 0);
+		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
-	else
+	else if (nav->kind == RPR_NAV_PREV_FIRST ||
+			 nav->kind == RPR_NAV_NEXT_FIRST)
 	{
-		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navMaxOffset = ctx.maxOffset;
-	}
-}
+		int64		inner = eval_nav_offset_helper(context->winstate,
+												   nav->offset_arg, 0);
+		int64		outer = eval_nav_offset_helper(context->winstate,
+												   nav->compound_offset_arg, 1);
+		int64		reach;
 
-typedef struct
-{
-	WindowAggState *winstate;
-	int64		minOffset;
-	bool		found;
-} EvalNavFirstContext;
-
-/*
- * eval_nav_first_offset_walker
- *		Walk expression tree evaluating forward-from-match_start offsets.
- *
- * Handles simple FIRST and compound PREV_FIRST/NEXT_FIRST.
- */
-static bool
-eval_nav_first_offset_walker(Node *node, void *ctx)
-{
-	EvalNavFirstContext *context = (EvalNavFirstContext *) ctx;
-
-	if (node == NULL)
-		return false;
-
-	if (IsA(node, RPRNavExpr))
-	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-		int64		combined = INT64_MAX;
-
-		if (nav->kind == RPR_NAV_FIRST)
+		if (nav->kind == RPR_NAV_PREV_FIRST)
 		{
-			context->found = true;
-			combined = eval_nav_offset_helper(context->winstate,
-											  nav->offset_arg, 0);
+			/*
+			 * reach = inner - outer.  Both are non-negative, so the result >=
+			 * -INT64_MAX, which cannot underflow int64.
+			 */
+			reach = inner - outer;
 		}
-		else if (nav->kind == RPR_NAV_PREV_FIRST ||
-				 nav->kind == RPR_NAV_NEXT_FIRST)
+		else
 		{
-			int64		inner = eval_nav_offset_helper(context->winstate,
-													   nav->offset_arg, 0);
-			int64		outer = eval_nav_offset_helper(context->winstate,
-													   nav->compound_offset_arg, 1);
-
-			context->found = true;
-			if (nav->kind == RPR_NAV_PREV_FIRST)
-			{
-				/*
-				 * combined = inner - outer.  Both are non-negative, so the
-				 * result >= -INT64_MAX, which cannot underflow int64.
-				 */
-				combined = inner - outer;
-			}
-			else
-			{
-				/*
-				 * NEXT_FIRST: combined = inner + outer.  This can overflow,
-				 * but the result is always >= 0, so it never updates
-				 * minOffset (which tracks the minimum).  Clamp to INT64_MAX
-				 * on overflow.
-				 */
-				if (pg_add_s64_overflow(inner, outer, &combined))
-					combined = INT64_MAX;
-			}
+			/*
+			 * NEXT_FIRST: reach = inner + outer.  This can overflow, but the
+			 * result is always >= 0, so it never updates minFirstOffset
+			 * (which tracks the minimum).  Clamp to INT64_MAX on overflow.
+			 */
+			if (pg_add_s64_overflow(inner, outer, &reach))
+				reach = INT64_MAX;
 		}
-
-		context->minOffset = Min(context->minOffset, combined);
-
-		return false;
+		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
-
-	return expression_tree_walker(node, eval_nav_first_offset_walker, ctx);
 }
 
 /*
- * eval_nav_first_offset
- *		Evaluate non-constant forward-from-match_start offsets at executor
- *		init time.
+ * eval_define_offsets
+ *		Evaluate non-constant nav offsets at executor init time.
+ *
+ * Called when the planner set navMaxOffsetKind and/or navFirstOffsetKind
+ * to RPR_NAV_OFFSET_NEEDS_EVAL because some offset contains a parameter
+ * or non-foldable expression.  Updates only the fields whose kind was
+ * NEEDS_EVAL; FIXED kinds are left unchanged.
  *
- * Called when the planner set navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
- * because some offset in FIRST or compound PREV_FIRST/NEXT_FIRST contains
- * a parameter or non-foldable expression.
+ * On backward-reach overflow, sets navMaxOffsetKind to
+ * RPR_NAV_OFFSET_RETAIN_ALL so that tuplestore trim is disabled for
+ * backward navigation.
  */
 static void
-eval_nav_first_offset(WindowAggState *winstate, List *defineClause)
+eval_define_offsets(WindowAggState *winstate, List *defineClause)
 {
-	EvalNavFirstContext ctx;
+	EvalDefineOffsetsContext ctx;
+	NavTraversal trav;
 	ListCell   *lc;
+	bool		needsMax = (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
+	bool		needsFirst = (winstate->hasFirstNav &&
+							  winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
+
+	if (!needsMax && !needsFirst)
+		return;
 
 	ctx.winstate = winstate;
-	ctx.minOffset = INT64_MAX;
-	ctx.found = false;
+	ctx.maxOffset = 0;
+	ctx.maxOverflow = false;
+	ctx.minFirstOffset = INT64_MAX;
+
+	trav.visit = visit_nav_exec;
+	trav.data = &ctx;
 
 	foreach(lc, defineClause)
 	{
 		TargetEntry *te = (TargetEntry *) lfirst(lc);
 
-		eval_nav_first_offset_walker((Node *) te->expr, &ctx);
+		nav_traversal_walker((Node *) te->expr, &trav);
 	}
 
-	if (ctx.found && ctx.minOffset < INT64_MAX)
+	if (needsMax)
 	{
-		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navFirstOffset = ctx.minOffset;
+		if (ctx.maxOverflow)
+		{
+			winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
+			winstate->navMaxOffset = 0;
+		}
+		else
+		{
+			winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+			winstate->navMaxOffset = ctx.maxOffset;
+		}
 	}
-	else
+
+	if (needsFirst)
 	{
 		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navFirstOffset = 0;
+		if (ctx.minFirstOffset < INT64_MAX)
+			winstate->navFirstOffset = ctx.minFirstOffset;
+		else
+			winstate->navFirstOffset = INT64_MAX;
 	}
 }
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 52205cc7159..c8ecaeea7cf 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -294,6 +294,9 @@ static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 RPRPattern *compiledPattern,
 								 List *defineClause,
 								 Bitmapset *defineMatchStartDependent,
+								 RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
+								 bool hasFirstNav,
+								 RPRNavOffsetKind navFirstOffsetKind, int64 navFirstOffset,
 								 List *qual, bool topWindow,
 								 Plan *lefttree);
 static Group *make_group(List *tlist, List *qual, int numGroupCols,
@@ -2464,13 +2467,20 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 }
 
 /*
- * NavOffsetContext - context for compute_nav_offsets walker.
+ * DefineMetadataContext - context for compute_define_metadata walker.
  *
- * Collects both backward reach (PREV, LAST-with-offset, compound
- * PREV_LAST/NEXT_LAST) and forward-from-match-start reach (FIRST,
- * compound PREV_FIRST/NEXT_FIRST) in a single tree walk.
+ * Collects three pieces of metadata from the DEFINE clause in a single
+ * tree walk per variable:
+ *   - backward reach (PREV, LAST-with-offset, compound PREV_LAST/NEXT_LAST)
+ *   - forward-from-match-start reach (FIRST, compound PREV_FIRST/NEXT_FIRST)
+ *   - per-variable match_start dependency (variables containing FIRST,
+ *     LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/
+ *     NEXT_LAST-with-offset require per-context re-evaluation)
+ *
+ * The driver sets curVarIdx to the index of the variable being walked
+ * before each invocation; the walker uses it to populate matchStartDependent.
  */
-typedef struct NavOffsetContext
+typedef struct DefineMetadataContext
 {
 	int64		maxOffset;		/* max PREV/LAST backward offset (>= 0) */
 	bool		maxNeedsEval;	/* non-constant PREV/LAST offset found */
@@ -2479,7 +2489,10 @@ typedef struct NavOffsetContext
 								 * PREV_FIRST) */
 	bool		hasFirst;		/* any FIRST node found */
 	bool		firstNeedsEval; /* non-constant FIRST offset found */
-} NavOffsetContext;
+	int			curVarIdx;		/* DEFINE variable currently being walked */
+	Bitmapset  *matchStartDependent;	/* variables that depend on
+										 * match_start */
+} DefineMetadataContext;
 
 /*
  * Helper: extract constant offset from an expression, handling NULL/negative.
@@ -2514,175 +2527,207 @@ extract_const_offset(Expr *expr, int64 defaultOffset, int64 *result)
 }
 
 /*
- * nav_offset_walker
- *		Expression tree walker for compute_nav_offsets.
+ * visit_nav_plan
+ *		nav_traversal_walker callback (NavVisitFn) for the planner side.
+ *		At each RPRNavExpr in a DEFINE expression, computes:
+ *
+ *		  1. backward reach (maxOffset) for tuplestore trim:
+ *			 - PREV(v, N), LAST(v, N)              -> N (default 1)
+ *			 - compound PREV_LAST(v, N, M)         -> N + M (overflow -> maxOverflow)
+ *			 - compound NEXT_LAST(v, N, M)         -> max(N - M, 0)
  *
- * For each RPRNavExpr found, extract its constant offset(s) and update the
- * NavOffsetContext with the maximum backward reach (maxOffset) and minimum
- * forward reach (firstOffset).  Handles simple navigation (PREV, NEXT,
- * FIRST, LAST) and compound forms (PREV_FIRST, NEXT_FIRST, PREV_LAST,
- * NEXT_LAST) by combining inner and outer offsets.
+ *		  2. forward reach (firstOffset) for tuplestore trim:
+ *			 - FIRST(v, N)                         -> N (default 0)
+ *			 - compound PREV_FIRST(v, N, M)        -> N - M (may be negative)
+ *			 - compound NEXT_FIRST(v, N, M)        -> N + M
  *
- * Non-constant offsets set maxNeedsEval or firstNeedsEval.  Overflow sets
- * maxOverflow or firstOverflow for RETAIN_ALL fallback.
+ *		  3. per-variable match_start dependency for absorption suppression:
+ *			 outer nav kinds that reach match_start (FIRST, LAST-with-offset,
+ *			 PREV_FIRST, NEXT_FIRST, PREV_LAST/NEXT_LAST-with-offset) add
+ *			 curVarIdx to matchStartDependent.
+ *
+ * Constant offsets are extracted via extract_const_offset; non-constant
+ * offsets set maxNeedsEval / firstNeedsEval so the executor can resolve
+ * them at init time (see visit_nav_exec).  Classification uses only the
+ * outer nav kind: parser nesting restrictions prevent FIRST/LAST inside
+ * a PREV/NEXT value subexpression.
  */
-static bool
-nav_offset_walker(Node *node, void *ctx)
+static void
+visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 {
-	NavOffsetContext *context = (NavOffsetContext *) ctx;
+	DefineMetadataContext *context = (DefineMetadataContext *) t->data;
 
-	if (node == NULL)
-		return false;
+	/*
+	 * Parser guarantee: by the time the planner sees a DEFINE expression,
+	 * compound nesting has been flattened into a single RPRNavExpr and any
+	 * other RPRNavExpr nesting has been rejected.  So nav's direct child
+	 * fields are not themselves RPRNavExpr nodes, and outer-kind dispatch
+	 * below is sufficient.
+	 */
+	Assert(nav->arg == NULL || !IsA(nav->arg, RPRNavExpr));
+	Assert(nav->offset_arg == NULL || !IsA(nav->offset_arg, RPRNavExpr));
+	Assert(nav->compound_offset_arg == NULL ||
+		   !IsA(nav->compound_offset_arg, RPRNavExpr));
 
-	if (IsA(node, RPRNavExpr))
+	/*
+	 * Simple PREV(v, N) and LAST(v, N): backward reach from currentpos. LAST
+	 * without offset = currentpos, no backward reach. NEXT: forward only,
+	 * irrelevant for trim.
+	 */
+	if (nav->kind == RPR_NAV_PREV ||
+		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
 	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-
-		/*
-		 * Simple PREV(v, N) and LAST(v, N): backward reach from currentpos.
-		 * LAST without offset = currentpos, no backward reach. NEXT: forward
-		 * only, irrelevant for trim.
-		 */
-		if (nav->kind == RPR_NAV_PREV ||
-			(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
+		if (!context->maxNeedsEval)
 		{
-			if (!context->maxNeedsEval)
-			{
-				int64		offset;
+			int64		offset;
 
-				if (extract_const_offset(nav->offset_arg, 1, &offset))
-					context->maxOffset = Max(context->maxOffset, offset);
-				else
-					context->maxNeedsEval = true;
-			}
+			if (extract_const_offset(nav->offset_arg, 1, &offset))
+				context->maxOffset = Max(context->maxOffset, offset);
+			else
+				context->maxNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Simple FIRST(v, N): forward reach from match_start. Smaller N means
-		 * older rows needed.
-		 */
-		if (nav->kind == RPR_NAV_FIRST)
-		{
-			context->hasFirst = true;
+	/*
+	 * Simple FIRST(v, N): forward reach from match_start. Smaller N means
+	 * older rows needed.
+	 */
+	if (nav->kind == RPR_NAV_FIRST)
+	{
+		context->hasFirst = true;
 
-			if (!context->firstNeedsEval)
-			{
-				int64		offset;
+		if (!context->firstNeedsEval)
+		{
+			int64		offset;
 
-				if (extract_const_offset(nav->offset_arg, 0, &offset))
-					context->firstOffset = Min(context->firstOffset, offset);
-				else
-					context->firstNeedsEval = true;
-			}
+			if (extract_const_offset(nav->offset_arg, 0, &offset))
+				context->firstOffset = Min(context->firstOffset, offset);
+			else
+				context->firstNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Compound PREV_LAST / NEXT_LAST: base = currentpos. PREV_LAST(v, N,
-		 * M): target = currentpos - N - M -> lookback = N + M NEXT_LAST(v, N,
-		 * M): target = currentpos - N + M -> lookback = max(N - M, 0)
-		 */
-		if (nav->kind == RPR_NAV_PREV_LAST ||
-			nav->kind == RPR_NAV_NEXT_LAST)
+	/*
+	 * Compound PREV_LAST / NEXT_LAST: base = currentpos. PREV_LAST(v, N, M):
+	 * target = currentpos - N - M -> lookback = N + M NEXT_LAST(v, N, M):
+	 * target = currentpos - N + M -> lookback = max(N - M, 0)
+	 */
+	if (nav->kind == RPR_NAV_PREV_LAST ||
+		nav->kind == RPR_NAV_NEXT_LAST)
+	{
+		if (!context->maxNeedsEval)
 		{
-			if (!context->maxNeedsEval)
-			{
-				int64		inner,
-							outer,
-							combined;
+			int64		inner;
+			int64		outer;
+			int64		reach;
 
-				if (extract_const_offset(nav->offset_arg, 0, &inner) &&
-					extract_const_offset(nav->compound_offset_arg, 1, &outer))
+			if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+				extract_const_offset(nav->compound_offset_arg, 1, &outer))
+			{
+				if (nav->kind == RPR_NAV_PREV_LAST)
 				{
-					if (nav->kind == RPR_NAV_PREV_LAST)
-					{
-						if (pg_add_s64_overflow(inner, outer, &combined))
-						{
-							context->maxOverflow = true;
-							return false;
-						}
-					}
+					if (pg_add_s64_overflow(inner, outer, &reach))
+						context->maxOverflow = true;
 					else
-						combined = (inner > outer) ? inner - outer : 0;
-
-					context->maxOffset = Max(context->maxOffset, combined);
+						context->maxOffset = Max(context->maxOffset, reach);
 				}
 				else
-					context->maxNeedsEval = true;
+				{
+					reach = Max(inner - outer, 0);
+					context->maxOffset = Max(context->maxOffset, reach);
+				}
 			}
+			else
+				context->maxNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Compound PREV_FIRST / NEXT_FIRST: base = match_start. PREV_FIRST(v,
-		 * N, M): target = match_start + N - M NEXT_FIRST(v, N, M): target =
-		 * match_start + N + M The combined offset (N+/-M) from match_start
-		 * can be negative, meaning rows before match_start are needed.
-		 */
-		if (nav->kind == RPR_NAV_PREV_FIRST ||
-			nav->kind == RPR_NAV_NEXT_FIRST)
+	/*
+	 * Compound PREV_FIRST / NEXT_FIRST: base = match_start. PREV_FIRST(v, N,
+	 * M): target = match_start + N - M NEXT_FIRST(v, N, M): target =
+	 * match_start + N + M The combined offset (N+/-M) from match_start can be
+	 * negative, meaning rows before match_start are needed.
+	 */
+	if (nav->kind == RPR_NAV_PREV_FIRST ||
+		nav->kind == RPR_NAV_NEXT_FIRST)
+	{
+		context->hasFirst = true;
+
+		if (!context->firstNeedsEval)
 		{
-			context->hasFirst = true;
+			int64		inner;
+			int64		outer;
+			int64		reach;
 
-			if (!context->firstNeedsEval)
+			if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+				extract_const_offset(nav->compound_offset_arg, 1, &outer))
 			{
-				int64		inner,
-							outer,
-							combined;
-
-				if (extract_const_offset(nav->offset_arg, 0, &inner) &&
-					extract_const_offset(nav->compound_offset_arg, 1, &outer))
+				if (nav->kind == RPR_NAV_PREV_FIRST)
 				{
-					if (nav->kind == RPR_NAV_PREV_FIRST)
-					{
-						/*
-						 * combined = inner - outer.  Both are non-negative,
-						 * so the result >= -INT64_MAX, which cannot underflow
-						 * int64.  No overflow check needed.
-						 */
-						combined = inner - outer;
-					}
-					else
-					{
-						/*
-						 * NEXT_FIRST: combined = inner + outer.  This can
-						 * overflow, but the result is always >= 0, so it
-						 * never updates firstOffset (which tracks the
-						 * minimum).  Clamp to INT64_MAX on overflow.
-						 */
-						if (pg_add_s64_overflow(inner, outer, &combined))
-							combined = INT64_MAX;
-					}
-
-					context->firstOffset = Min(context->firstOffset, combined);
+					/*
+					 * reach = inner - outer.  Both are non-negative, so the
+					 * result >= -INT64_MAX, which cannot underflow int64. No
+					 * overflow check needed.
+					 */
+					reach = inner - outer;
 				}
 				else
-					context->firstNeedsEval = true;
+				{
+					/*
+					 * NEXT_FIRST: reach = inner + outer.  This can overflow,
+					 * but the result is always >= 0, so it never updates
+					 * firstOffset (which tracks the minimum).  Clamp to
+					 * INT64_MAX on overflow.
+					 */
+					if (pg_add_s64_overflow(inner, outer, &reach))
+						reach = INT64_MAX;
+				}
+
+				context->firstOffset = Min(context->firstOffset, reach);
 			}
+			else
+				context->firstNeedsEval = true;
 		}
-
-		/* Don't walk into RPRNavExpr children */
-		return false;
 	}
 
-	return expression_tree_walker(node, nav_offset_walker, ctx);
+	/* Match-start dependency: classify the outer nav kind. */
+	if (nav->kind == RPR_NAV_FIRST ||
+		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL) ||
+		nav->kind == RPR_NAV_PREV_FIRST ||
+		nav->kind == RPR_NAV_NEXT_FIRST ||
+		((nav->kind == RPR_NAV_PREV_LAST ||
+		  nav->kind == RPR_NAV_NEXT_LAST) &&
+		 nav->offset_arg != NULL))
+		context->matchStartDependent =
+			bms_add_member(context->matchStartDependent,
+						   context->curVarIdx);
 }
 
 /*
- * compute_nav_offsets
- *		Compute navigation offsets for tuplestore trim in a single pass.
+ * compute_define_metadata
+ *		Compute navigation offsets and match_start dependency for the
+ *		DEFINE clause in a single pass per variable.
  *
- * Walks all DEFINE clause expressions once, computing:
+ * Walks each DEFINE variable expression once, computing:
  *   - maxOffset: max backward reach from PREV, LAST-with-offset,
  *     compound PREV_LAST/NEXT_LAST
  *   - hasFirst/firstOffset: min forward-from-match-start reach from
  *     FIRST, compound PREV_FIRST/NEXT_FIRST
+ *   - matchStartDependent: bitmapset of variable indices whose
+ *     expressions contain navigation that depends on match_start
+ *     (FIRST, LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/
+ *     PREV_LAST/NEXT_LAST-with-offset).  Such variables require
+ *     per-context re-evaluation during NFA processing.
  */
 static void
-compute_nav_offsets(List *defineClause,
-					RPRNavOffsetKind *maxKind, int64 *maxResult,
-					bool *hasFirst,
-					RPRNavOffsetKind *firstKind, int64 *firstResult)
+compute_define_metadata(List *defineClause,
+						RPRNavOffsetKind *maxKind, int64 *maxResult,
+						bool *hasFirst,
+						RPRNavOffsetKind *firstKind, int64 *firstResult,
+						Bitmapset **matchStartDependent)
 {
-	NavOffsetContext ctx;
+	DefineMetadataContext ctx;
+	NavTraversal trav;
 	ListCell   *lc;
 
 	ctx.maxOffset = 0;
@@ -2691,14 +2736,22 @@ compute_nav_offsets(List *defineClause,
 	ctx.firstOffset = INT64_MAX;	/* sentinel: no FIRST found yet */
 	ctx.hasFirst = false;
 	ctx.firstNeedsEval = false;
+	ctx.curVarIdx = 0;
+	ctx.matchStartDependent = NULL;
+
+	trav.visit = visit_nav_plan;
+	trav.data = &ctx;
 
 	foreach(lc, defineClause)
 	{
 		TargetEntry *te = (TargetEntry *) lfirst(lc);
 
-		nav_offset_walker((Node *) te->expr, &ctx);
+		nav_traversal_walker((Node *) te->expr, &trav);
+		ctx.curVarIdx++;
 	}
 
+	*matchStartDependent = ctx.matchStartDependent;
+
 	/* Max backward offset */
 	if (ctx.maxOverflow)
 	{
@@ -2725,15 +2778,11 @@ compute_nav_offsets(List *defineClause,
 			*firstKind = RPR_NAV_OFFSET_NEEDS_EVAL;
 			*firstResult = 0;
 		}
-		else if (ctx.firstOffset == INT64_MAX)
-		{
-			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = 0;	/* only implicit FIRST(v) */
-		}
 		else
 		{
 			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = ctx.firstOffset; /* may be negative */
+			*firstResult = ctx.firstOffset; /* may be negative; INT64_MAX if
+											 * overflowed */
 		}
 	}
 	else
@@ -2743,83 +2792,6 @@ compute_nav_offsets(List *defineClause,
 	}
 }
 
-/*
- * has_match_start_dependency
- *		Check if an expression tree contains navigation that depends on
- *		match_start: FIRST, LAST-with-offset, or compound PREV_FIRST/
- *		NEXT_FIRST/PREV_LAST/NEXT_LAST with offset.  Such expressions
- *		require per-context re-evaluation during NFA processing.
- *
- * LAST without offset always resolves to currentpos and is
- * match_start-independent.
- */
-static bool
-has_match_start_dependency(Node *node, void *context)
-{
-	if (node == NULL)
-		return false;
-
-	if (IsA(node, RPRNavExpr))
-	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-
-		if (nav->kind == RPR_NAV_FIRST)
-			return true;
-		if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
-			return true;
-
-		/* Compound kinds with FIRST base depend on match_start */
-		if (nav->kind == RPR_NAV_PREV_FIRST ||
-			nav->kind == RPR_NAV_NEXT_FIRST)
-			return true;
-
-		/*
-		 * PREV_LAST/NEXT_LAST: inner is LAST, which uses currentpos.
-		 * match_start-dependent only if inner has offset (clamped to
-		 * match_start).
-		 */
-		if ((nav->kind == RPR_NAV_PREV_LAST ||
-			 nav->kind == RPR_NAV_NEXT_LAST) &&
-			nav->offset_arg != NULL)
-			return true;
-
-		/* Check children (arg may contain further nav expressions) */
-		return has_match_start_dependency((Node *) nav->arg, context);
-	}
-
-	return expression_tree_walker(node, has_match_start_dependency, NULL);
-}
-
-/*
- * compute_match_start_dependent
- *		Build a Bitmapset of DEFINE variable indices whose expressions
- *		depend on match_start (contain FIRST, LAST-with-offset, or
- *		compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST with offset).
- *
- * Variables in this set require per-context re-evaluation during NFA
- * processing, because different contexts may have different match_start
- * values.
- */
-static Bitmapset *
-compute_match_start_dependent(List *defineClause)
-{
-	Bitmapset  *result = NULL;
-	ListCell   *lc;
-	int			varIdx = 0;
-
-	foreach(lc, defineClause)
-	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
-		if (has_match_start_dependency((Node *) te->expr, NULL))
-			result = bms_add_member(result, varIdx);
-
-		varIdx++;
-	}
-
-	return result;
-}
-
 /*
  * create_windowagg_plan
  *
@@ -2848,6 +2820,11 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 	List	   *filteredDefineClause = NIL;
 	RPRPattern *compiledPattern = NULL;
 	Bitmapset  *matchStartDependent = NULL;
+	RPRNavOffsetKind navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+	int64		navMaxOffset = 0;
+	bool		hasFirstNav = false;
+	RPRNavOffsetKind navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
+	int64		navFirstOffset = 0;
 
 
 	/*
@@ -2910,8 +2887,16 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		buildDefineVariableList(wc->defineClause, &defineVariableList);
 		filteredDefineClause = wc->defineClause;
 
-		/* Identify match_start-dependent DEFINE variables */
-		matchStartDependent = compute_match_start_dependent(wc->defineClause);
+		/*
+		 * Walk DEFINE once: collect nav offsets (for tuplestore trim) and the
+		 * bitmapset of match_start-dependent variables (for absorption
+		 * suppression in buildRPRPattern).
+		 */
+		compute_define_metadata(wc->defineClause,
+								&navMaxOffsetKind, &navMaxOffset,
+								&hasFirstNav,
+								&navFirstOffsetKind, &navFirstOffset,
+								&matchStartDependent);
 
 		/* Compile and optimize RPR patterns */
 		compiledPattern = buildRPRPattern(wc->rpPattern,
@@ -2937,6 +2922,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  compiledPattern,
 						  filteredDefineClause,
 						  matchStartDependent,
+						  navMaxOffsetKind, navMaxOffset,
+						  hasFirstNav,
+						  navFirstOffsetKind, navFirstOffset,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -7011,6 +6999,9 @@ make_windowagg(List *tlist, WindowClause *wc,
 			   RPRPattern *compiledPattern,
 			   List *defineClause,
 			   Bitmapset *defineMatchStartDependent,
+			   RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
+			   bool hasFirstNav,
+			   RPRNavOffsetKind navFirstOffsetKind, int64 navFirstOffset,
 			   List *qual, bool topWindow, Plan *lefttree)
 {
 	WindowAgg  *node = makeNode(WindowAgg);
@@ -7048,11 +7039,12 @@ make_windowagg(List *tlist, WindowClause *wc,
 	/* Store pre-computed match_start dependency bitmapset */
 	node->defineMatchStartDependent = defineMatchStartDependent;
 
-	/* Compute nav offsets for tuplestore trim optimization */
-	compute_nav_offsets(defineClause,
-						&node->navMaxOffsetKind, &node->navMaxOffset,
-						&node->hasFirstNav,
-						&node->navFirstOffsetKind, &node->navFirstOffset);
+	/* Store pre-computed nav offsets for tuplestore trim optimization */
+	node->navMaxOffsetKind = navMaxOffsetKind;
+	node->navMaxOffset = navMaxOffset;
+	node->hasFirstNav = hasFirstNav;
+	node->navFirstOffsetKind = navFirstOffsetKind;
+	node->navFirstOffset = navFirstOffset;
 
 	plan->targetlist = tlist;
 	plan->lefttree = lefttree;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2543170c374..a817eb4a63f 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -41,6 +41,7 @@
 
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
 #include "optimizer/rpr.h"
 
 /* Forward declarations - pattern comparison */
@@ -1991,3 +1992,36 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 
 	return result;
 }
+
+/*
+ * nav_traversal_walker
+ *		Shared expression-tree walker that locates RPRNavExpr nodes in a
+ *		DEFINE expression and dispatches each one to a caller-supplied
+ *		visitor.  Used by:
+ *		  - planner (visit_nav_plan in createplan.c) to collect tuplestore
+ *		    trim offsets and per-variable match_start dependency
+ *		  - executor (visit_nav_exec in nodeWindowAgg.c) to evaluate
+ *		    non-constant nav offsets at WindowAggState init time
+ *
+ * The driver wraps a mode-specific context in a NavTraversal and passes
+ * it as ctx; the visitor casts t->data to its own context type.  Children
+ * of an RPRNavExpr are not walked: the parser's nesting restrictions
+ * ensure offsets and dependencies are fully captured by the outer nav
+ * kind, so the visitor only needs to inspect the RPRNavExpr itself.
+ */
+bool
+nav_traversal_walker(Node *node, void *ctx)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		NavTraversal *t = (NavTraversal *) ctx;
+
+		t->visit(t, (RPRNavExpr *) node);
+		return false;
+	}
+
+	return expression_tree_walker(node, nav_traversal_walker, ctx);
+}
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index f56b7db5bc8..87411abcbe2 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -25,6 +25,7 @@
 
 #include "postgres.h"
 
+#include "catalog/pg_proc.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -35,14 +36,33 @@
 #include "parser/parse_expr.h"
 #include "parser/parse_rpr.h"
 #include "parser/parse_target.h"
+#include "utils/lsyscache.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 *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);
+static bool define_walker(Node *node, void *context);
+static bool nav_volatile_func_checker(Oid funcid, void *context);
 
 /*
  * transformRPR
@@ -412,9 +432,22 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	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 */
+	/*
+	 * Validate DEFINE expressions: nested PREV/NEXT, column references,
+	 * compound flatten, volatile callees -- all in a single walk per
+	 * variable.
+	 */
 	foreach_ptr(TargetEntry, te, defineClause)
-		(void) check_rpr_nav_nesting_walker((Node *) te->expr, pstate);
+	{
+		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);
@@ -426,169 +459,239 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 }
 
 /*
- * 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.
+ * Single-pass DEFINE clause validator.
  *
- * 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
+ * One walker function (define_walker) visits every node in a DEFINE
+ * expression exactly once and enforces every rule:
+ *   - Volatile callees and NextValueExpr are rejected at parse time
+ *     (RPR's NFA may evaluate the same row's predicate multiple times
+ *     during backtracking, so a volatile result would make matching
+ *     non-deterministic).
+ *   - For each outer RPRNavExpr (per SQL 5.6.4 nesting rules):
+ *     * arg must contain at least one column reference
+ *     * PREV/NEXT wrapping FIRST/LAST flattens to a compound kind
+ *     * Other nestings are rejected (FIRST(PREV()), PREV(PREV()), ...)
+ *     * offset_arg / compound_offset_arg must not contain column refs
+ *
+ * The walker uses a phase tag to know which subtree it is in: DEFINE
+ * body (top-level), inside a nav.arg, or inside a nav.offset_arg /
+ * compound_offset_arg.  When entering an outer nav (PHASE_BODY), it
+ * walks nav.arg in PHASE_NAV_ARG to collect nesting/column-ref state,
+ * applies compound flatten or raises a nesting error, then walks the
+ * (post-flatten) offset(s) in PHASE_NAV_OFFSET to enforce the
+ * constant-offset rule.  No subtree is walked twice.
  */
-typedef struct
+
+/*
+ * nav_volatile_func_checker
+ *		check_functions_in_node callback: true if funcid is VOLATILE.
+ */
+static bool
+nav_volatile_func_checker(Oid funcid, void *context)
 {
-	int			nav_count;		/* number of RPRNavExpr nodes found */
-	bool		has_column_ref; /* Var found */
-	RPRNavKind	inner_kind;		/* kind of first (outermost) nested RPRNavExpr */
-} NavCheckResult;
+	return (func_volatile(funcid) == PROVOLATILE_VOLATILE);
+}
 
+/*
+ * define_walker
+ *		Single-pass DEFINE clause validator.  At each node, enforces:
+ *
+ *		  [1] no volatile callees (and no NextValueExpr) -- anywhere in
+ *			  the tree, regardless of phase
+ *		  [2] 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)
+ *			  - any other nesting is rejected (FIRST(PREV()),
+ *				PREV(PREV()), FIRST(FIRST()), three-or-more deep)
+ *		  [3] for each nav offset (PHASE_NAV_OFFSET):
+ *			  - must be a run-time constant (no column references)
+ *
+ * Var sightings feed the column-ref rule for the enclosing nav scope;
+ * RPRNavExpr sightings inside PHASE_NAV_ARG feed the nesting decision.
+ * See the comment block above DefinePhase for the overall design and
+ * how each subtree is walked exactly once.
+ */
 static bool
-nav_check_walker(Node *node, void *context)
+define_walker(Node *node, void *context)
 {
-	NavCheckResult *result = (NavCheckResult *) context;
+	DefineWalkCtx *ctx = (DefineWalkCtx *) 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);
+	/*
+	 * Reject volatile callees and sequence operations anywhere in the DEFINE
+	 * clause: they are non-deterministic across the multiple predicate
+	 * evaluations that NFA backtracking and PREV/NEXT navigation may trigger
+	 * for a single row.
+	 */
+	if (check_functions_in_node(node, nav_volatile_func_checker, NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("volatile functions are not allowed in DEFINE clause"),
+				 parser_errposition(ctx->pstate, exprLocation(node))));
+	if (IsA(node, NextValueExpr))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("sequence operations are not allowed in DEFINE clause"),
+				 parser_errposition(ctx->pstate, exprLocation(node))));
 
-	/* Check arg subtree: nesting + column reference in one walk */
-	memset(&result, 0, sizeof(result));
-	(void) nav_check_walker((Node *) nav->arg, &result);
+	/* 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 (result.nav_count > 0)
+	if (IsA(node, RPRNavExpr))
 	{
-		bool		inner_is_physical = (result.inner_kind == RPR_NAV_PREV ||
-										 result.inner_kind == RPR_NAV_NEXT);
+		RPRNavExpr *nav = (RPRNavExpr *) node;
 
-		if (outer_is_physical && !inner_is_physical)
+		if (ctx->phase == DEFINE_PHASE_NAV_ARG)
 		{
 			/*
-			 * 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.
+			 * Nested nav inside an outer nav.arg: record for the outer's
+			 * compound / nesting decision, then keep recursing so deeper Vars
+			 * and volatile callees are still observed.
 			 */
-			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)));
+			if (ctx->nav_count == 0)
+				ctx->inner_kind = nav->kind;
+			ctx->nav_count++;
+			return expression_tree_walker(node, define_walker, ctx);
 		}
-		else if (outer_is_physical && inner_is_physical)
+
+		if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
 		{
-			/* 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)));
+			/*
+			 * Navs inside offset_arg are unusual but not directly banned; the
+			 * constant-offset rule will catch any Var or volatile they
+			 * contain.
+			 */
+			return expression_tree_walker(node, define_walker, ctx);
 		}
-		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).
+		 */
 		{
-			/* 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)));
+			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);
 
-	/* 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)));
-	}
-}
+				if (outer_phys && !inner_phys)
+				{
+					RPRNavExpr *inner;
 
-/*
- * 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;
+					/* Reject triple-or-deeper nesting */
+					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)));
+
+					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)));
+
+					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;
+				}
+				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, check_rpr_nav_nesting_walker, context);
+
+	return expression_tree_walker(node, define_walker, ctx);
 }
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 0a14cfad79b..63c4b09daff 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -62,4 +62,26 @@ extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariable
 								   RPSkipTo rpSkipTo, int frameOptions,
 								   bool hasMatchStartDependent);
 
+/*
+ * Shared traversal walker for DEFINE clause RPRNavExpr collection.
+ *
+ * Both planner (nav-offset / match_start dependency analysis) and executor
+ * (runtime offset evaluation) need to walk DEFINE expressions and dispatch
+ * per RPRNavExpr.  They differ only in what they do at each nav node, so
+ * the traversal frame is shared (nav_traversal_walker, defined in rpr.c)
+ * and the per-nav action is supplied as a callback.  The driver allocates
+ * a mode-specific context, points NavTraversal.data at it, and casts
+ * inside its visitor.
+ */
+struct NavTraversal;
+typedef void (*NavVisitFn) (struct NavTraversal *t, RPRNavExpr *nav);
+
+typedef struct NavTraversal
+{
+	NavVisitFn	visit;
+	void	   *data;			/* mode-specific context */
+} NavTraversal;
+
+extern bool nav_traversal_walker(Node *node, void *ctx);
+
 #endif							/* OPTIMIZER_RPR_H */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 85384f6b096..8793dda3cc3 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1144,7 +1144,31 @@ WINDOW w AS (
 );
 ERROR:  row pattern navigation offset must be a run-time constant
 LINE 7:     DEFINE A AS PREV(price, price) > 0
-                        ^
+                                    ^
+-- Non-constant offset: column reference in compound inner offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, price), 2) > 0
+);
+ERROR:  row pattern navigation offset must be a run-time constant
+LINE 7:     DEFINE A AS PREV(LAST(price, price), 2) > 0
+                                         ^
+-- Non-constant offset: column reference in compound outer offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, 1), price) > 0
+);
+ERROR:  row pattern navigation offset must be a run-time constant
+LINE 7:     DEFINE A AS PREV(LAST(price, 1), price) > 0
+                                             ^
 -- Non-constant offset: volatile function as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -1154,9 +1178,9 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS PREV(price, random()::int) > 0
 );
-ERROR:  row pattern navigation offset must be a run-time constant
+ERROR:  volatile functions are not allowed in DEFINE clause
 LINE 7:     DEFINE A AS PREV(price, random()::int) > 0
-                        ^
+                                    ^
 -- Non-constant offset: subquery as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -1181,7 +1205,7 @@ WINDOW w AS (
 ERROR:  cannot use subquery in DEFINE expression
 LINE 7:     DEFINE A AS PREV(price + (SELECT 1)) > 0
                                      ^
--- First arg: volatile function is allowed (evaluated on target row)
+-- Volatile function inside nav.arg is rejected at parse time
 SELECT company, tdate, price,
        first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
 FROM stock
@@ -1191,30 +1215,24 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price + random() * 0) >= 0
 );
- company  |   tdate    | price | first_value | last_value | count 
-----------+------------+-------+-------------+------------+-------
- company1 | 07-01-2023 |   100 |             |            |     0
- company1 | 07-02-2023 |   200 |         200 |        130 |     9
- company1 | 07-03-2023 |   150 |             |            |     0
- company1 | 07-04-2023 |   140 |             |            |     0
- company1 | 07-05-2023 |   150 |             |            |     0
- company1 | 07-06-2023 |    90 |             |            |     0
- company1 | 07-07-2023 |   110 |             |            |     0
- company1 | 07-08-2023 |   130 |             |            |     0
- company1 | 07-09-2023 |   120 |             |            |     0
- company1 | 07-10-2023 |   130 |             |            |     0
- company2 | 07-01-2023 |    50 |             |            |     0
- company2 | 07-02-2023 |  2000 |        2000 |       1300 |     9
- company2 | 07-03-2023 |  1500 |             |            |     0
- company2 | 07-04-2023 |  1400 |             |            |     0
- company2 | 07-05-2023 |  1500 |             |            |     0
- company2 | 07-06-2023 |    60 |             |            |     0
- company2 | 07-07-2023 |  1100 |             |            |     0
- company2 | 07-08-2023 |  1300 |             |            |     0
- company2 | 07-09-2023 |  1200 |             |            |     0
- company2 | 07-10-2023 |  1300 |             |            |     0
-(20 rows)
-
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 8:     DEFINE A AS PREV(price + random() * 0) >= 0
+                                     ^
+-- nextval is volatile (per pg_proc), so it is rejected via the FuncExpr
+-- path with the "volatile functions" message
+CREATE SEQUENCE rpr_seq;
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > nextval('rpr_seq')
+);
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 7:     DEFINE A AS price > nextval('rpr_seq')
+                                ^
+DROP SEQUENCE rpr_seq;
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index dc3522f930f..0a049d1beba 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -4744,9 +4744,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s
 (5 rows)
 
--- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
--- N + M overflows int64, but target is forward from match_start so it never
--- constrains trim.  Lookahead remains at default (0).
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> infinite
+-- N + M overflows int64; forward reach is unbounded, displayed as infinite.
 EXPLAIN (COSTS OFF) SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
 WINDOW w AS (
@@ -4760,7 +4759,7 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+
    Nav Mark Lookback: 0
-   Nav Mark Lookahead: 0
+   Nav Mark Lookahead: infinite
    ->  Function Scan on generate_series s
 (6 rows)
 
@@ -4803,7 +4802,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
 
 RESET plan_cache_mode;
 DEALLOCATE test_overflow_lookback;
--- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> infinite
 PREPARE test_overflow_lookahead(int8, int8) AS
 SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
@@ -4821,7 +4820,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+
    Nav Mark Lookback: 0
-   Nav Mark Lookahead: 0
+   Nav Mark Lookahead: infinite
    Storage: Memory  Maximum Storage: 17kB
    NFA States: 1 peak, 11 total, 0 merged
    NFA Contexts: 2 peak, 11 total, 10 pruned
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index ef6a157f45d..905bd3538de 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1406,23 +1406,12 @@ DROP INDEX rpr_integ_id_idx;
 -- ============================================================
 -- B9. RPR + Volatile function in DEFINE
 -- ============================================================
--- Records the current behaviour: DEFINE today accepts volatile
--- functions such as random() and the query runs to completion.
--- To keep the expected output deterministic the predicate uses
--- "random() >= 0.0", which is structurally equivalent to TRUE and
--- therefore does not perturb the match result.  The interesting
--- property is that volatile invocation does not crash or short-
--- circuit pattern matching.
---
--- XXX: volatile functions in DEFINE are slated to be rejected at
--- parse time.  Under RPR's NFA engine the same row's DEFINE
--- predicate may be evaluated multiple times (backtracking,
--- PREV/NEXT navigation), so a truly volatile result would make
--- pattern matching non-deterministic.  When the prohibition lands,
--- this test must be replaced with an error-case test that expects
--- random() in DEFINE to be rejected.
+-- Volatile functions in DEFINE are rejected at parse time.  Under
+-- RPR's NFA engine the same row's DEFINE predicate may be evaluated
+-- multiple times (backtracking, PREV/NEXT navigation), so a volatile
+-- result would make pattern matching non-deterministic.  STABLE and
+-- IMMUTABLE callees are accepted.
 -- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
--- This locks the boundary of the volatile-only prohibition.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -1446,7 +1435,7 @@ ORDER BY id;
  10 |  45 |   0
 (10 rows)
 
--- Volatile (random) is the prohibition target; today still accepted.
+-- Volatile (random) is rejected.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -1454,20 +1443,9 @@ WINDOW w AS (ORDER BY id
     PATTERN (A B+)
     DEFINE B AS val > PREV(val) AND random() >= 0.0)
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   2
-  2 |  20 |   0
-  3 |  15 |   2
-  4 |  25 |   0
-  5 |   5 |   3
-  6 |  30 |   0
-  7 |  35 |   0
-  8 |  20 |   3
-  9 |  40 |   0
- 10 |  45 |   0
-(10 rows)
-
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 6:     DEFINE B AS val > PREV(val) AND random() >= 0.0)
+                                            ^
 -- ============================================================
 -- B10. RPR + Correlated subquery in WHERE
 -- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 5563e062cde..e4790f75b0a 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -541,6 +541,26 @@ WINDOW w AS (
     DEFINE A AS PREV(price, price) > 0
 );
 
+-- Non-constant offset: column reference in compound inner offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, price), 2) > 0
+);
+
+-- Non-constant offset: column reference in compound outer offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, 1), price) > 0
+);
+
 -- Non-constant offset: volatile function as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -571,7 +591,7 @@ WINDOW w AS (
     DEFINE A AS PREV(price + (SELECT 1)) > 0
 );
 
--- First arg: volatile function is allowed (evaluated on target row)
+-- Volatile function inside nav.arg is rejected at parse time
 SELECT company, tdate, price,
        first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
 FROM stock
@@ -582,6 +602,19 @@ WINDOW w AS (
     DEFINE A AS PREV(price + random() * 0) >= 0
 );
 
+-- nextval is volatile (per pg_proc), so it is rejected via the FuncExpr
+-- path with the "volatile functions" message
+CREATE SEQUENCE rpr_seq;
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > nextval('rpr_seq')
+);
+DROP SEQUENCE rpr_seq;
+
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index a3789e92631..e123be60aea 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2699,9 +2699,8 @@ WINDOW w AS (
     DEFINE A AS PREV(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
 );
 
--- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
--- N + M overflows int64, but target is forward from match_start so it never
--- constrains trim.  Lookahead remains at default (0).
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> infinite
+-- N + M overflows int64; forward reach is unbounded, displayed as infinite.
 EXPLAIN (COSTS OFF) SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
 WINDOW w AS (
@@ -2728,7 +2727,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
 RESET plan_cache_mode;
 DEALLOCATE test_overflow_lookback;
 
--- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> infinite
 PREPARE test_overflow_lookahead(int8, int8) AS
 SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index d9748979d54..29b2db2f7bb 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -868,24 +868,13 @@ DROP INDEX rpr_integ_id_idx;
 -- ============================================================
 -- B9. RPR + Volatile function in DEFINE
 -- ============================================================
--- Records the current behaviour: DEFINE today accepts volatile
--- functions such as random() and the query runs to completion.
--- To keep the expected output deterministic the predicate uses
--- "random() >= 0.0", which is structurally equivalent to TRUE and
--- therefore does not perturb the match result.  The interesting
--- property is that volatile invocation does not crash or short-
--- circuit pattern matching.
---
--- XXX: volatile functions in DEFINE are slated to be rejected at
--- parse time.  Under RPR's NFA engine the same row's DEFINE
--- predicate may be evaluated multiple times (backtracking,
--- PREV/NEXT navigation), so a truly volatile result would make
--- pattern matching non-deterministic.  When the prohibition lands,
--- this test must be replaced with an error-case test that expects
--- random() in DEFINE to be rejected.
+-- Volatile functions in DEFINE are rejected at parse time.  Under
+-- RPR's NFA engine the same row's DEFINE predicate may be evaluated
+-- multiple times (backtracking, PREV/NEXT navigation), so a volatile
+-- result would make pattern matching non-deterministic.  STABLE and
+-- IMMUTABLE callees are accepted.
 
 -- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
--- This locks the boundary of the volatile-only prohibition.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -896,7 +885,7 @@ WINDOW w AS (ORDER BY id
                 AND to_char(date '2026-01-01', 'YYYY') = '2026')
 ORDER BY id;
 
--- Volatile (random) is the prohibition target; today still accepted.
+-- Volatile (random) is rejected.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8e889ab5e0f..d23b392800e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -663,7 +663,10 @@ DecodingWorkerShared
 DefElem
 DefElemAction
 DefaultACLInfo
+DefineMetadataContext
+DefinePhase
 DefineStmt
+DefineWalkCtx
 DefnDumperPtr
 DeleteStmt
 DependenciesParseState
@@ -762,8 +765,7 @@ ErrorData
 ErrorSaveContext
 EstimateDSMForeignScan_function
 EstimationInfo
-EvalNavFirstContext
-EvalNavMaxContext
+EvalDefineOffsetsContext
 EventTriggerCacheEntry
 EventTriggerCacheItem
 EventTriggerCacheStateType
@@ -1824,8 +1826,8 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
-NavCheckResult
-NavOffsetContext
+NavTraversal
+NavVisitFn
 NestLoop
 NestLoopParam
 NestLoopState
-- 
2.50.1 (Apple Git-155)