public inbox for [email protected]  
help / color / mirror / Atom feed
From: Henson Choi <[email protected]>
To: Tatsuo Ishii <[email protected]>
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Subject: Re: Row pattern recognition
Date: Sun, 21 Jun 2026 15:39:11 +0900
Message-ID: <CAAAe_zCsaf=WedELLjqLe3BV_8dWiO1DPDGA9sXj4qhe+=-XXw@mail.gmail.com> (raw)
In-Reply-To: <[email protected]>
References: <CAAAe_zBY0rrgf+tKXMUc-Y3nDDD69hddRBKopEKAZobhY=Cy-Q@mail.gmail.com>
	<CAAAe_zDYxq0d3exCDwvKncD0kaL2uehDir6HXo4r5DXMitKrSg@mail.gmail.com>
	<[email protected]>
	<[email protected]>

Hi hackers,

This is an increment on top of v49: it lands the two fixes I left as
still-to-come there -- the DEFINE-evaluation use-after-free, now a dedicated
ExprContext, and the PREV/NEXT/FIRST/LAST namespace collision -- adds a
correctness fix found while reviewing the tuplestore spool (dormant
matches), and applies Jian He's and Tatsuo Ishii's review of the v48 series
as a set of mostly behavior-neutral commits.

Before the patch list, a note on CI: cfbot has been red here, but the
failure is not RPR -- it is the libLLVM 19 + ASAN JIT crash (CF 6870), which
reproduces on plain master.  The build-system fix (exclude sanitizer flags
from JIT bitcode generation) is Matheus Alcantara's; the meson half is
v3-0001-Exclude-sanitizer-flags-from-LLVM-JIT-bitcode-gen.patch:


https://postgr.es/m/CAAAe_zBX5uV9K0ikuROLgdNvDCgGqHRskT-73L+oX9=3aXR2AQ@mail.gmail.com

For v50, what would you think about folding that meson patch in as a
temporary prerequisite at the front of the series, so cfbot's ASAN build
gets past the JIT crash and actually exercises RPR?

First, two cleanups splitting changes unrelated to RPR out of the feature
patch:

  nocfbot-0001  Remove blank-line changes unrelated to row pattern
                recognition
  nocfbot-0002  Remove unnecessary includes from the row pattern
                recognition patch

The fixes -- the two I left as still-to-come in v48, plus the dormant-match
fix found since (nocfbot-0003..0005, all behavior-changing):

  nocfbot-0003  Recognize row pattern navigation operations by name in
                DEFINE
      The placeholder PREV/NEXT/FIRST/LAST pg_proc functions polluted the
      ordinary function namespace and could be silently misbound to
      same-named user functions.  They are dropped; an unqualified
      PREV/NEXT/FIRST/LAST inside a DEFINE clause is recognized as
      navigation by name, while a schema-qualified call still reaches an
      ordinary function.  A navigation operation inside a navigation offset
      -- which must be a run-time constant -- is now rejected instead of
      crashing the planner.  (f).prev follows attribute notation as an
      ordinary function, the same inside and outside DEFINE.

  nocfbot-0004  Use a dedicated ExprContext for RPR DEFINE clause
                evaluation
      nocfbot-0039's interim leak fix had turned into a use-after-free and
      was voided in v48.  DEFINE evaluation now has its own ExprContext,
      reset once per row and separate from both the per-output-tuple context
      and tmpcontext, resolving the leak and the use-after-free together.

  nocfbot-0005  Drive RPR row pattern matching once per row
      Matching only advanced when a window function read the frame, so a row
      whose only window function skips the frame (e.g. nth_value() with a
      NULL offset) left the match behind the current row -- silently wrong
      results and a spurious "cannot fetch row ... before WindowObject's
mark
      position" error.
      The match is now driven once per row, before the window functions run;
      the navigation mark advances from the frontier the match reached, so
      the tuplestore is trimmed sooner.

Review of v48 (Jian He, and Tatsuo Ishii), as nocfbot-0006..0013 --
behavior-neutral except where tagged [behavior change]:

  nocfbot-0006  Tidy up row pattern recognition plumbing
      Remove the dead collectPatternVariables()/buildDefineVariableList()
      helpers; drop the redundant rpSkipTo/defineClause arguments of
      make_windowagg(); mark RPRNavExpr.resulttype query_jumble_ignore;
      rename WindowAggState.defineClauseList to defineClauseExprs; assorted
      block flattening.  No change to planner or executor output.

  nocfbot-0007  Further tidy up row pattern recognition plumbing
      Drop the now-unused WindowClause argument of transformDefineClause();
      use foreach_node()/foreach_current_index() in the DEFINE walkers and
      drop their redundant end-of-list break tests; minor include and
      comment fixups.  No output change.

  nocfbot-0008  Refactor transformDefineClause in row pattern recognition
                [behavior change]
      Hoist the "DEFINE variable not used in PATTERN" cross-check out of the
      recursive walker into its caller, and reorder per-variable processing
      to transformExpr -> coerce_to_boolean -> pull_var_clause, dropping the
      separate second coercion pass.  The only observable change is one
      error-cursor position: the duplicate-variable error now points at the
      later definition.  New regression coverage for DEFINE coercion and Var
      propagation is added.

  nocfbot-0009  Replace a bare block with an else in the RPR DEFINE clause
                walker
      Cosmetic flattening of define_walker()'s phase dispatch into an
      if / else if / else chain.

  nocfbot-0010  Rename loop index variables in row pattern deparse helpers
      Tatsuo's suggestion; descriptive names for the deparse index/loop
      variables, no change to deparsed output.

  nocfbot-0011  Rename absorption "judgment point" to "comparison point" in
                comments
      Comment and executor-README wording only; identifiers unchanged.

  nocfbot-0012  Improve comments, documentation, and naming for row pattern
                recognition
      A batch of comment/doc clarity fixes -- the AST-vs-parse-tree wording,
      the Run Condition EXPLAIN test, the contain_rpr_walker comment, the
      ALT-marker and quantifier comments, the RPCommonSyntax.location "or
-1"
      convention, and the transformDefineClause header -- plus renaming the
      saturated-count sentinel RPR_COUNT_MAX to RPR_COUNT_INF for
      consistency with RPR_QUANTITY_INF.

  nocfbot-0013  Document eval_nav_offset_helper's NULL/negative offset
                handling
      Comment only.  The NEEDS_EVAL offset branch is reachable (a Param
      offset can be NULL or negative at run time), so it stays a graceful
      return rather than an assertion.


For traceability, where each patch came from:

  patch          proposer        proposal
  -------------  --------------
 -------------------------------------------------
  nocfbot-0001   Henson          drop blank-line churn unrelated to RPR
  nocfbot-0002   Henson          drop unnecessary includes
  nocfbot-0003   Henson          nav namespace collision; (f).prev as
ordinary function
  nocfbot-0004   Henson          dedicated ExprContext for DEFINE evaluation
  nocfbot-0005   Henson          drive row pattern matching once per row
  nocfbot-0006   Jian He         tidy RPR plumbing
  nocfbot-0007   Henson          further plumbing tidy
  nocfbot-0008   Jian He         refactor transformDefineClause
  nocfbot-0009   Jian He         bare block -> else in the DEFINE walker
  nocfbot-0010   Tatsuo Ishii    rename deparse loop/index variables
  nocfbot-0011   Jian He         "judgment point" -> "comparison point"
wording
  nocfbot-0012   Jian He         comment/doc clarity batch + RPR_COUNT_INF
rename
  nocfbot-0013   Jian He         document eval_nav_offset NULL/negative
offset handling


Still to come -- Jian He's follow-ups on this thread (2026-06-19), to fold
once each approach is agreed (all under review):

  topic                          proposed change
  -----------------------------  -------------------------------------------
  DEFINE qualified column-ref    make the differing pg_temp.t.c /
   error messages                 public.t.c / t.c messages consistent, on
                                  the standard "invalid reference to
                                  FROM-clause entry for table"
  validateRPRPatternVarCount     rename to preprocessRPRPattern
  quantifier INF bound           abstract behind an RPR_QUANTITY_INF macro
  "nullable" variables           rename to match_empty
  splitRPRTrailingAlt            use foreach_node / foreach_current_index
  buildRPRPattern signature      pass the WindowClause directly
  collectDefineVariables /       inline and drop the helpers
   tryUnwrapSingleChild
  variable-limit error msg       fold the maximum into the primary message


Quality work to run alongside, on Linux: Valgrind (leak and use-after-free,
to exercise the DEFINE ExprContext fix, whose reproducer is cassert-only)
and standing gcov coverage to catch untested planner paths.


Longer term, and out of scope for this CF entry:

  - Smaller documentation and error-message follow-ups (glossary, the
    bounded-quantifier message, README wording).
  - Short-circuit / tri-state DEFINE evaluation, as a separate series.
  - SEEK clause support (SQL:2016).
  - Empty pattern PATTERN () -- correctly rejected today; deferred because
    its empty-match semantics (SHOW/OMIT EMPTY MATCHES) are tied to the
    still-out-of-scope MEASURES clause.
  - Relaxing the DEFINE-subquery over-rejection where the standard permits
    it (the subquery does no RPR of its own and makes no outer reference).
  - Prefix-pattern absorption (an optimization; designed and intentionally
    split out as its own series).
  - R010 (MATCH_RECOGNIZE in the FROM clause) and the shared RPRContext it
    would back.


Please let me know if any of the slicing or grouping looks off.

On a personal note, some fatigue has built up, so I'll be easing off the
pace a little for a while and may be slower to follow up on this thread
than I have been.  The work continues -- just at a gentler pace.

Thanks again to Jian and Tatsuo for the careful review.

Best regards,
Henson

From 9d58cdf40d78efdb35131279a006b618213843db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 16 Jun 2026 23:26:16 +0900
Subject: [PATCH 01/13] Remove blank-line changes unrelated to row pattern
 recognition

The RPR patch added or dropped blank lines in five existing files with no
related code change.  Revert them so the files differ from the base only
where RPR actually touches them.
---
 src/backend/executor/nodeWindowAgg.c | 2 --
 src/backend/optimizer/plan/setrefs.c | 1 +
 src/backend/parser/parse_clause.c    | 1 +
 src/backend/utils/adt/ruleutils.c    | 1 -
 src/backend/utils/adt/windowfuncs.c  | 1 +
 5 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index cb6a484b7de..5b2385f1d8d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -177,7 +177,6 @@ typedef struct WindowStatePerAggData
 	bool		restart;		/* need to restart this agg in this cycle? */
 } WindowStatePerAggData;
 
-
 static void initialize_windowaggregate(WindowAggState *winstate,
 									   WindowStatePerFunc perfuncstate,
 									   WindowStatePerAgg peraggstate);
@@ -1086,7 +1085,6 @@ next_tuple:
 		ExecClearTuple(agg_row_slot);
 	}
 
-
 	/* The frame's end is not supposed to move backwards, ever */
 	Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto);
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 813a326bd78..6e4f3fd61e2 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -214,6 +214,7 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
 							   NodeTag elided_type, Bitmapset *relids);
 
+
 /*****************************************************************************
  *
  *		SUBPLAN REFERENCES
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 550ea4eb9c0..8eb367aa579 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -102,6 +102,7 @@ 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,
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 415da6417d4..4eb7e35bee4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7311,7 +7311,6 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
 		get_rule_orderby(wc->orderClause, targetList, false, context);
 		needspace = true;
 	}
-
 	/* framing clause is never inherited, so print unless it's default */
 	if (wc->frameOptions & FRAMEOPTION_NONDEFAULT)
 	{
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 3869f6c8994..d15aa0c75db 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -41,6 +41,7 @@ static bool rank_up(WindowObject winobj);
 static Datum leadlag_common(FunctionCallInfo fcinfo,
 							bool forward, bool withoffset, bool withdefault);
 
+
 /*
  * utility routine for *_rank functions.
  */
-- 
2.50.1 (Apple Git-155)


From 7f135d9dee30c6c4c86f470a97996880b0f6ece2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 15 Jun 2026 15:29:59 +0900
Subject: [PATCH 03/13] Recognize row pattern navigation operations by name in
 DEFINE

PREV, NEXT, FIRST, and LAST were placeholder functions in pg_proc that
polluted the ordinary function namespace and could be silently misbound to
same-named user functions.  Recognize them by name inside a DEFINE clause
and drop the placeholders; an unqualified call is always navigation, while
a schema-qualified call still reaches an ordinary function.  Also reject a
navigation operation inside a navigation offset, which must be a run-time
constant and previously could crash the planner.

Recognition happens in two steps in ParseFuncOrColumn: note the matched
name up front, skip the catalog lookup by treating it as FUNCDETAIL_NORMAL,
let the common decoration and wrong-kind-of-routine checks run, and only
then route to ParseRPRNavCall to build the RPRNavExpr.  ParseRPRNavCall
therefore does not duplicate the aggregate/window decoration checks
(agg_star, DISTINCT, WITHIN GROUP, ORDER BY, FILTER, OVER); the common path
performs them with identical messages.

Document this in func-window.sgml.
---
 doc/src/sgml/func/func-window.sgml     |   6 +
 src/backend/parser/parse_func.c        | 296 ++++++++++-----
 src/backend/parser/parse_rpr.c         |  15 +-
 src/backend/utils/adt/ruleutils.c      |  55 ++-
 src/backend/utils/adt/windowfuncs.c    | 118 ------
 src/include/catalog/pg_proc.dat        |  24 --
 src/test/regress/expected/rpr.out      |  32 --
 src/test/regress/expected/rpr_base.out | 492 ++++++++++++++++++++++++-
 src/test/regress/sql/rpr.sql           |  15 -
 src/test/regress/sql/rpr_base.sql      | 251 +++++++++++++
 10 files changed, 1007 insertions(+), 297 deletions(-)

diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index ab469b56fd7..1079b6abb6e 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -282,6 +282,10 @@ IGNORE NULLS
    Row Pattern Recognition navigation functions are listed in
    <xref linkend="functions-rpr-navigation-table"/>.  These functions
    can be used to describe the DEFINE clause of Row Pattern Recognition.
+   The names <function>PREV</function>, <function>NEXT</function>,
+   <function>FIRST</function>, and <function>LAST</function> are
+   recognized as navigation functions only in an unqualified call; a
+   schema-qualified call is resolved as an ordinary function instead.
   </para>
 
    <table id="functions-rpr-navigation-table">
@@ -397,6 +401,8 @@ IGNORE NULLS
     permitted. Same-category nesting (e.g.,
     <function>PREV</function> inside <function>PREV</function>) is also
     prohibited.
+    The <parameter>offset</parameter> argument must be a run-time constant:
+    it cannot reference columns or contain a navigation operation.
    </para>
 
   <note>
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 1f6c8fa4fb2..f3b37aa992c 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -31,7 +31,6 @@
 #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"
 
@@ -49,6 +48,9 @@ static void unify_hypothetical_args(ParseState *pstate,
 									List *fargs, int numAggregatedArgs,
 									Oid *actual_arg_types, Oid *declared_arg_types);
 static Oid	FuncNameAsType(List *funcname);
+static Node *ParseRPRNavCall(ParseState *pstate, List *funcname,
+							 List *fargs, List *argnames, FuncCall *fn,
+							 int location);
 static Node *ParseComplexProjection(ParseState *pstate, const char *funcname,
 									Node *first_arg, int location);
 static Oid	LookupFuncNameInternal(ObjectType objtype, List *funcname,
@@ -122,6 +124,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	int			fgc_flags;
 	char		aggkind = 0;
 	ParseCallbackState pcbstate;
+	bool		could_be_rpr_nav = false;
 
 	/*
 	 * If there's an aggregate filter, transform it using transformWhereClause
@@ -218,6 +221,28 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 		Assert(first_arg != NULL);
 	}
 
+	/*
+	 * Inside an RPR DEFINE clause, an unqualified call to one of the row
+	 * pattern navigation names PREV/NEXT/FIRST/LAST denotes the navigation
+	 * operation, not an ordinary function.  Just note that here; the catalog
+	 * lookup is skipped and the RPRNavExpr is built at the end, after the
+	 * common decoration checks have run (see the could_be_rpr_nav handling
+	 * below).  A schema-qualified call is the explicit way to reach an
+	 * ordinary function of one of these names.
+	 */
+	if (!is_column && !proc_call &&
+		pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+		list_length(funcname) == 1)
+	{
+		const char *name = strVal(linitial(funcname));
+
+		if (strcmp(name, "prev") == 0 ||
+			strcmp(name, "next") == 0 ||
+			strcmp(name, "first") == 0 ||
+			strcmp(name, "last") == 0)
+			could_be_rpr_nav = true;
+	}
+
 	/*
 	 * Decide whether it's legitimate to consider the construct to be a column
 	 * projection.  For that, there has to be a single argument of complex
@@ -266,17 +291,32 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	 * with default arguments.
 	 */
 
-	setup_parser_errposition_callback(&pcbstate, pstate, location);
+	if (!could_be_rpr_nav)
+	{
+		setup_parser_errposition_callback(&pcbstate, pstate, location);
+
+		fdresult = func_get_detail(funcname, fargs, argnames, nargs,
+								   actual_arg_types,
+								   !func_variadic, true, proc_call,
+								   &fgc_flags,
+								   &funcid, &rettype, &retset,
+								   &nvargs, &vatype,
+								   &declared_arg_types, &argdefaults);
 
-	fdresult = func_get_detail(funcname, fargs, argnames, nargs,
-							   actual_arg_types,
-							   !func_variadic, true, proc_call,
-							   &fgc_flags,
-							   &funcid, &rettype, &retset,
-							   &nvargs, &vatype,
-							   &declared_arg_types, &argdefaults);
+		cancel_parser_errposition_callback(&pcbstate);
+	}
+	else
+	{
+		/*
+		 * A recognized navigation name skips catalog lookup entirely.  Treat
+		 * it as an ordinary function so the common wrong-kind-of-routine and
+		 * decoration checks below run with the existing messages, then route
+		 * to ParseRPRNavCall to build the RPRNavExpr.
+		 */
+		Assert(!proc_call);
 
-	cancel_parser_errposition_callback(&pcbstate);
+		fdresult = FUNCDETAIL_NORMAL;
+	}
 
 	/*
 	 * Check for various wrong-kind-of-routine cases.
@@ -653,6 +693,15 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 					 parser_errposition(pstate, location)));
 	}
 
+	/*
+	 * A recognized navigation name has now passed the common decoration and
+	 * wrong-kind checks above; build the RPRNavExpr.  No fallback to function
+	 * resolution ever happens here.
+	 */
+	if (could_be_rpr_nav)
+		return ParseRPRNavCall(pstate, funcname, fargs, argnames, fn,
+							   location);
+
 	/*
 	 * If there are default arguments, we have to include their types in
 	 * actual_arg_types for the purpose of checking generic type consistency.
@@ -759,88 +808,8 @@ 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 &&
-		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)
+	if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		FuncExpr   *funcexpr = makeNode(FuncExpr);
 
@@ -2111,6 +2080,151 @@ FuncNameAsType(List *funcname)
 	return result;
 }
 
+/*
+ * ParseRPRNavCall
+ *		Recognize a row pattern navigation operation in a DEFINE clause.
+ *
+ * Inside an EXPR_KIND_RPR_DEFINE clause an unqualified call to one of the
+ * names PREV/NEXT/FIRST/LAST denotes the corresponding row pattern navigation
+ * operation (ISO/IEC 19075-5 Subclause 5.6), not an ordinary function call.
+ * The name is matched here, before any catalog lookup, with no fallback to
+ * function resolution: once it matches, decoration and argument-count
+ * violations are dedicated errors rather than letting an ordinary function of
+ * the same name take over.  A schema-qualified call (the caller restricts us
+ * to unqualified names) is the documented way to reach such a function
+ * instead.
+ *
+ * The caller routes here only after the name has matched one of the four
+ * navigation names and the common decoration/wrong-kind checks in
+ * ParseFuncOrColumn have run, so this always returns an RPRNavExpr.
+ */
+static Node *
+ParseRPRNavCall(ParseState *pstate, List *funcname, List *fargs,
+				List *argnames, FuncCall *fn, int location)
+{
+	const char *name = strVal(linitial(funcname));
+	RPRNavKind	kind;
+	const char *navname;
+	int			nargs = list_length(fargs);
+	Node	   *arg;
+	RPRNavExpr *navexpr;
+
+	/* match the parser-downcased identifier; otherwise not a navigation name */
+	if (strcmp(name, "prev") == 0)
+	{
+		kind = RPR_NAV_PREV;
+		navname = "PREV";
+	}
+	else if (strcmp(name, "next") == 0)
+	{
+		kind = RPR_NAV_NEXT;
+		navname = "NEXT";
+	}
+	else if (strcmp(name, "first") == 0)
+	{
+		kind = RPR_NAV_FIRST;
+		navname = "FIRST";
+	}
+	else if (strcmp(name, "last") == 0)
+	{
+		kind = RPR_NAV_LAST;
+		navname = "LAST";
+	}
+	else
+	{
+		/* the caller only routes here after matching one of the four names */
+		pg_unreachable();
+		return NULL;
+	}
+
+	/*
+	 * Once the name matches we never fall back to function resolution, so any
+	 * decoration that does not make sense for a navigation operation is a
+	 * hard error.  The aggregate/window decorations (agg_star, DISTINCT,
+	 * WITHIN GROUP, ORDER BY, FILTER, OVER, RESPECT/IGNORE NULLS) are already
+	 * rejected by the common path in ParseFuncOrColumn, which treated the
+	 * recognized name as an ordinary function; what remains are the
+	 * decorations that path accepts for a plain function but a navigation
+	 * operation must still reject.
+	 */
+	if (fn->func_variadic)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("cannot use VARIADIC with row pattern navigation function %s",
+						navname),
+				 parser_errposition(pstate, location)));
+	if (argnames != NIL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("row pattern navigation operations cannot use named arguments"),
+				 parser_errposition(pstate, location)));
+	/* takes a value expression and an optional offset */
+	if (nargs == 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("too few arguments for row pattern navigation function %s",
+						navname),
+				 errdetail("%s takes a value expression and an optional offset argument.",
+						   navname),
+				 parser_errposition(pstate, location)));
+	if (nargs > 2)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("too many arguments for row pattern navigation function %s",
+						navname),
+				 errdetail("%s takes a value expression and an optional offset argument.",
+						   navname),
+				 parser_errposition(pstate, location)));
+
+	/*
+	 * Resolve a still-unknown first argument to text, the same way the
+	 * anycompatible family does.  A navigation operation is not a polymorphic
+	 * function, so the old "could not determine polymorphic type" error does
+	 * not apply; an unknown literal cannot contain a column reference, so the
+	 * walker still rejects it later.
+	 */
+	arg = linitial(fargs);
+	if (exprType(arg) == UNKNOWNOID)
+		arg = coerce_to_common_type(pstate, arg, TEXTOID, navname);
+
+	navexpr = makeNode(RPRNavExpr);
+	navexpr->kind = kind;
+	navexpr->arg = (Expr *) arg;
+
+	/* an explicit offset is coerced to int8, which the executor reads */
+	if (nargs == 2)
+	{
+		Node	   *offset = lsecond(fargs);
+		Oid			offtype = exprType(offset);
+
+		if (offtype != INT8OID)
+		{
+			Node	   *newoffset;
+
+			newoffset = coerce_to_target_type(pstate, offset, offtype,
+											  INT8OID, -1, COERCION_IMPLICIT,
+											  COERCE_IMPLICIT_CAST, -1);
+			if (newoffset == NULL)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("offset argument of %s must be type %s, not type %s",
+								navname, "bigint", format_type_be(offtype)),
+						 parser_errposition(pstate, exprLocation(offset))));
+			offset = newoffset;
+		}
+		navexpr->offset_arg = (Expr *) offset;
+	}
+	else
+		navexpr->offset_arg = NULL;
+
+	/* compound_offset_arg stays NULL; define_walker flattening fills it in */
+	navexpr->resulttype = exprType(arg);
+	/* resultcollid will be set by parse_collate.c */
+	navexpr->location = location;
+
+	return (Node *) navexpr;
+}
+
 /*
  * ParseComplexProjection -
  *	  handles function calls with a single argument that is of complex type.
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3eaea2be750..8ed01bb8f28 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -472,6 +472,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  *     * 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
+ *       or nested navigation operations
  *
  * Volatile callees (and sequence operations) are rejected later in the
  * planner via validate_rpr_define_volatility(); see optimizer/plan/rpr.c.
@@ -482,7 +483,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  * 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.
+ * constant-offset and no-nested-nav rules.  No subtree is walked twice.
  */
 
 /*
@@ -498,6 +499,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  *				PREV(PREV()), FIRST(FIRST()), three-or-more deep)
  *		  [2] for each nav offset (PHASE_NAV_OFFSET):
  *			  - must be a run-time constant (no column references)
+ *			  - must not contain a row pattern navigation operation
  *
  * Var sightings feed the column-ref rule for the enclosing nav scope;
  * RPRNavExpr sightings inside PHASE_NAV_ARG feed the nesting decision.
@@ -538,11 +540,14 @@ define_walker(Node *node, void *context)
 		if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
 		{
 			/*
-			 * Navs inside offset_arg are unusual but not directly banned; the
-			 * constant-offset rule will catch any Var or volatile they
-			 * contain.
+			 * A navigation offset must be a run-time constant, so it cannot
+			 * contain a navigation operation.
 			 */
-			return expression_tree_walker(node, define_walker, ctx);
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("row pattern navigation offset cannot contain a row pattern navigation operation"),
+					errdetail("A navigation offset must be a run-time constant."),
+					parser_errposition(ctx->pstate, nav->location));
 		}
 
 		/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 4eb7e35bee4..2b7fd7367f3 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -127,6 +127,7 @@ typedef struct
 	bool		varprefix;		/* true to print prefixes on Vars */
 	bool		colNamesVisible;	/* do we care about output column names? */
 	bool		inGroupBy;		/* deparsing GROUP BY clause? */
+	bool		inRPRDefine;	/* deparsing an RPR DEFINE clause? */
 	bool		varInOrderBy;	/* deparsing simple Var in ORDER BY? */
 	Bitmapset  *appendparents;	/* if not null, map child Vars of these relids
 								 * back to the parent rel */
@@ -545,7 +546,7 @@ static char *generate_qualified_relation_name(Oid relid);
 static char *generate_function_name(Oid funcid, int nargs,
 									List *argnames, Oid *argtypes,
 									bool has_variadic, bool *use_variadic_p,
-									bool inGroupBy);
+									bool inGroupBy, bool inRPRDefine);
 static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2);
 static void add_cast_to(StringInfo buf, Oid typid);
 static char *generate_qualified_type_name(Oid typid);
@@ -1131,6 +1132,7 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty)
 		context.indentLevel = PRETTYINDENT_STD;
 		context.colNamesVisible = true;
 		context.inGroupBy = false;
+		context.inRPRDefine = false;
 		context.varInOrderBy = false;
 		context.appendparents = NULL;
 
@@ -1142,7 +1144,7 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty)
 	appendStringInfo(&buf, "EXECUTE FUNCTION %s(",
 					 generate_function_name(trigrec->tgfoid, 0,
 											NIL, NULL,
-											false, NULL, false));
+											false, NULL, false, false));
 
 	if (trigrec->tgnargs > 0)
 	{
@@ -3401,7 +3403,7 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 		appendStringInfo(&buf, " SUPPORT %s",
 						 generate_function_name(proc->prosupport, 1,
 												NIL, argtypes,
-												false, NULL, false));
+												false, NULL, false, false));
 	}
 
 	if (oldlen != buf.len)
@@ -4054,6 +4056,7 @@ deparse_expression_pretty(Node *expr, List *dpcontext,
 	context.indentLevel = startIndent;
 	context.colNamesVisible = true;
 	context.inGroupBy = false;
+	context.inRPRDefine = false;
 	context.varInOrderBy = false;
 	context.appendparents = NULL;
 
@@ -5849,6 +5852,7 @@ make_ruledef(StringInfo buf, HeapTuple ruletup, TupleDesc rulettc,
 		context.indentLevel = PRETTYINDENT_STD;
 		context.colNamesVisible = true;
 		context.inGroupBy = false;
+		context.inRPRDefine = false;
 		context.varInOrderBy = false;
 		context.appendparents = NULL;
 
@@ -6041,6 +6045,7 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace,
 	context.indentLevel = startIndent;
 	context.colNamesVisible = colNamesVisible;
 	context.inGroupBy = false;
+	context.inRPRDefine = false;
 	context.varInOrderBy = false;
 	context.appendparents = NULL;
 
@@ -7220,15 +7225,25 @@ get_rule_define(List *defineClause, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
 	const char *sep;
+	bool		save_inrprdefine = context->inRPRDefine;
 
 	sep = "  ";
 
+	/*
+	 * Within the DEFINE clause an unqualified prev/next/first/last is a
+	 * navigation operation, so a user function of one of those names must be
+	 * schema-qualified to survive a reparse; see generate_function_name().
+	 */
+	context->inRPRDefine = true;
+
 	foreach_node(TargetEntry, te, defineClause)
 	{
 		appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
 		get_rule_expr((Node *) te->expr, context, false);
 		sep = ",\n  ";
 	}
+
+	context->inRPRDefine = save_inrprdefine;
 }
 
 /*
@@ -7459,6 +7474,7 @@ get_window_frame_options_for_explain(int frameOptions,
 	context.indentLevel = 0;
 	context.colNamesVisible = true;
 	context.inGroupBy = false;
+	context.inRPRDefine = false;
 	context.varInOrderBy = false;
 	context.appendparents = NULL;
 
@@ -11691,7 +11707,8 @@ get_func_expr(FuncExpr *expr, deparse_context *context,
 											argnames, argtypes,
 											expr->funcvariadic,
 											&use_variadic,
-											context->inGroupBy));
+											context->inGroupBy,
+											context->inRPRDefine));
 	nargs = 0;
 	foreach(l, expr->args)
 	{
@@ -11761,7 +11778,8 @@ get_agg_expr_helper(Aggref *aggref, deparse_context *context,
 		funcname = generate_function_name(aggref->aggfnoid, nargs, NIL,
 										  argtypes, aggref->aggvariadic,
 										  &use_variadic,
-										  context->inGroupBy);
+										  context->inGroupBy,
+										  context->inRPRDefine);
 
 	/* Print the aggregate name, schema-qualified if needed */
 	appendStringInfo(buf, "%s(%s", funcname,
@@ -11902,7 +11920,8 @@ get_windowfunc_expr_helper(WindowFunc *wfunc, deparse_context *context,
 	if (!funcname)
 		funcname = generate_function_name(wfunc->winfnoid, nargs, argnames,
 										  argtypes, false, NULL,
-										  context->inGroupBy);
+										  context->inGroupBy,
+										  context->inRPRDefine);
 
 	appendStringInfo(buf, "%s(", funcname);
 
@@ -13743,7 +13762,7 @@ get_tablesample_def(TableSampleClause *tablesample, deparse_context *context)
 	appendStringInfo(buf, " TABLESAMPLE %s (",
 					 generate_function_name(tablesample->tsmhandler, 1,
 											NIL, argtypes,
-											false, NULL, false));
+											false, NULL, false, false));
 
 	nargs = 0;
 	foreach(l, tablesample->args)
@@ -14157,12 +14176,14 @@ generate_qualified_relation_name(Oid relid)
  *
  * inGroupBy must be true if we're deparsing a GROUP BY clause.
  *
+ * inRPRDefine must be true if we're deparsing an RPR DEFINE clause.
+ *
  * The result includes all necessary quoting and schema-prefixing.
  */
 static char *
 generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes,
 					   bool has_variadic, bool *use_variadic_p,
-					   bool inGroupBy)
+					   bool inGroupBy, bool inRPRDefine)
 {
 	char	   *result;
 	HeapTuple	proctup;
@@ -14196,6 +14217,24 @@ generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes,
 			force_qualify = true;
 	}
 
+	/*
+	 * Inside a row pattern DEFINE clause, the parser binds an unqualified
+	 * prev/next/first/last to a navigation operation before any catalog
+	 * lookup, so an unqualified call to a user function of one of those names
+	 * would change meaning across a deparse/reparse cycle.  Force schema
+	 * qualification; the qualified form is the documented escape hatch.  Only
+	 * the exact lower-case names are at risk: a mixed-case proname deparses
+	 * quoted and cannot match the parser's downcased comparison.
+	 */
+	if (inRPRDefine)
+	{
+		if (strcmp(proname, "prev") == 0 ||
+			strcmp(proname, "next") == 0 ||
+			strcmp(proname, "first") == 0 ||
+			strcmp(proname, "last") == 0)
+			force_qualify = true;
+	}
+
 	/*
 	 * Determine whether VARIADIC should be printed.  We must do this first
 	 * since it affects the lookup rules in func_get_detail().
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index d15aa0c75db..78b7f05aba2 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -724,121 +724,3 @@ window_nth_value(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
-
-/*
- * prev
- * Catalog placeholder for RPR's PREV navigation operator.
- *
- * The parser transforms prev() calls inside DEFINE into RPRNavExpr nodes,
- * so this function is never reached during normal RPR execution.  It exists
- * only so that the parser can resolve the function name from pg_proc.
- * Calls outside DEFINE are rejected by parse_func.c (EXPR_KIND_RPR_DEFINE
- * check).  The error below is a defensive measure in case that check is
- * bypassed (e.g., direct C-level function invocation).
- */
-Datum
-window_prev(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			errmsg("cannot use PREV() outside a DEFINE clause"));
-	PG_RETURN_NULL();			/* not reached */
-}
-
-/*
- * next
- * Catalog placeholder for RPR's NEXT navigation operator.
- * See window_prev() for details.
- */
-Datum
-window_next(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			errmsg("cannot use NEXT() outside a DEFINE clause"));
-	PG_RETURN_NULL();			/* not reached */
-}
-
-/*
- * prev(value, offset)
- * Catalog placeholder for RPR's PREV navigation operator with offset.
- * See window_prev() for details.
- */
-Datum
-window_prev_offset(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			errmsg("cannot use PREV() outside a DEFINE clause"));
-	PG_RETURN_NULL();			/* not reached */
-}
-
-/*
- * next(value, offset)
- * Catalog placeholder for RPR's NEXT navigation operator with offset.
- * See window_prev() for details.
- */
-Datum
-window_next_offset(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			errmsg("cannot use NEXT() outside a DEFINE clause"));
-	PG_RETURN_NULL();			/* not reached */
-}
-
-/*
- * first
- * Catalog placeholder for RPR's FIRST navigation operator.
- * See window_prev() for details.
- */
-Datum
-window_first(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			errmsg("cannot use FIRST() outside a DEFINE clause"));
-	PG_RETURN_NULL();			/* not reached */
-}
-
-/*
- * last
- * Catalog placeholder for RPR's LAST navigation operator.
- * See window_prev() for details.
- */
-Datum
-window_last(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			errmsg("cannot use LAST() outside a DEFINE clause"));
-	PG_RETURN_NULL();			/* not reached */
-}
-
-/*
- * first(value, offset)
- * Catalog placeholder for RPR's FIRST navigation operator with offset.
- * See window_prev() for details.
- */
-Datum
-window_first_offset(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			errmsg("cannot use FIRST() outside a DEFINE clause"));
-	PG_RETURN_NULL();			/* not reached */
-}
-
-/*
- * last(value, offset)
- * Catalog placeholder for RPR's LAST navigation operator with offset.
- * See window_prev() for details.
- */
-Datum
-window_last_offset(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			errmsg("cannot use LAST() outside a DEFINE clause"));
-	PG_RETURN_NULL();			/* not reached */
-}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b3aa42fc66e..be157a5fbe9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10967,30 +10967,6 @@
 { oid => '3114', descr => 'fetch the Nth row value',
   proname => 'nth_value', prokind => 'w', prorettype => 'anyelement',
   proargtypes => 'anyelement int4', prosrc => 'window_nth_value' },
-{ oid => '8126', descr => 'fetch the preceding row value',
-  proname => 'prev', provolatile => 's', prorettype => 'anyelement',
-  proargtypes => 'anyelement', prosrc => 'window_prev' },
-{ oid => '8128', descr => 'fetch the Nth preceding row value',
-  proname => 'prev', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
-  proargtypes => 'anyelement int8', prosrc => 'window_prev_offset' },
-{ oid => '8127', descr => 'fetch the following row value',
-  proname => 'next', provolatile => 's', prorettype => 'anyelement',
-  proargtypes => 'anyelement', prosrc => 'window_next' },
-{ oid => '8129', descr => 'fetch the Nth following row value',
-  proname => 'next', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
-  proargtypes => 'anyelement int8', prosrc => 'window_next_offset' },
-{ oid => '8130', descr => 'fetch the first row value within match',
-  proname => 'first', provolatile => 's', prorettype => 'anyelement',
-  proargtypes => 'anyelement', prosrc => 'window_first' },
-{ oid => '8132', descr => 'fetch the Nth row value within match',
-  proname => 'first', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
-  proargtypes => 'anyelement int8', prosrc => 'window_first_offset' },
-{ oid => '8131', descr => 'fetch the last row value within match',
-  proname => 'last', provolatile => 's', prorettype => 'anyelement',
-  proargtypes => 'anyelement', prosrc => 'window_last' },
-{ oid => '8133', descr => 'fetch the Nth-from-last row value within match',
-  proname => 'last', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
-  proargtypes => 'anyelement int8', prosrc => 'window_last_offset' },
 
 # functions for range types
 { oid => '3832', descr => 'I/O',
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index c02c9d75a9a..dc5140fecc9 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1021,16 +1021,6 @@ WINDOW w AS (
 --
 -- Error cases: PREV/NEXT usage restrictions
 --
--- PREV outside DEFINE clause
-SELECT prev(price) FROM stock;
-ERROR:  cannot use prev outside a DEFINE clause
-LINE 1: SELECT prev(price) FROM stock;
-               ^
--- NEXT outside DEFINE clause
-SELECT next(price) FROM stock;
-ERROR:  cannot use next outside a DEFINE clause
-LINE 1: SELECT next(price) FROM stock;
-               ^
 -- Nested PREV
 SELECT price FROM stock
 WINDOW w AS (
@@ -1598,15 +1588,6 @@ WINDOW w AS (
  company2 | 07-10-2023 |  1300 |             |            |     0
 (20 rows)
 
--- 2-arg PREV/NEXT outside DEFINE clause
-SELECT prev(price, 2) FROM stock;
-ERROR:  cannot use prev outside a DEFINE clause
-LINE 1: SELECT prev(price, 2) FROM stock;
-               ^
-SELECT next(price, 2) FROM stock;
-ERROR:  cannot use next outside a DEFINE clause
-LINE 1: SELECT next(price, 2) FROM stock;
-               ^
 -- 2-arg PREV/NEXT: negative offset
 SELECT company, tdate, price, first_value(price) OVER w
 FROM stock
@@ -2134,19 +2115,6 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
     DEFINE A AS LAST(val, -1) IS NULL
 );
 ERROR:  row pattern navigation offset must not be negative
--- FIRST/LAST outside DEFINE clause (error cases)
-SELECT first(val) FROM rpr_nav;
-ERROR:  cannot use first outside a DEFINE clause
-LINE 1: SELECT first(val) FROM rpr_nav;
-               ^
-SELECT last(val) FROM rpr_nav;
-ERROR:  cannot use last outside a DEFINE clause
-LINE 1: SELECT last(val) FROM rpr_nav;
-               ^
-SELECT first(val, 1) FROM rpr_nav;
-ERROR:  cannot use first outside a DEFINE clause
-LINE 1: SELECT first(val, 1) FROM rpr_nav;
-               ^
 -- Functional notation: should access column, not RPR navigation
 CREATE TEMP TABLE rpr_names (prev int, next int, first text, last text);
 INSERT INTO rpr_names VALUES (1, 2, 'Joe', 'Blow');
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 41541898f5a..cf158e1c043 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1709,9 +1709,10 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
-ERROR:  cannot use prev outside a DEFINE clause
+ERROR:  function prev(integer) does not exist
 LINE 1: SELECT PREV(id), id, val, COUNT(*) OVER w as cnt
                ^
+DETAIL:  There is no function of that name.
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1724,9 +1725,10 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
-ERROR:  cannot use next outside a DEFINE clause
+ERROR:  function next(integer) does not exist
 LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
                ^
+DETAIL:  There is no function of that name.
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1792,15 +1794,376 @@ ORDER BY id;
 
 -- FIRST function cannot be used other than in DEFINE
 SELECT FIRST(id), id, val FROM rpr_nav;
-ERROR:  cannot use first outside a DEFINE clause
+ERROR:  function first(integer) does not exist
 LINE 1: SELECT FIRST(id), id, val FROM rpr_nav;
                ^
+DETAIL:  There is no function of that name.
 -- LAST function cannot be used other than in DEFINE
 SELECT LAST(id), id, val FROM rpr_nav;
-ERROR:  cannot use last outside a DEFINE clause
+ERROR:  function last(integer) does not exist
 LINE 1: SELECT LAST(id), id, val FROM rpr_nav;
                ^
+DETAIL:  There is no function of that name.
 DROP TABLE rpr_nav;
+-- Name-space: prev/next/first/last are navigation functions, not ordinary functions
+CREATE SCHEMA rpr_navns;
+SET search_path TO rpr_navns, public;
+CREATE TABLE nt (g text, id int, val int);
+INSERT INTO nt VALUES ('x', 1, 100), ('x', 2, 200), ('x', 3, 150),
+                      ('x', 4, 140), ('x', 5, 150);
+-- Outside DEFINE these are ordinary identifiers and resolve to nothing
+SELECT prev(val) FROM nt;
+ERROR:  function prev(integer) does not exist
+LINE 1: SELECT prev(val) FROM nt;
+               ^
+DETAIL:  There is no function of that name.
+SELECT next(val) FROM nt;
+ERROR:  function next(integer) does not exist
+LINE 1: SELECT next(val) FROM nt;
+               ^
+DETAIL:  There is no function of that name.
+SELECT prev(val, 2) FROM nt;
+ERROR:  function prev(integer, integer) does not exist
+LINE 1: SELECT prev(val, 2) FROM nt;
+               ^
+DETAIL:  There is no function of that name.
+SELECT next(val, 2) FROM nt;
+ERROR:  function next(integer, integer) does not exist
+LINE 1: SELECT next(val, 2) FROM nt;
+               ^
+DETAIL:  There is no function of that name.
+SELECT first(val) FROM nt;
+ERROR:  function first(integer) does not exist
+LINE 1: SELECT first(val) FROM nt;
+               ^
+DETAIL:  There is no function of that name.
+SELECT last(val) FROM nt;
+ERROR:  function last(integer) does not exist
+LINE 1: SELECT last(val) FROM nt;
+               ^
+DETAIL:  There is no function of that name.
+SELECT first(val, 1) FROM nt;
+ERROR:  function first(integer, integer) does not exist
+LINE 1: SELECT first(val, 1) FROM nt;
+               ^
+DETAIL:  There is no function of that name.
+-- A schema-qualified call is also a plain (failing) function lookup
+SELECT pg_catalog.prev(val) FROM nt;
+ERROR:  function pg_catalog.prev(integer) does not exist
+LINE 1: SELECT pg_catalog.prev(val) FROM nt;
+               ^
+-- Outside DEFINE, a user-defined function of that name is callable
+CREATE FUNCTION next(numeric) RETURNS numeric AS 'SELECT -999::numeric'
+  LANGUAGE sql IMMUTABLE;
+SELECT next(10);
+ next 
+------
+ -999
+(1 row)
+
+-- Inside DEFINE, unqualified PREV is nav whether or not a user prev() exists
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+)
+    DEFINE START AS TRUE, UP AS val > PREV(val))
+  ORDER BY id;
+ id | val | cnt | last_id 
+----+-----+-----+---------
+  1 | 100 |   2 |       2
+  2 | 200 |   0 |        
+  3 | 150 |   0 |        
+  4 | 140 |   2 |       5
+  5 | 150 |   0 |        
+(5 rows)
+
+-- A qualified call invokes the function, so its volatility still matters
+-- VOLATILE: unqualified is nav; qualified is rejected as a volatile function
+CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999'
+  LANGUAGE sql VOLATILE;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+)
+    DEFINE START AS TRUE, UP AS val > PREV(val))
+  ORDER BY id;
+ id | val | cnt | last_id 
+----+-----+-----+---------
+  1 | 100 |   2 |       2
+  2 | 200 |   0 |        
+  3 | 150 |   0 |        
+  4 | 140 |   2 |       5
+  5 | 150 |   0 |        
+(5 rows)
+
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+)
+    DEFINE A AS rpr_navns.prev(val) = -999)
+  ORDER BY id;
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 6:     DEFINE A AS rpr_navns.prev(val) = -999)
+                        ^
+DROP FUNCTION prev(integer);
+-- IMMUTABLE: unqualified is nav; qualified is the escape hatch and succeeds
+CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999'
+  LANGUAGE sql IMMUTABLE;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+)
+    DEFINE START AS TRUE, UP AS val > PREV(val))
+  ORDER BY id;
+ id | val | cnt | last_id 
+----+-----+-----+---------
+  1 | 100 |   2 |       2
+  2 | 200 |   0 |        
+  3 | 150 |   0 |        
+  4 | 140 |   2 |       5
+  5 | 150 |   0 |        
+(5 rows)
+
+-- (val).prev is attribute notation, so it calls the ordinary function prev(val)
+-- (the IMMUTABLE user prev here), the same as the schema-qualified call below
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+)
+    DEFINE A AS (val).prev = -999)
+  ORDER BY id;
+ id | val | cnt | last_id 
+----+-----+-----+---------
+  1 | 100 |   5 |       5
+  2 | 200 |   0 |        
+  3 | 150 |   0 |        
+  4 | 140 |   0 |        
+  5 | 150 |   0 |        
+(5 rows)
+
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+)
+    DEFINE A AS rpr_navns.prev(val) = -999)
+  ORDER BY id;
+ id | val | cnt | last_id 
+----+-----+-----+---------
+  1 | 100 |   5 |       5
+  2 | 200 |   0 |        
+  3 | 150 |   0 |        
+  4 | 140 |   0 |        
+  5 | 150 |   0 |        
+(5 rows)
+
+-- Zero or more than two arguments is an error, with no function fallback
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV() IS NULL);
+ERROR:  too few arguments for row pattern navigation function PREV
+LINE 4:     PATTERN (A+) DEFINE A AS PREV() IS NULL);
+                                     ^
+DETAIL:  PREV takes a value expression and an optional offset argument.
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+ERROR:  too many arguments for row pattern navigation function PREV
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+                                     ^
+DETAIL:  PREV takes a value expression and an optional offset argument.
+-- the error stands even when a user function of that exact arity exists
+CREATE FUNCTION prev(integer, integer, integer) RETURNS integer
+  AS 'SELECT -999' LANGUAGE sql IMMUTABLE;
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+ERROR:  too many arguments for row pattern navigation function PREV
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+                                     ^
+DETAIL:  PREV takes a value expression and an optional offset argument.
+DROP FUNCTION prev(integer, integer, integer);
+-- Syntactic decoration is rejected
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(*) IS NULL);
+ERROR:  prev(*) specified, but prev is not an aggregate function
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(*) IS NULL);
+                                     ^
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL);
+ERROR:  DISTINCT specified, but prev is not an aggregate function
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL);
+                                     ^
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL);
+ERROR:  ORDER BY specified, but prev is not an aggregate function
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL)...
+                                     ^
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) IS NULL);
+ERROR:  FILTER specified, but prev is not an aggregate function
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) I...
+                                     ^
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER BY val) IS NULL);
+ERROR:  WITHIN GROUP specified, but prev is not an aggregate function
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER B...
+                                     ^
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL);
+ERROR:  OVER specified, but prev is not a window function nor an aggregate function
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL);
+                                     ^
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NULL);
+ERROR:  cannot use VARIADIC with row pattern navigation function PREV
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NU...
+                                     ^
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS prev(x => val) IS NULL);
+ERROR:  row pattern navigation operations cannot use named arguments
+LINE 4:     PATTERN (A+) DEFINE A AS prev(x => val) IS NULL);
+                                     ^
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL);
+ERROR:  only window functions accept RESPECT/IGNORE NULLS
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL)...
+                                     ^
+-- Quoting does not escape: "prev" is nav, "PREV" is an ordinary name
+SELECT id, val, count(*) OVER w AS cnt
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+)
+    DEFINE START AS TRUE, UP AS val > "prev"(val))
+  ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 | 100 |   2
+  2 | 200 |   0
+  3 | 150 |   0
+  4 | 140 |   2
+  5 | 150 |   0
+(5 rows)
+
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL);
+ERROR:  function PREV(integer) does not exist
+LINE 4:     PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL);
+                                     ^
+DETAIL:  There is no function of that name.
+-- A view round-trips: bare PREV stays a navigation function, and a qualified
+-- user prev() stays schema-qualified so it does not reparse as navigation
+CREATE VIEW navns_nav AS
+  SELECT id, count(*) OVER w AS cnt FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+) DEFINE START AS TRUE, UP AS val > PREV(val));
+CREATE VIEW navns_fn AS
+  SELECT id, count(*) OVER w AS cnt FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS rpr_navns.prev(val) = -999);
+SELECT pg_get_viewdef('navns_nav');
+                                       pg_get_viewdef                                        
+---------------------------------------------------------------------------------------------
+  SELECT id,                                                                                +
+     count(*) OVER w AS cnt                                                                 +
+    FROM nt                                                                                 +
+   WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                                           +
+   INITIAL                                                                                  +
+   PATTERN (start up+)                                                                      +
+   DEFINE                                                                                   +
+   start AS true,                                                                           +
+   up AS (val > PREV(val)) );
+(1 row)
+
+SELECT pg_get_viewdef('navns_fn');
+                                       pg_get_viewdef                                        
+---------------------------------------------------------------------------------------------
+  SELECT id,                                                                                +
+     count(*) OVER w AS cnt                                                                 +
+    FROM nt                                                                                 +
+   WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                                           +
+   INITIAL                                                                                  +
+   PATTERN (a+)                                                                             +
+   DEFINE                                                                                   +
+   a AS (rpr_navns.prev(val) = '-999'::integer) );
+(1 row)
+
+DROP VIEW navns_nav, navns_fn;
+-- Attribute notation is field selection only, never a function fallback
+CREATE TYPE rpr_navns_pair AS (first int, last int);
+CREATE TABLE ct (id int, p rpr_navns_pair);
+INSERT INTO ct VALUES (1, (10, 20)), (2, (30, 40));
+SELECT (p).last FROM ct ORDER BY id;
+ last 
+------
+   20
+   40
+(2 rows)
+
+SELECT count(*) OVER w FROM ct
+  WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS (p).last > 0);
+ count 
+-------
+     2
+     0
+(2 rows)
+
+SELECT count(*) OVER w FROM ct
+  WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS (p).prev > 0);
+ERROR:  column "prev" not found in data type rpr_navns_pair
+LINE 4:     PATTERN (A+) DEFINE A AS (p).prev > 0);
+                                      ^
+-- Navigation offset must not contain a navigation operation
+SELECT id, val
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+)
+    DEFINE A AS PREV(val, FIRST(1)) > 0)
+  ORDER BY id;
+ERROR:  row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 6:     DEFINE A AS PREV(val, FIRST(1)) > 0)
+                                  ^
+DETAIL:  A navigation offset must be a run-time constant.
+DROP SCHEMA rpr_navns CASCADE;
+RESET search_path;
 -- ============================================================
 -- SKIP TO / INITIAL Tests
 -- ============================================================
@@ -3655,6 +4018,127 @@ ERROR:  cannot nest row pattern navigation more than two levels deep
 LINE 6:     DEFINE A AS PREV(FIRST(PREV(v))) > 0
                         ^
 HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+-- A navigation offset must be a run-time constant, not a navigation operation
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0);
+ERROR:  row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0);
+                                             ^
+DETAIL:  A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0);
+ERROR:  row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0);
+                                             ^
+DETAIL:  A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0);
+ERROR:  row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0);
+                                             ^
+DETAIL:  A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0);
+ERROR:  row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0);
+                                                    ^
+DETAIL:  A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0);
+ERROR:  row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0);
+                                             ^
+DETAIL:  A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0);
+ERROR:  row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4:     PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0);
+                                             ^
+DETAIL:  A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0);
+ERROR:  cannot nest row pattern navigation more than two levels deep
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0)...
+                                     ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0);
+ERROR:  row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0);
+                                             ^
+DETAIL:  A navigation offset must be a run-time constant.
+-- An unknown literal argument resolves to text; it must still reference a column
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV('foo') = 'bar');
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 4:     PATTERN (A+) DEFINE A AS PREV('foo') = 'bar');
+                                     ^
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV('foo'));
+ERROR:  argument of DEFINE must be type boolean, not type text
+LINE 4:     PATTERN (A+) DEFINE A AS PREV('foo'));
+                                     ^
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 4:     PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL);
+                                     ^
+PREPARE rpr_navarg AS SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV($1) IS NULL);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 4:     PATTERN (A+) DEFINE A AS PREV($1) IS NULL);
+                                     ^
+-- An int2 offset is coerced to int8 like any implicit cast (same as plain 0)
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, 0::smallint) = v);
+ count 
+-------
+     5
+     0
+     0
+     0
+     0
+(5 rows)
+
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, 0) = v);
+ count 
+-------
+     5
+     0
+     0
+     0
+     0
+(5 rows)
+
 -- ============================================================
 -- Window Deduplication Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b15b30c85ac..e3e9de789db 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -444,12 +444,6 @@ WINDOW w AS (
 -- Error cases: PREV/NEXT usage restrictions
 --
 
--- PREV outside DEFINE clause
-SELECT prev(price) FROM stock;
-
--- NEXT outside DEFINE clause
-SELECT next(price) FROM stock;
-
 -- Nested PREV
 SELECT price FROM stock
 WINDOW w AS (
@@ -831,10 +825,6 @@ WINDOW w AS (
     DEFINE A AS PREV(price, 0) = price
 );
 
--- 2-arg PREV/NEXT outside DEFINE clause
-SELECT prev(price, 2) FROM stock;
-SELECT next(price, 2) FROM stock;
-
 -- 2-arg PREV/NEXT: negative offset
 SELECT company, tdate, price, first_value(price) OVER w
 FROM stock
@@ -1124,11 +1114,6 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
     DEFINE A AS LAST(val, -1) IS NULL
 );
 
--- FIRST/LAST outside DEFINE clause (error cases)
-SELECT first(val) FROM rpr_nav;
-SELECT last(val) FROM rpr_nav;
-SELECT first(val, 1) FROM rpr_nav;
-
 -- Functional notation: should access column, not RPR navigation
 CREATE TEMP TABLE rpr_names (prev int, next int, first text, last text);
 INSERT INTO rpr_names VALUES (1, 2, 'Joe', 'Blow');
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index fcefd59de4a..e71f0dd3680 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1309,6 +1309,195 @@ SELECT LAST(id), id, val FROM rpr_nav;
 
 DROP TABLE rpr_nav;
 
+-- Name-space: prev/next/first/last are navigation functions, not ordinary functions
+CREATE SCHEMA rpr_navns;
+SET search_path TO rpr_navns, public;
+CREATE TABLE nt (g text, id int, val int);
+INSERT INTO nt VALUES ('x', 1, 100), ('x', 2, 200), ('x', 3, 150),
+                      ('x', 4, 140), ('x', 5, 150);
+
+-- Outside DEFINE these are ordinary identifiers and resolve to nothing
+SELECT prev(val) FROM nt;
+SELECT next(val) FROM nt;
+SELECT prev(val, 2) FROM nt;
+SELECT next(val, 2) FROM nt;
+SELECT first(val) FROM nt;
+SELECT last(val) FROM nt;
+SELECT first(val, 1) FROM nt;
+-- A schema-qualified call is also a plain (failing) function lookup
+SELECT pg_catalog.prev(val) FROM nt;
+
+-- Outside DEFINE, a user-defined function of that name is callable
+CREATE FUNCTION next(numeric) RETURNS numeric AS 'SELECT -999::numeric'
+  LANGUAGE sql IMMUTABLE;
+SELECT next(10);
+
+-- Inside DEFINE, unqualified PREV is nav whether or not a user prev() exists
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+)
+    DEFINE START AS TRUE, UP AS val > PREV(val))
+  ORDER BY id;
+
+-- A qualified call invokes the function, so its volatility still matters
+-- VOLATILE: unqualified is nav; qualified is rejected as a volatile function
+CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999'
+  LANGUAGE sql VOLATILE;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+)
+    DEFINE START AS TRUE, UP AS val > PREV(val))
+  ORDER BY id;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+)
+    DEFINE A AS rpr_navns.prev(val) = -999)
+  ORDER BY id;
+DROP FUNCTION prev(integer);
+-- IMMUTABLE: unqualified is nav; qualified is the escape hatch and succeeds
+CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999'
+  LANGUAGE sql IMMUTABLE;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+)
+    DEFINE START AS TRUE, UP AS val > PREV(val))
+  ORDER BY id;
+-- (val).prev is attribute notation, so it calls the ordinary function prev(val)
+-- (the IMMUTABLE user prev here), the same as the schema-qualified call below
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+)
+    DEFINE A AS (val).prev = -999)
+  ORDER BY id;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+)
+    DEFINE A AS rpr_navns.prev(val) = -999)
+  ORDER BY id;
+
+-- Zero or more than two arguments is an error, with no function fallback
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV() IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+-- the error stands even when a user function of that exact arity exists
+CREATE FUNCTION prev(integer, integer, integer) RETURNS integer
+  AS 'SELECT -999' LANGUAGE sql IMMUTABLE;
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+DROP FUNCTION prev(integer, integer, integer);
+
+-- Syntactic decoration is rejected
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(*) IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER BY val) IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS prev(x => val) IS NULL);
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL);
+
+-- Quoting does not escape: "prev" is nav, "PREV" is an ordinary name
+SELECT id, val, count(*) OVER w AS cnt
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+)
+    DEFINE START AS TRUE, UP AS val > "prev"(val))
+  ORDER BY id;
+SELECT count(*) OVER w FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL);
+
+-- A view round-trips: bare PREV stays a navigation function, and a qualified
+-- user prev() stays schema-qualified so it does not reparse as navigation
+CREATE VIEW navns_nav AS
+  SELECT id, count(*) OVER w AS cnt FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (START UP+) DEFINE START AS TRUE, UP AS val > PREV(val));
+CREATE VIEW navns_fn AS
+  SELECT id, count(*) OVER w AS cnt FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS rpr_navns.prev(val) = -999);
+SELECT pg_get_viewdef('navns_nav');
+SELECT pg_get_viewdef('navns_fn');
+DROP VIEW navns_nav, navns_fn;
+
+-- Attribute notation is field selection only, never a function fallback
+CREATE TYPE rpr_navns_pair AS (first int, last int);
+CREATE TABLE ct (id int, p rpr_navns_pair);
+INSERT INTO ct VALUES (1, (10, 20)), (2, (30, 40));
+SELECT (p).last FROM ct ORDER BY id;
+SELECT count(*) OVER w FROM ct
+  WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS (p).last > 0);
+SELECT count(*) OVER w FROM ct
+  WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS (p).prev > 0);
+
+-- Navigation offset must not contain a navigation operation
+SELECT id, val
+  FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+)
+    DEFINE A AS PREV(val, FIRST(1)) > 0)
+  ORDER BY id;
+
+DROP SCHEMA rpr_navns CASCADE;
+RESET search_path;
+
 -- ============================================================
 -- SKIP TO / INITIAL Tests
 -- ============================================================
@@ -2420,6 +2609,68 @@ WINDOW w AS (
     DEFINE A AS PREV(FIRST(PREV(v))) > 0
 );
 
+-- A navigation offset must be a run-time constant, not a navigation operation
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0);
+
+-- An unknown literal argument resolves to text; it must still reference a column
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV('foo') = 'bar');
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV('foo'));
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL);
+PREPARE rpr_navarg AS SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV($1) IS NULL);
+
+-- An int2 offset is coerced to int8 like any implicit cast (same as plain 0)
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, 0::smallint) = v);
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+) DEFINE A AS PREV(v, 0) = v);
+
 -- ============================================================
 -- Window Deduplication Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)


From 2025f549ef7c653b0e0e87f282ac8fa46feb2fc5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 08:16:57 +0900
Subject: [PATCH 02/13] Remove unnecessary includes from the row pattern
 recognition patch

Drop includes that are unused or covered by existing forward typedefs:
condition_variable.h, hsearch.h, queryenvironment.h from execnodes.h;
<limits.h> from rpr.c; windowapi.h from execRPR.h.  Switch rpr.h from
parsenodes.h to primnodes.h, where RPRNavExpr is actually defined.
---
 src/backend/optimizer/plan/rpr.c | 2 --
 src/include/executor/execRPR.h   | 1 -
 src/include/nodes/execnodes.h    | 3 ---
 src/include/optimizer/rpr.h      | 7 ++++---
 4 files changed, 4 insertions(+), 9 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 175777a8ffc..50bca59451f 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -37,8 +37,6 @@
 
 #include "postgres.h"
 
-#include <limits.h>
-
 #include "catalog/pg_proc.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
diff --git a/src/include/executor/execRPR.h b/src/include/executor/execRPR.h
index 7b2b0febb76..fb7dc63a4c6 100644
--- a/src/include/executor/execRPR.h
+++ b/src/include/executor/execRPR.h
@@ -15,7 +15,6 @@
 #define EXECRPR_H
 
 #include "nodes/execnodes.h"
-#include "windowapi.h"
 
 /* NFA context management */
 extern RPRNFAContext *ExecRPRStartContext(WindowAggState *winstate,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 792aa3f0d05..8060b018ef8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -38,9 +38,6 @@
 #include "nodes/plannodes.h"
 #include "partitioning/partdefs.h"
 #include "storage/buf.h"
-#include "storage/condition_variable.h"
-#include "utils/hsearch.h"
-#include "utils/queryenvironment.h"
 #include "utils/reltrigger.h"
 #include "utils/typcache.h"
 
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 802d2f1dd69..b4f87d8caa4 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -10,11 +10,12 @@
  *
  *-------------------------------------------------------------------------
  */
-#ifndef OPTIMIZER_RPR_H
-#define OPTIMIZER_RPR_H
+#ifndef RPR_H
+#define RPR_H
 
 #include "nodes/parsenodes.h"
 #include "nodes/plannodes.h"
+#include "nodes/primnodes.h"
 
 /* Limits and special values */
 /*
@@ -96,4 +97,4 @@ typedef struct NavTraversal
 
 extern bool nav_traversal_walker(Node *node, void *ctx);
 
-#endif							/* OPTIMIZER_RPR_H */
+#endif							/* RPR_H */
-- 
2.50.1 (Apple Git-155)


From a70f1c1e753425adcec6021c07a0f5af6dc0968f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 15 Jun 2026 16:54:13 +0900
Subject: [PATCH 04/13] Use a dedicated ExprContext for RPR DEFINE clause
 evaluation

DEFINE clauses were evaluated in the per-output-tuple context.  Because
EEOP_RPR_NAV_RESTORE copies pass-by-reference navigation results into that
context, resetting it per row freed window function results before
ExecProject (a use-after-free), while not resetting it leaked across the
partition.  tmpcontext cannot be reused either, as ExecQualAndReset() resets
it mid-expression when NEXT re-enters spool_tuples.  Use a third ExprContext,
reset once per row and separate from both.
---
 src/backend/executor/execRPR.c       |  5 ++++-
 src/backend/executor/nodeWindowAgg.c | 25 ++++++++++++++++++++++---
 src/include/nodes/execnodes.h        |  1 +
 3 files changed, 27 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index b326a58bbf5..de78b06d277 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1592,10 +1592,13 @@ static void
 nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 							  int64 currentPos)
 {
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	ExprContext *econtext = winstate->rprContext;
 	int64		saved_match_start = winstate->nav_match_start;
 	int64		saved_pos = winstate->currentpos;
 
+	/* Release the previous evaluation's DEFINE expression memory */
+	ResetExprContext(econtext);
+
 	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
 	winstate->nav_match_start = ctx->matchStartRow;
 	winstate->currentpos = currentPos;
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 5b2385f1d8d..90f33bdee40 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2742,12 +2742,28 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 
 	/*
 	 * Create expression contexts.  We need two, one for per-input-tuple
-	 * processing and one for per-output-tuple processing.  We cheat a little
-	 * by using ExecAssignExprContext() to build both.
+	 * processing and one for per-output-tuple processing, plus an optional
+	 * third for row pattern recognition DEFINE evaluation (built just below
+	 * when a DEFINE clause is present).  We cheat a little by using
+	 * ExecAssignExprContext() to build them all.  Each call overwrites
+	 * ps_ExprContext, so the last call must establish the output context.
 	 */
 	ExecAssignExprContext(estate, &winstate->ss.ps);
 	tmpcontext = winstate->ss.ps.ps_ExprContext;
 	winstate->tmpcontext = tmpcontext;
+
+	/*
+	 * Row pattern recognition evaluates DEFINE clauses in a third context,
+	 * reset before each DEFINE evaluation pass.  It must be distinct from
+	 * tmpcontext and ps_ExprContext so its reset frees neither input nor
+	 * output tuple memory.
+	 */
+	if (node->defineClause != NIL)
+	{
+		ExecAssignExprContext(estate, &winstate->ss.ps);
+		winstate->rprContext = winstate->ss.ps.ps_ExprContext;
+	}
+
 	ExecAssignExprContext(estate, &winstate->ss.ps);
 
 	/* Create long-lived context for storage of partition-local memory etc */
@@ -4572,12 +4588,15 @@ static bool
 nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 {
 	WindowAggState *winstate = winobj->winstate;
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	ExprContext *econtext = winstate->rprContext;
 	int			numDefineVars = list_length(winstate->defineVariableList);
 	int			varIdx = 0;
 	TupleTableSlot *slot;
 	int64		saved_pos;
 
+	/* Release the previous row's DEFINE evaluation memory */
+	ResetExprContext(econtext);
+
 	/* Fetch current row into temp_slot_1 */
 	slot = winstate->temp_slot_1;
 	if (!window_gettupleslot(winobj, pos, slot))
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 8060b018ef8..5b4ac8a6c33 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2698,6 +2698,7 @@ typedef struct WindowAggState
 	MemoryContext aggcontext;	/* shared context for aggregate working data */
 	MemoryContext curaggcontext;	/* current aggregate's working data */
 	ExprContext *tmpcontext;	/* short-term evaluation context */
+	ExprContext *rprContext;	/* DEFINE clause evaluation context */
 
 	bool		all_first;		/* true if the scan is starting */
 	bool		partition_spooled;	/* true if all tuples in current partition
-- 
2.50.1 (Apple Git-155)


From 0ed285dc51016e273694e9724e588493ee243564 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 16 Jun 2026 13:02:05 +0900
Subject: [PATCH 05/13] Drive RPR row pattern matching once per row

Row pattern matching only advanced when a window function read the frame,
so a row whose only window function skips the frame (e.g. nth_value() with
a NULL offset) left the match state behind the current row, producing
silently wrong results and a spurious "cannot fetch row before mark
position" error.

Advance the match once per row, before the window functions run, so it
tracks the row scan rather than frame access.  Extract the reduced-frame
loop into advance_reduced_frame_nfa() and the mark advance into
advance_nav_mark(), advancing the navigation mark from the frontier the
match reached rather than from the output row, so tuplestore_trim() frees
rows sooner.

Add regression coverage for the frame-skipping and PREV-only deferred-frame
cases, and assert the contexts' nondecreasing matchStartRow ordering.
---
 src/backend/executor/execRPR.c       |   8 +-
 src/backend/executor/nodeWindowAgg.c | 254 ++++++++++++++++-----------
 src/test/regress/expected/rpr.out    | 149 ++++++++++++++++
 src/test/regress/sql/rpr.sql         |  75 ++++++++
 4 files changed, 385 insertions(+), 101 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index de78b06d277..cea7e0b2973 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1666,7 +1666,13 @@ ExecRPRStartContext(WindowAggState *winstate, int64 startPos)
 		ctx->states->isAbsorbable = false;
 	}
 
-	/* Add to tail of active context list (doubly-linked, oldest-first) */
+	/*
+	 * Add to tail of active context list (doubly-linked, oldest-first).
+	 * matchStartRow is nondecreasing along the list, so the head holds the
+	 * smallest -- an ordering other code relies on.
+	 */
+	Assert(winstate->nfaContextTail == NULL ||
+		   startPos >= winstate->nfaContextTail->matchStartRow);
 	ctx->prev = winstate->nfaContextTail;
 	ctx->next = NULL;
 	if (winstate->nfaContextTail != NULL)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 90f33bdee40..2d97710da8a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -239,6 +239,10 @@ static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);
 
 static void clear_reduced_frame(WindowAggState *winstate);
 static int	get_reduced_frame_status(WindowAggState *winstate, int64 pos);
+static void advance_nav_mark(WindowAggState *winstate, int64 currentPos);
+static void advance_reduced_frame_nfa(WindowObject winobj,
+									  RPRNFAContext *targetCtx, int64 pos,
+									  bool hasLimitedFrame, int64 frameOffset);
 static void update_reduced_frame(WindowObject winobj, int64 pos);
 
 /* Forward declarations - NFA row evaluation */
@@ -2521,6 +2525,16 @@ ExecWindowAgg(PlanState *pstate)
 			{
 				if (winstate->rpSkipTo == ST_NEXT_ROW)
 					clear_reduced_frame(winstate);
+
+				/*
+				 * Drive the row pattern match every row, so it tracks the row
+				 * scan rather than frame access: a window function that skips
+				 * the frame (e.g. nth_value() with a NULL offset) must not
+				 * leave the match state behind currentpos.
+				 */
+				Assert(winstate->nav_winobj != NULL);
+				(void) row_is_in_reduced_frame(winstate->nav_winobj,
+											   winstate->currentpos);
 			}
 
 			/*
@@ -2562,43 +2576,6 @@ ExecWindowAgg(PlanState *pstate)
 		if (winstate->grouptail_ptr >= 0)
 			update_grouptailpos(winstate);
 
-		/*
-		 * Advance RPR navigation mark pointer if possible, so that
-		 * tuplestore_trim() can free rows no longer reachable by navigation.
-		 */
-		if (winstate->nav_winobj &&
-			winstate->rpPattern != NULL &&
-			winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED)
-		{
-			int64		navmarkpos;
-
-			/* Backward reach from PREV/LAST/compound PREV_LAST/NEXT_LAST */
-			if (winstate->currentpos > winstate->navMaxOffset)
-				navmarkpos = winstate->currentpos - winstate->navMaxOffset;
-			else
-				navmarkpos = 0;
-
-			/*
-			 * If FIRST is used, also consider match_start + navFirstOffset.
-			 * The oldest active context (nfaContext) has the smallest
-			 * matchStartRow.
-			 */
-			if (winstate->hasFirstNav &&
-				winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED &&
-				winstate->nfaContext != NULL)
-			{
-				int64		firstreach;
-
-				if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
-										 winstate->navFirstOffset,
-										 &firstreach))
-					navmarkpos = Min(navmarkpos, Max(firstreach, 0));
-			}
-
-			if (navmarkpos > winstate->nav_winobj->markpos)
-				WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
-		}
-
 		/*
 		 * Truncate any no-longer-needed rows from the tuplestore.
 		 */
@@ -4382,6 +4359,143 @@ get_reduced_frame_status(WindowAggState *winstate, int64 pos)
 	return RF_SKIPPED;
 }
 
+/*
+ * advance_nav_mark
+ *		Advance the RPR navigation mark, derived from the NFA frontier
+ *		(currentPos) but held back by the navigation's backward reach, so
+ *		tuplestore_trim() can free rows no longer reachable by navigation.
+ *
+ * The nav read pointer is independent of the aggregate and per-function read
+ * pointers, so moving its mark does not affect their fetches; it only bounds
+ * the DEFINE clause's own PREV/LAST/FIRST lookups.  Backward reach (PREV/LAST)
+ * is measured from the frontier.  FIRST reaches back from the head context's
+ * matchStartRow instead, so it is bounded separately; without FIRST the mark
+ * can follow the frontier freely.
+ */
+static void
+advance_nav_mark(WindowAggState *winstate, int64 currentPos)
+{
+	int64		navmarkpos;
+
+	/* No RPR navigation read pointer: nothing to advance */
+	if (winstate->nav_winobj == NULL)
+		return;
+
+	/* RETAIN_ALL disables trim for the backward (PREV/LAST) dimension */
+	if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_RETAIN_ALL)
+		return;
+
+	/* navMax is FIXED here: NEEDS_EVAL resolved, RETAIN_ALL returned */
+	Assert(winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED);
+
+	if (currentPos > winstate->navMaxOffset)
+		navmarkpos = currentPos - winstate->navMaxOffset;
+	else
+		navmarkpos = 0;
+
+	if (winstate->hasFirstNav && winstate->nfaContext != NULL)
+	{
+		int64		firstreach;
+
+		/* navFirst is always FIXED; it never takes RETAIN_ALL */
+		Assert(winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED);
+
+		/*
+		 * Head context has the smallest matchStartRow (contexts appended in
+		 * nondecreasing order), so bounding by it covers every FIRST reach.
+		 */
+		if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
+								 winstate->navFirstOffset,
+								 &firstreach))
+			navmarkpos = Min(navmarkpos, Max(firstreach, 0));
+	}
+
+	if (navmarkpos > winstate->nav_winobj->markpos)
+		WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
+}
+
+/*
+ * advance_reduced_frame_nfa
+ *		Drive the NFA forward until targetCtx completes or the partition ends.
+ *
+ * This is the match driver, extracted from update_reduced_frame(), which calls
+ * it to advance the match and then records the resolved result.  Row
+ * evaluations are shared across all active contexts.
+ */
+static void
+advance_reduced_frame_nfa(WindowObject winobj, RPRNFAContext *targetCtx,
+						  int64 pos, bool hasLimitedFrame, int64 frameOffset)
+{
+	WindowAggState *winstate = winobj->winstate;
+	int64		currentPos;
+	int64		startPos;
+
+	/*
+	 * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
+	 * pos since contexts are created at currentPos+1 during processing.
+	 * However, pos can exceed this when rows are skipped (e.g., unmatched
+	 * rows don't update nfaLastProcessedRow).
+	 */
+	startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
+
+	/*
+	 * Process rows until target context completes or we hit boundaries. Each
+	 * row evaluation is shared across all active contexts.
+	 */
+	for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
+	{
+		bool		rowExists;
+
+		/*
+		 * Evaluate variables for this row - done only once, shared by all
+		 * contexts.
+		 *
+		 * Set nav_match_start to the head context's matchStartRow for
+		 * FIRST/LAST navigation.  Match_start-dependent variables (FIRST,
+		 * LAST-with-offset) are re-evaluated per-context in ExecRPRProcessRow
+		 * when matchStartRow differs.
+		 */
+		winstate->nav_match_start = targetCtx->matchStartRow;
+		rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched);
+
+		/* No more rows in partition? Finalize all contexts */
+		if (!rowExists)
+		{
+			ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
+			/* Clean up dead contexts from finalization */
+			ExecRPRCleanupDeadContexts(winstate, targetCtx);
+			break;
+		}
+
+		/* Update last processed row */
+		winstate->nfaLastProcessedRow = currentPos;
+
+		/*--------------------------
+		 * Process all contexts for this row:
+		 *   1. Match all (convergence)
+		 *   2. Absorb redundant
+		 *   3. Advance all (divergence)
+		 */
+		ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
+
+		/*
+		 * Create a new context for the next potential start position. This
+		 * enables overlapping match detection for SKIP TO NEXT ROW.
+		 */
+		ExecRPRStartContext(winstate, currentPos + 1);
+
+		/*
+		 * Clean up dead contexts (failed with no active states and no match).
+		 * This removes contexts that failed during processing and counts them
+		 * appropriately as pruned or mismatched.
+		 */
+		ExecRPRCleanupDeadContexts(winstate, targetCtx);
+
+		/* Advance the nav mark to the frontier so trim can free old rows. */
+		advance_nav_mark(winstate, currentPos);
+	}
+}
+
 /*
  * update_reduced_frame
  *		Update reduced frame info using multi-context NFA pattern matching.
@@ -4401,8 +4515,6 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 {
 	WindowAggState *winstate = winobj->winstate;
 	RPRNFAContext *targetCtx;
-	int64		currentPos;
-	int64		startPos;
 	int			frameOptions = winstate->frameOptions;
 	bool		hasLimitedFrame;
 	int64		frameOffset = 0;
@@ -4468,67 +4580,9 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		goto register_result;
 	}
 
-	/*
-	 * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
-	 * pos since contexts are created at currentPos+1 during processing.
-	 * However, pos can exceed this when rows are skipped (e.g., unmatched
-	 * rows don't update nfaLastProcessedRow).
-	 */
-	startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
-
-	/*
-	 * Process rows until target context completes or we hit boundaries. Each
-	 * row evaluation is shared across all active contexts.
-	 */
-	for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
-	{
-		bool		rowExists;
-
-		/*
-		 * Evaluate variables for this row - done only once, shared by all
-		 * contexts.
-		 *
-		 * Set nav_match_start to the head context's matchStartRow for
-		 * FIRST/LAST navigation.  Match_start-dependent variables (FIRST,
-		 * LAST-with-offset) are re-evaluated per-context in ExecRPRProcessRow
-		 * when matchStartRow differs.
-		 */
-		winstate->nav_match_start = targetCtx->matchStartRow;
-		rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched);
-
-		/* No more rows in partition? Finalize all contexts */
-		if (!rowExists)
-		{
-			ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
-			/* Clean up dead contexts from finalization */
-			ExecRPRCleanupDeadContexts(winstate, targetCtx);
-			break;
-		}
-
-		/* Update last processed row */
-		winstate->nfaLastProcessedRow = currentPos;
-
-		/*--------------------------
-		 * Process all contexts for this row:
-		 *   1. Match all (convergence)
-		 *   2. Absorb redundant
-		 *   3. Advance all (divergence)
-		 */
-		ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
-
-		/*
-		 * Create a new context for the next potential start position. This
-		 * enables overlapping match detection for SKIP TO NEXT ROW.
-		 */
-		ExecRPRStartContext(winstate, currentPos + 1);
-
-		/*
-		 * Clean up dead contexts (failed with no active states and no match).
-		 * This removes contexts that failed during processing and counts them
-		 * appropriately as pruned or mismatched.
-		 */
-		ExecRPRCleanupDeadContexts(winstate, targetCtx);
-	}
+	/* Drive the NFA forward until pos's match is resolved. */
+	advance_reduced_frame_nfa(winobj, targetCtx, pos, hasLimitedFrame,
+							  frameOffset);
 
 register_result:
 	Assert(pos == targetCtx->matchStartRow);
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index dc5140fecc9..6ad830b9e36 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -3297,6 +3297,155 @@ WINDOW w AS (
   4 |  20 |           |   0
 (4 rows)
 
+--
+-- nth_value with a NULL offset
+--
+CREATE TABLE rpr_dormant (id int, price int);
+INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g;
+-- reference: first_value(id) is the start row of the match beginning at the
+-- current row, count(*) is that match's length over the reduced frame
+SELECT * FROM (
+  SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len
+  FROM rpr_dormant
+  WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+) s WHERE id > 50 ORDER BY id;
+ id | match_start | match_len 
+----+-------------+-----------
+ 51 |          51 |        10
+ 52 |             |         0
+ 53 |             |         0
+ 54 |             |         0
+ 55 |             |         0
+ 56 |             |         0
+ 57 |             |         0
+ 58 |             |         0
+ 59 |             |         0
+ 60 |             |         0
+(10 rows)
+
+-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv  
+----+-----
+ 51 | 510
+ 52 |    
+ 53 |    
+ 54 |    
+ 55 |    
+ 56 |    
+ 57 |    
+ 58 |    
+ 59 |    
+ 60 |    
+(10 rows)
+
+-- the same window with first_value and count alongside nth_value
+SELECT * FROM (
+  SELECT id, nv, fv, cnt FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv,
+               first_value(id) OVER w AS fv, count(*) OVER w AS cnt
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv  | fv | cnt 
+----+-----+----+-----
+ 51 | 510 | 51 |  10
+ 52 |     |    |   0
+ 53 |     |    |   0
+ 54 |     |    |   0
+ 55 |     |    |   0
+ 56 |     |    |   0
+ 57 |     |    |   0
+ 58 |     |    |   0
+ 59 |     |    |   0
+ 60 |     |    |   0
+(10 rows)
+
+-- the same nth_value with a non-navigation DEFINE
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > 0)
+  ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv 
+----+----
+ 51 |   
+ 52 |   
+ 53 |   
+ 54 |   
+ 55 |   
+ 56 |   
+ 57 |   
+ 58 |   
+ 59 |   
+ 60 |   
+(10 rows)
+
+-- the same nth_value with a PREV-only DEFINE (no FIRST navigation)
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(price, 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv  
+----+-----
+ 51 | 510
+ 52 |    
+ 53 |    
+ 54 |    
+ 55 |    
+ 56 |    
+ 57 |    
+ 58 |    
+ 59 |    
+ 60 |    
+(10 rows)
+
+-- nth_value with a NULL offset band in the middle of the partition
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id BETWEEN 38 AND 46 ORDER BY id;
+ id | nv 
+----+----
+ 38 |   
+ 39 |   
+ 40 |   
+ 41 |   
+ 42 |   
+ 43 |   
+ 44 |   
+ 45 |   
+ 46 |   
+(9 rows)
+
+DROP TABLE rpr_dormant;
 --
 -- NULL handling
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e3e9de789db..3363691c041 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1812,6 +1812,81 @@ WINDOW w AS (
     B AS val IS NULL
 );
 
+--
+-- nth_value with a NULL offset
+--
+
+CREATE TABLE rpr_dormant (id int, price int);
+INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g;
+
+-- reference: first_value(id) is the start row of the match beginning at the
+-- current row, count(*) is that match's length over the reduced frame
+SELECT * FROM (
+  SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len
+  FROM rpr_dormant
+  WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+) s WHERE id > 50 ORDER BY id;
+
+-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same window with first_value and count alongside nth_value
+SELECT * FROM (
+  SELECT id, nv, fv, cnt FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv,
+               first_value(id) OVER w AS fv, count(*) OVER w AS cnt
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same nth_value with a non-navigation DEFINE
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > 0)
+  ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same nth_value with a PREV-only DEFINE (no FIRST navigation)
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(price, 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- nth_value with a NULL offset band in the middle of the partition
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id BETWEEN 38 AND 46 ORDER BY id;
+
+DROP TABLE rpr_dormant;
+
 --
 -- NULL handling
 --
-- 
2.50.1 (Apple Git-155)


From b8484088192a3ea8d24bdbff4b4596450e1383c3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 13:12:36 +0900
Subject: [PATCH 06/13] Tidy up row pattern recognition plumbing

Several behavior-neutral cleanups to the row pattern recognition code,
with no change to planner or executor output:

- Remove the dead collectPatternVariables() and buildDefineVariableList()
  helpers.  The parser already guarantees that every DEFINE variable
  appears in PATTERN, so create_windowagg_plan() and cost_windowagg() walk
  the DEFINE clause directly instead.

- Drop the redundant rpSkipTo and defineClause arguments of
  make_windowagg(); both are read from the WindowClause.

- Mark RPRNavExpr.resulttype as query_jumble_ignore, matching the other
  derived result-type fields.

- Rename WindowAggState.defineClauseList to defineClauseExprs to reflect
  that it stores ExprState nodes.

- Replace a post-loop ListCell NULL test in remove_unused_subquery_outputs()
  with a boolean flag, flatten the nested block in show_window_def(), and
  flatten the DEFINE init loop in ExecInitWindowAgg().

- Minor regression-test comment cleanup.
---
 src/backend/commands/explain.c                | 94 +++++++++----------
 src/backend/executor/README.rpr               |  5 +-
 src/backend/executor/execRPR.c                |  2 +-
 src/backend/executor/nodeWindowAgg.c          | 41 ++++----
 src/backend/optimizer/path/allpaths.c         | 13 ++-
 src/backend/optimizer/path/costsize.c         | 22 +----
 src/backend/optimizer/plan/createplan.c       | 24 +++--
 src/backend/optimizer/plan/rpr.c              | 77 ---------------
 src/include/nodes/execnodes.h                 |  2 +-
 src/include/nodes/primnodes.h                 |  3 +-
 src/include/optimizer/rpr.h                   |  3 -
 src/test/regress/expected/rpr_integration.out | 29 ++----
 src/test/regress/sql/rpr_integration.sql      | 31 ++----
 13 files changed, 110 insertions(+), 236 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 70fd7f386a0..696bdb9c8b5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3212,77 +3212,73 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 	/* Show Row Pattern Recognition pattern if present */
 	if (wagg->rpPattern != NULL)
 	{
+		RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
+		int64		maxOffset = wagg->navMaxOffset;
+		RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
+		int64		firstOffset = wagg->navFirstOffset;
+
 		char	   *patternStr = deparse_rpr_pattern(wagg->rpPattern);
 
-		if (patternStr != NULL)
-		{
-			ExplainPropertyText("Pattern", patternStr, es);
-			pfree(patternStr);
-		}
+		ExplainPropertyText("Pattern", patternStr, es);
+
+		pfree(patternStr);
 
 		/*
 		 * Show navigation offsets for tuplestore trim.  For EXPLAIN ANALYZE,
 		 * use the executor-resolved values (which may differ from the plan
 		 * when NEEDS_EVAL was resolved to FIXED or RETAIN_ALL at init).
 		 */
+		if (es->analyze)
 		{
-			RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
-			int64		maxOffset = wagg->navMaxOffset;
-			RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
-			int64		firstOffset = wagg->navFirstOffset;
+			maxKind = planstate->navMaxOffsetKind;
+			maxOffset = planstate->navMaxOffset;
+			firstKind = planstate->navFirstOffsetKind;
+			firstOffset = planstate->navFirstOffset;
+		}
 
-			if (es->analyze)
-			{
-				maxKind = planstate->navMaxOffsetKind;
-				maxOffset = planstate->navMaxOffset;
-				firstKind = planstate->navFirstOffsetKind;
-				firstOffset = planstate->navFirstOffset;
-			}
+		switch (maxKind)
+		{
+			case RPR_NAV_OFFSET_NEEDS_EVAL:
+				ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+				break;
+			case RPR_NAV_OFFSET_RETAIN_ALL:
+				ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+				break;
+			case RPR_NAV_OFFSET_FIXED:
+				ExplainPropertyInteger("Nav Mark Lookback", NULL,
+									   maxOffset, es);
+				break;
+			default:
+				elog(ERROR, "unrecognized RPR nav offset kind: %d",
+					 maxKind);
+				break;
+		}
 
-			switch (maxKind)
+		if (wagg->hasFirstNav)
+		{
+			switch (firstKind)
 			{
 				case RPR_NAV_OFFSET_NEEDS_EVAL:
-					ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+					ExplainPropertyText("Nav Mark Lookahead", "runtime",
+										es);
 					break;
 				case RPR_NAV_OFFSET_RETAIN_ALL:
-					ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+					ExplainPropertyText("Nav Mark Lookahead", "retain all",
+										es);
 					break;
 				case RPR_NAV_OFFSET_FIXED:
-					ExplainPropertyInteger("Nav Mark Lookback", NULL,
-										   maxOffset, es);
+					if (firstOffset == PG_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",
-						 maxKind);
+						 firstKind);
 					break;
 			}
-
-			if (wagg->hasFirstNav)
-			{
-				switch (firstKind)
-				{
-					case RPR_NAV_OFFSET_NEEDS_EVAL:
-						ExplainPropertyText("Nav Mark Lookahead", "runtime",
-											es);
-						break;
-					case RPR_NAV_OFFSET_RETAIN_ALL:
-						ExplainPropertyText("Nav Mark Lookahead", "retain all",
-											es);
-						break;
-					case RPR_NAV_OFFSET_FIXED:
-						if (firstOffset == PG_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",
-							 firstKind);
-						break;
-				}
-			}
 		}
 	}
 }
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 00af86681b8..713ad84e1d9 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -188,7 +188,6 @@ Chapter IV  Compilation Phase
 IV-1. Entry Point
 
   create_windowagg_plan() (createplan.c)
-    +-- buildDefineVariableList()    Build variable name list from DEFINE
     +-- buildRPRPattern()           NFA compilation (6 phases)
 
 IV-2. The 6 Phases of buildRPRPattern()
@@ -1468,8 +1467,6 @@ Appendix A. Key Function Index
   --------------------------------------------------------------------------
   transformRPR                  parse_rpr.c           Parser entry point
   transformDefineClause         parse_rpr.c           DEFINE transformation
-  collectPatternVariables       rpr.c                 Variable collection
-  buildDefineVariableList       rpr.c                 DEFINE variable list
   buildRPRPattern               rpr.c                 NFA compilation main
   optimizeRPRPattern            rpr.c                 AST optimization
   fillRPRPattern                rpr.c                 NFA element generation
@@ -1538,7 +1535,7 @@ Appendix B. Data Structure Relationship Diagram
     |--- rpSkipTo: RPSkipTo (AFTER MATCH SKIP mode)
     |--- rpPattern: RPRPattern* (copied from plan)
     |--- defineVariableList: List<String> (variable names, DEFINE order)
-    |--- defineClauseList: List<ExprState>
+    |--- defineClauseExprs: List<ExprState>
     |--- nfaVarMatched: bool[] (per-row cache)
     |--- defineMatchStartDependent: Bitmapset* (match_start_dependent
     |        DEFINE vars; see VI-4)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index cea7e0b2973..def0b8423b3 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1606,7 +1606,7 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Invalidate nav_slot cache since match_start changed */
 	winstate->nav_slot_pos = -1;
 
-	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
 	{
 		int			varIdx = foreach_current_index(exprState);
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 2d97710da8a..5d4832d1db9 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3067,27 +3067,26 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 
 	/* Set up row pattern recognition DEFINE clause */
 	winstate->defineVariableList = NIL;
-	winstate->defineClauseList = NIL;
-	if (node->defineClause != NIL)
+	winstate->defineClauseExprs = NIL;
+
+	/*
+	 * Compile DEFINE clause expressions.  PREV/NEXT navigation is handled by
+	 * EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so no
+	 * varno rewriting is needed here.
+	 */
+	foreach_node(TargetEntry, te, node->defineClause)
 	{
-		/*
-		 * Compile DEFINE clause expressions.  PREV/NEXT navigation is handled
-		 * by EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so
-		 * no varno rewriting is needed here.
-		 */
-		foreach_node(TargetEntry, te, node->defineClause)
-		{
-			char	   *name = te->resname;
-			Expr	   *expr = te->expr;
-			ExprState  *exps;
-
-			winstate->defineVariableList =
-				lappend(winstate->defineVariableList,
-						makeString(pstrdup(name)));
-			exps = ExecInitExpr(expr, (PlanState *) winstate);
-			winstate->defineClauseList =
-				lappend(winstate->defineClauseList, exps);
-		}
+		char	   *name = te->resname;
+		ExprState  *exprstate;
+
+		winstate->defineVariableList =
+			lappend(winstate->defineVariableList,
+					makeString(pstrdup(name)));
+
+		exprstate = ExecInitExpr(te->expr, (PlanState *) winstate);
+
+		winstate->defineClauseExprs =
+			lappend(winstate->defineClauseExprs, exprstate);
 	}
 
 	/* Initialize NFA free lists for row pattern matching */
@@ -4669,7 +4668,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	/* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
 	winstate->nav_slot_pos = -1;
 
-	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
 	{
 		Datum		result;
 		bool		isnull;
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f3c9f3c0bd6..44864b3635b 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4807,20 +4807,19 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		 */
 		if (IsA(texpr, WindowFunc))
 		{
+			bool		is_rpr = false;
 			WindowFunc *wfunc = (WindowFunc *) texpr;
-			ListCell   *wlc;
 
-			foreach(wlc, subquery->windowClause)
+			foreach_node(WindowClause, wc, subquery->windowClause)
 			{
-				WindowClause *wc = lfirst_node(WindowClause, wlc);
-
-				if (wc->winref == wfunc->winref &&
-					wc->defineClause != NIL)
+				if (wc->winref == wfunc->winref && wc->defineClause != NIL)
 				{
+					is_rpr = true;
 					break;
 				}
 			}
-			if (wlc != NULL)
+
+			if (is_rpr)
 				continue;
 		}
 
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 82472c3fe96..32a4b172e3d 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3231,30 +3231,18 @@ cost_windowagg(Path *path, PlannerInfo *root,
 	 * so we'll just do this for now.
 	 *
 	 * Moreover, if row pattern recognition is used, we charge the DEFINE
-	 * expressions once per tuple for each variable that appears in PATTERN.
+	 * expressions once per tuple for each DEFINE variable.
 	 */
 	if (winclause->rpPattern)
 	{
-		List	   *pattern_vars;
 		QualCost	defcosts;
 
-		pattern_vars = collectPatternVariables(winclause->rpPattern);
-
-		foreach_node(String, pv, pattern_vars)
+		foreach_node(TargetEntry, def, winclause->defineClause)
 		{
-			char	   *ptname = strVal(pv);
-
-			foreach_node(TargetEntry, def, winclause->defineClause)
-			{
-				if (!strcmp(ptname, def->resname))
-				{
-					cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
-					startup_cost += defcosts.startup;
-					total_cost += defcosts.per_tuple * input_tuples;
-				}
-			}
+			cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
+			startup_cost += defcosts.startup;
+			total_cost += defcosts.per_tuple * input_tuples;
 		}
-		list_free_deep(pattern_vars);
 	}
 
 	foreach(lc, windowFuncs)
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cca4126e511..d96e76b0221 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -290,9 +290,8 @@ static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators,
 static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
 								 int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
-								 List *runCondition, RPSkipTo rpSkipTo,
+								 List *runCondition,
 								 RPRPattern *compiledPattern,
-								 List *defineClause,
 								 Bitmapset *defineMatchStartDependent,
 								 RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
 								 bool hasFirstNav,
@@ -2822,7 +2821,6 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 	Oid		   *ordCollations;
 	ListCell   *lc;
 	List	   *defineVariableList = NIL;
-	List	   *filteredDefineClause = NIL;
 	RPRPattern *compiledPattern = NULL;
 	Bitmapset  *matchStartDependent = NULL;
 	RPRNavOffsetKind navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
@@ -2889,8 +2887,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		 * rejects DEFINE variables not used in PATTERN, so no filtering is
 		 * needed.
 		 */
-		buildDefineVariableList(wc->defineClause, &defineVariableList);
-		filteredDefineClause = wc->defineClause;
+		foreach_node(TargetEntry, te, wc->defineClause)
+			defineVariableList = lappend(defineVariableList,
+										 makeString(pstrdup(te->resname)));
 
 		/*
 		 * Walk DEFINE once: collect nav offsets (for tuplestore trim) and the
@@ -2923,13 +2922,13 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  ordOperators,
 						  ordCollations,
 						  best_path->runCondition,
-						  wc->rpSkipTo,
 						  compiledPattern,
-						  filteredDefineClause,
 						  matchStartDependent,
-						  navMaxOffsetKind, navMaxOffset,
+						  navMaxOffsetKind,
+						  navMaxOffset,
 						  hasFirstNav,
-						  navFirstOffsetKind, navFirstOffset,
+						  navFirstOffsetKind,
+						  navFirstOffset,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -7000,9 +6999,8 @@ static WindowAgg *
 make_windowagg(List *tlist, WindowClause *wc,
 			   int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
 			   int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
-			   List *runCondition, RPSkipTo rpSkipTo,
+			   List *runCondition,
 			   RPRPattern *compiledPattern,
-			   List *defineClause,
 			   Bitmapset *defineMatchStartDependent,
 			   RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
 			   bool hasFirstNav,
@@ -7034,12 +7032,12 @@ make_windowagg(List *tlist, WindowClause *wc,
 	node->inRangeAsc = wc->inRangeAsc;
 	node->inRangeNullsFirst = wc->inRangeNullsFirst;
 	node->topWindow = topWindow;
-	node->rpSkipTo = rpSkipTo;
+	node->rpSkipTo = wc->rpSkipTo;
 
 	/* Store compiled pattern for NFA execution */
 	node->rpPattern = compiledPattern;
 
-	node->defineClause = defineClause;
+	node->defineClause = wc->defineClause;
 
 	/* Store pre-computed match_start dependency bitmapset */
 	node->defineMatchStartDependent = defineMatchStartDependent;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 50bca59451f..48b76a842ed 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -96,9 +96,6 @@ static void computeAbsorbabilityRecursive(RPRPattern *pattern,
 										  bool *hasAbsorbable);
 static void computeAbsorbability(RPRPattern *pattern);
 
-static void collectPatternVariablesRecursive(RPRPatternNode *node,
-											 List **varNames);
-
 /*
  * rprPatternEqual
  *		Compare two RPRPatternNode trees for equality.
@@ -1824,59 +1821,6 @@ computeAbsorbability(RPRPattern *pattern)
 	pattern->isAbsorbable = hasAbsorbable;
 }
 
-/*
- * collectPatternVariablesRecursive
- *		Recursively collect variable names from pattern AST.
- */
-static void
-collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
-{
-	Assert(node != NULL);
-
-	check_stack_depth();
-
-	switch (node->nodeType)
-	{
-		case RPR_PATTERN_VAR:
-			/* Add variable if not already in list */
-			foreach_node(String, varname, *varNames)
-			{
-				if (strcmp(strVal(varname), node->varName) == 0)
-					return;		/* Already collected */
-			}
-			*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
-			break;
-
-		case RPR_PATTERN_SEQ:
-		case RPR_PATTERN_ALT:
-		case RPR_PATTERN_GROUP:
-			foreach_node(RPRPatternNode, child, node->children)
-			{
-				collectPatternVariablesRecursive(child, varNames);
-			}
-			break;
-	}
-}
-
-/*
- * collectPatternVariables
- *		Collect unique variable names used in PATTERN clause.
- *
- * Returns a list of String nodes containing variable names.
- * Used to filter defineClause before varId assignment.
- */
-List *
-collectPatternVariables(RPRPatternNode *pattern)
-{
-	List	   *varNames = NIL;
-
-	/* Caller ensures pattern is not NULL */
-	Assert(pattern != NULL);
-
-	collectPatternVariablesRecursive(pattern, &varNames);
-	return varNames;
-}
-
 /*
  * rpr_volatile_func_checker
  *		check_functions_in_node callback: true if funcid is VOLATILE.
@@ -1953,27 +1897,6 @@ validate_rpr_define_volatility(List *defineClause)
 	}
 }
 
-/*
- * buildDefineVariableList
- *		Build defineVariableList from defineClause.
- *
- * The parser already ensures that all DEFINE variables appear in PATTERN,
- * so no filtering is needed here.
- *
- * Sets *defineVariableList to list of variable names (String nodes).
- */
-void
-buildDefineVariableList(List *defineClause, List **defineVariableList)
-{
-	*defineVariableList = NIL;
-
-	foreach_node(TargetEntry, te, defineClause)
-	{
-		*defineVariableList = lappend(*defineVariableList,
-									  makeString(pstrdup(te->resname)));
-	}
-}
-
 /*
  * buildRPRPattern
  *		Compile pattern AST to flat bytecode array.
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5b4ac8a6c33..59b8656c857 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2654,7 +2654,7 @@ typedef struct WindowAggState
 	struct RPRPattern *rpPattern;	/* compiled pattern for NFA execution */
 	List	   *defineVariableList; /* list of row pattern definition
 									 * variables (list of String) */
-	List	   *defineClauseList;	/* expression for row pattern definition
+	List	   *defineClauseExprs;	/* expression for row pattern definition
 									 * search conditions ExprState list */
 	RPRNFAContext *nfaContext;	/* active matching contexts (head) */
 	RPRNFAContext *nfaContextTail;	/* tail of active contexts (for reverse
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 7c1b3d1bb07..8576a3c9c5b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -687,7 +687,8 @@ typedef struct RPRNavExpr
 	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) */
+	/* result type (same as arg's type) */
+	Oid			resulttype pg_node_attr(query_jumble_ignore);
 	/* OID of collation of result */
 	Oid			resultcollid pg_node_attr(query_jumble_ignore);
 	/* token location, or -1 if unknown */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index b4f87d8caa4..23763442b65 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -67,10 +67,7 @@
 #define RPRElemIsFin(e)			((e)->varId == RPR_VARID_FIN)
 #define RPRElemCanSkip(e)		((e)->min == 0)
 
-extern List *collectPatternVariables(RPRPatternNode *pattern);
 extern void validate_rpr_define_volatility(List *defineClause);
-extern void buildDefineVariableList(List *defineClause,
-									List **defineVariableList);
 extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 								   RPSkipTo rpSkipTo, int frameOptions,
 								   bool hasMatchStartDependent);
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 80a32cca9ab..652989927d7 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -240,9 +240,9 @@ ORDER BY id;
 -- must therefore yield two separate WindowAgg nodes.  Inline specs
 -- are used here because dedup only applies to inline windows.
 -- Baseline: two inline RPR windows that are structurally identical
--- (same ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
--- parser into a single WindowAgg node, confirming that parser-level
--- dedup is active for RPR windows whose DEFINE matches.
+-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
+-- parser into a single WindowAgg node, confirming that parser-level dedup is
+-- active for RPR windows whose DEFINE matches.
 EXPLAIN (COSTS OFF)
 SELECT
     count(*) OVER (ORDER BY id
@@ -325,15 +325,10 @@ ORDER BY id;
 -- A5. Unused window removal prevention
 -- ============================================================
 -- Verify that remove_unused_subquery_outputs() does not drop an RPR
--- window function even when the outer query does not reference its
--- result.  The RPR WindowAgg node is responsible for performing pattern
--- matching, so removing the window function would silently skip the
--- pattern match even though the surrounding query still depends on
--- RPR semantics.
--- The outer query ignores the per-row window result, yet pattern
--- matching must still execute.  The plan must still contain a
--- WindowAgg node below the outer Aggregate; if the window were
--- removed, only Aggregate + Seq Scan would appear.
+-- window function when the outer query does not reference its result.
+-- The WindowAgg node performs the pattern match itself; without it,
+-- the match would be silently skipped.  The plan must contain a
+-- WindowAgg node beneath the outer Aggregate.
 EXPLAIN (COSTS OFF)
 SELECT count(*) FROM (
     SELECT count(*) OVER w FROM rpr_integ
@@ -587,10 +582,8 @@ WHERE cnt > 0;
 -- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on
 -- the outer WindowAgg, with no DEFINE-derived expression leaking in.
 -- Note: columns referenced by DEFINE (e.g., "val") may appear as
--- resjunk entries in upper WindowAgg targetlists -- that is a
--- harmless byproduct of the column guard's broad scope and does not
--- affect client output.  The claim here is limited to the full
--- DEFINE boolean expression.
+-- resjunk entries in upper WindowAgg targetlists -- but that is harmless.
+-- The claim here is limited to the full DEFINE boolean expression.
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT
     count(*) OVER w_rpr AS rpr_cnt,
@@ -620,10 +613,6 @@ WINDOW
                      Output: id, val
 (13 rows)
 
--- Executing the same query shows the client result is limited to
--- the two projected columns; "id" and "val" that appeared in the
--- upper WindowAgg Output line are resjunk-only and do not reach
--- the client.
 SELECT
     count(*) OVER w_rpr AS rpr_cnt,
     count(*) OVER w_normal AS normal_cnt
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 1d6075265bb..2b990b24704 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -166,9 +166,9 @@ ORDER BY id;
 -- are used here because dedup only applies to inline windows.
 
 -- Baseline: two inline RPR windows that are structurally identical
--- (same ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
--- parser into a single WindowAgg node, confirming that parser-level
--- dedup is active for RPR windows whose DEFINE matches.
+-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
+-- parser into a single WindowAgg node, confirming that parser-level dedup is
+-- active for RPR windows whose DEFINE matches.
 EXPLAIN (COSTS OFF)
 SELECT
     count(*) OVER (ORDER BY id
@@ -214,16 +214,10 @@ ORDER BY id;
 -- A5. Unused window removal prevention
 -- ============================================================
 -- Verify that remove_unused_subquery_outputs() does not drop an RPR
--- window function even when the outer query does not reference its
--- result.  The RPR WindowAgg node is responsible for performing pattern
--- matching, so removing the window function would silently skip the
--- pattern match even though the surrounding query still depends on
--- RPR semantics.
-
--- The outer query ignores the per-row window result, yet pattern
--- matching must still execute.  The plan must still contain a
--- WindowAgg node below the outer Aggregate; if the window were
--- removed, only Aggregate + Seq Scan would appear.
+-- window function when the outer query does not reference its result.
+-- The WindowAgg node performs the pattern match itself; without it,
+-- the match would be silently skipped.  The plan must contain a
+-- WindowAgg node beneath the outer Aggregate.
 EXPLAIN (COSTS OFF)
 SELECT count(*) FROM (
     SELECT count(*) OVER w FROM rpr_integ
@@ -399,11 +393,8 @@ WHERE cnt > 0;
 -- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on
 -- the outer WindowAgg, with no DEFINE-derived expression leaking in.
 -- Note: columns referenced by DEFINE (e.g., "val") may appear as
--- resjunk entries in upper WindowAgg targetlists -- that is a
--- harmless byproduct of the column guard's broad scope and does not
--- affect client output.  The claim here is limited to the full
--- DEFINE boolean expression.
-
+-- resjunk entries in upper WindowAgg targetlists -- but that is harmless.
+-- The claim here is limited to the full DEFINE boolean expression.
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT
     count(*) OVER w_rpr AS rpr_cnt,
@@ -417,10 +408,6 @@ WINDOW
     w_normal AS (ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
 
--- Executing the same query shows the client result is limited to
--- the two projected columns; "id" and "val" that appeared in the
--- upper WindowAgg Output line are resjunk-only and do not reach
--- the client.
 SELECT
     count(*) OVER w_rpr AS rpr_cnt,
     count(*) OVER w_normal AS normal_cnt
-- 
2.50.1 (Apple Git-155)


From 4cf9108ce7796a3821873f8377c95e34799760f8 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 14:05:25 +0900
Subject: [PATCH 07/13] Further tidy up row pattern recognition plumbing

More behavior-neutral cleanups to the row pattern recognition code,
continuing the previous tidy-up and with no change to planner or
executor output:

- Drop the now-unused WindowClause argument of transformDefineClause()
  and flatten the redundant nested block in its DEFINE-variable loop.

- Replace manual ListCell iteration and index bookkeeping with
  foreach_node()/foreach_current_index() in
  validate_rpr_define_volatility() and nfa_evaluate_row(), and drop the
  redundant end-of-list break tests in nfa_evaluate_row() and
  nfa_reevaluate_dependent_vars() now that the loops walk
  defineClauseExprs directly.

- Test winstate->defineVariableList against NIL instead of
  list_length() > 0 in ExecInitWindowAgg().

- Remove the now-unused makefuncs.h include from plan/rpr.c.

- Fix the stale struct name in the RPCommonSyntax header comment.
---
 src/backend/executor/execRPR.c       |  3 -
 src/backend/executor/nodeWindowAgg.c |  9 +--
 src/backend/optimizer/plan/rpr.c     |  7 +--
 src/backend/parser/parse_rpr.c       | 84 +++++++++++++---------------
 src/include/nodes/parsenodes.h       |  2 +-
 5 files changed, 44 insertions(+), 61 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index def0b8423b3..099b81aeb81 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1618,9 +1618,6 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 			result = ExecEvalExpr(exprState, econtext, &isnull);
 			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
 		}
-
-		if (varIdx + 1 >= list_length(winstate->defineVariableList))
-			break;
 	}
 
 	/* Restore original match_start, currentpos, and invalidate cache */
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 5d4832d1db9..819ad814bf5 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3103,7 +3103,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	 * ordering (DEFINE order first), varId == defineIdx for all defined
 	 * variables, so no mapping is needed.
 	 */
-	if (list_length(winstate->defineVariableList) > 0)
+	if (winstate->defineVariableList != NIL)
 		winstate->nfaVarMatched = palloc0(sizeof(bool) *
 										  list_length(winstate->defineVariableList));
 	else
@@ -4642,8 +4642,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 {
 	WindowAggState *winstate = winobj->winstate;
 	ExprContext *econtext = winstate->rprContext;
-	int			numDefineVars = list_length(winstate->defineVariableList);
-	int			varIdx = 0;
 	TupleTableSlot *slot;
 	int64		saved_pos;
 
@@ -4670,6 +4668,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 
 	foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
 	{
+		int			varIdx = foreach_current_index(exprState);
 		Datum		result;
 		bool		isnull;
 
@@ -4677,10 +4676,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 		result = ExecEvalExpr(exprState, econtext, &isnull);
 
 		varMatched[varIdx] = (!isnull && DatumGetBool(result));
-
-		varIdx++;
-		if (varIdx >= numDefineVars)
-			break;
 	}
 
 	winstate->currentpos = saved_pos;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 48b76a842ed..597a966c7b1 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -40,7 +40,6 @@
 #include "catalog/pg_proc.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
-#include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/rpr.h"
 #include "tcop/tcopprot.h"
@@ -1887,12 +1886,8 @@ reject_volatile_in_define_walker(Node *node, void *context)
 void
 validate_rpr_define_volatility(List *defineClause)
 {
-	ListCell   *lc;
-
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = lfirst_node(TargetEntry, lc);
-
 		(void) reject_volatile_in_define_walker((Node *) te->expr, NULL);
 	}
 }
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 8ed01bb8f28..3e6b2e579a3 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -54,8 +54,8 @@ typedef struct
 /* 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 List *transformDefineClause(ParseState *pstate, WindowDef *windef,
+								   List **targetlist);
 static bool define_walker(Node *node, void *context);
 
 /*
@@ -178,7 +178,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	wc->initial = windef->rpCommonSyntax->initial;
 
 	/* Transform DEFINE clause into list of TargetEntry's */
-	wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist);
+	wc->defineClause = transformDefineClause(pstate, windef, targetlist);
 
 	/* Store PATTERN AST for deparsing */
 	wc->rpPattern = windef->rpCommonSyntax->rpPattern;
@@ -313,7 +313,7 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  * parse_expr.c via the p_rpr_pattern_vars check.
  */
 static List *
-transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+transformDefineClause(ParseState *pstate, WindowDef *windef,
 					  List **targetlist)
 {
 	List	   *restargets;
@@ -345,6 +345,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
 	{
 		TargetEntry *teDefine;
+		Node	   *expr;
+		List	   *vars;
 
 		name = restarget->name;
 
@@ -374,54 +376,48 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		 * the individual Var nodes it references are present in the
 		 * targetlist, so the planner can propagate the referenced columns.
 		 */
-		{
-			Node	   *expr;
-			List	   *vars;
+		expr = transformExpr(pstate, restarget->val,
+							 EXPR_KIND_RPR_DEFINE);
 
-			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_node(Var, var, vars)
+		{
+			bool		found = false;
 
-			/*
-			 * Pull out Var nodes from the transformed expression and ensure
-			 * each one is present in the targetlist.  This is needed so the
-			 * planner propagates the referenced columns through the plan
-			 * tree, making them available to the WindowAgg's DEFINE
-			 * evaluation.
-			 */
-			vars = pull_var_clause(expr, 0);
-			foreach_node(Var, var, vars)
+			foreach_node(TargetEntry, tle, *targetlist)
 			{
-				bool		found = false;
-
-				foreach_node(TargetEntry, tle, *targetlist)
+				if (IsA(tle->expr, Var) &&
+					((Var *) tle->expr)->varno == var->varno &&
+					((Var *) tle->expr)->varattno == var->varattno)
 				{
-					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);
+					found = true;
+					break;
 				}
 			}
-			list_free(vars);
+			if (!found)
+			{
+				TargetEntry *newtle;
 
-			/* Build the defineClause entry directly from the transformed expr */
-			teDefine = makeTargetEntry((Expr *) expr,
-									   list_length(defineClause) + 1,
-									   pstrdup(name),
-									   true);
+				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);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e309dfbdb66..947e668020e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -644,7 +644,7 @@ typedef struct RPRPatternNode
 } RPRPatternNode;
 
 /*
- * RowPatternCommonSyntax - raw representation of row pattern common syntax
+ * RPCommonSyntax - raw representation of row pattern common syntax
  */
 typedef struct RPCommonSyntax
 {
-- 
2.50.1 (Apple Git-155)


From 7a69cb818e4d1c37998266a8c1883d5454a8d9f5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 14:55:51 +0900
Subject: [PATCH 08/13] Refactor transformDefineClause in row pattern
 recognition

Two related cleanups to DEFINE clause parse analysis, with no change to
planner or executor output beyond one error-cursor position:

- Hoist the "DEFINE variable not used in PATTERN" cross-check out of the
  recursive validateRPRPatternVarCount() into its caller.  The check only
  needs to run once, so the rpDefs argument and its NULL-sentinel gating
  are gone, and the recursive routine now only counts pattern variables.

- Reorder per-variable DEFINE processing to transformExpr ->
  coerce_to_boolean -> pull_var_clause and drop the separate second
  coercion pass, so pull_var_clause always operates on the final coerced
  expression and a type mismatch is reported before the targetlist is
  touched.  The duplicate-variable check moves to its own leading loop
  and now reports at the later (duplicate) definition.

Add regression coverage for DEFINE coercion and Var propagation: a
boolean-domain predicate (the one case where coerce_to_boolean is not a
no-op), a Var referenced only inside a navigation operation, and
rejection of a non-boolean DEFINE expression.
---
 src/backend/parser/parse_rpr.c         | 163 +++++++++++--------------
 src/test/regress/expected/rpr_base.out |  78 +++++++++++-
 src/test/regress/sql/rpr_base.sql      |  59 +++++++++
 3 files changed, 209 insertions(+), 91 deletions(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3e6b2e579a3..116cd206e39 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -53,7 +53,7 @@ typedef struct
 
 /* Forward declarations */
 static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
-									   List *rpDefs, List **varNames);
+									   List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowDef *windef,
 								   List **targetlist);
 static bool define_walker(Node *node, void *context);
@@ -192,15 +192,14 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  * Throws an error if the number of unique variables would require a varId
  * greater than RPR_VARID_MAX.
  *
- * If rpDefs is non-NULL, each DEFINE variable name is also validated against
- * varNames; any DEFINE name not present in PATTERN is rejected with an error.
- * varNames itself is not extended by this step -- it carries only PATTERN
- * variable names, which is what transformColumnRef checks via
- * p_rpr_pattern_vars to identify pattern variable qualifiers.
+ * varNames collects the unique PATTERN variable names, which is what
+ * transformColumnRef checks via p_rpr_pattern_vars to identify pattern
+ * variable qualifiers.  Cross-checking DEFINE variable names against this
+ * list is the caller's responsibility, since it only needs to run once.
  */
 static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
-						   List *rpDefs, List **varNames)
+						   List **varNames)
 {
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
@@ -255,39 +254,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			/* Recurse into children */
 			foreach_node(RPRPatternNode, child, node->children)
 			{
-				validateRPRPatternVarCount(pstate, child, NULL, varNames);
+				validateRPRPatternVarCount(pstate, child, varNames);
 			}
 			break;
 	}
-
-	/*
-	 * After the top-level call, validate that every DEFINE variable name is
-	 * present in the PATTERN variable list; reject names not used in PATTERN.
-	 * This is only done once at the outermost recursion level, detected by
-	 * rpDefs being non-NULL (recursive calls pass NULL).
-	 */
-	if (rpDefs)
-	{
-		foreach_node(ResTarget, rt, rpDefs)
-		{
-			bool		found = false;
-
-			foreach_node(String, varname, *varNames)
-			{
-				if (strcmp(strVal(varname), rt->name) == 0)
-				{
-					found = true;
-					break;
-				}
-			}
-			if (!found)
-				ereport(ERROR,
-						errcode(ERRCODE_SYNTAX_ERROR),
-						errmsg("DEFINE variable \"%s\" is not used in PATTERN",
-							   rt->name),
-						parser_errposition(pstate, rt->location));
-		}
-	}
 }
 
 /*
@@ -296,14 +266,16 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  *
  * First:
  *   1. Validates PATTERN variable count and collects RPR variable names
+ *   2. Rejects DEFINE variables not used in PATTERN
+ *   3. Checks for duplicate variable names in DEFINE clause
  *
  * 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
+ *   4. Transforms expression via transformExpr() and coerces it to boolean
+ *   5. Creates defineClause entry with proper resname (pattern variable name)
+ *   6. Ensures referenced Var nodes are present in the query targetlist (via
+ *      pull_var_clause)
+ *
+ * Finally 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.
@@ -316,9 +288,7 @@ static List *
 transformDefineClause(ParseState *pstate, WindowDef *windef,
 					  List **targetlist)
 {
-	List	   *restargets;
 	List	   *defineClause = NIL;
-	char	   *name;
 	List	   *patternVarNames = NIL;
 
 	/*
@@ -328,56 +298,86 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
 	Assert(windef->rpCommonSyntax->rpDefs != NULL);
 
 	/*
-	 * Validate PATTERN variable count, reject DEFINE variables not used in
-	 * PATTERN, and collect PATTERN variable names for transformColumnRef.
+	 * Validate PATTERN variable count and collect the PATTERN variable names
+	 * for transformColumnRef.
 	 */
 	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
-							   windef->rpCommonSyntax->rpDefs,
 							   &patternVarNames);
 	pstate->p_rpr_pattern_vars = patternVarNames;
 
+	/*
+	 * Reject any DEFINE variable whose name does not appear in PATTERN.  This
+	 * cross-check only needs to run once, so it lives here in the caller
+	 * rather than in the recursive validateRPRPatternVarCount().
+	 */
+	foreach_node(ResTarget, rt, windef->rpCommonSyntax->rpDefs)
+	{
+		bool		found = false;
+
+		foreach_node(String, varname, patternVarNames)
+		{
+			if (strcmp(strVal(varname), rt->name) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+		if (!found)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+						   rt->name),
+					parser_errposition(pstate, rt->location));
+	}
+
 	/*
 	 * Check for duplicate row pattern definition variables.  The standard
 	 * requires that no two row pattern definition variable names shall be
-	 * equivalent.
+	 * equivalent.  Report the error at the later (duplicate) definition.
 	 */
-	restargets = NIL;
 	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
 	{
-		TargetEntry *teDefine;
-		Node	   *expr;
-		List	   *vars;
-
-		name = restarget->name;
-
-		foreach_node(ResTarget, r, restargets)
+		foreach_node(ResTarget, prior, windef->rpCommonSyntax->rpDefs)
 		{
-			char	   *n;
-
-			n = r->name;
-
-			if (!strcmp(n, name))
+			if (prior == restarget)
+				break;
+			if (strcmp(prior->name, restarget->name) == 0)
 				ereport(ERROR,
 						errcode(ERRCODE_SYNTAX_ERROR),
 						errmsg("DEFINE variable \"%s\" appears more than once",
-							   name),
-						parser_errposition(pstate, exprLocation((Node *) r)));
+							   restarget->name),
+						parser_errposition(pstate,
+										   exprLocation((Node *) restarget)));
 		}
+	}
 
-		restargets = lappend(restargets, restarget);
+	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
+	{
+		TargetEntry *teDefine;
+		Node	   *expr;
+		List	   *vars;
 
 		/*
-		 * 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.
+		 * Transform the DEFINE expression and coerce it to boolean.  We must
+		 * NOT add the whole expression to the query targetlist, because it
+		 * may contain RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only
+		 * be evaluated inside the owning WindowAgg.  Coercing here, before
+		 * pull_var_clause, keeps pull_var_clause operating on the final
+		 * expression form and surfaces a type mismatch before the targetlist
+		 * is touched.
 		 */
 		expr = transformExpr(pstate, restarget->val,
 							 EXPR_KIND_RPR_DEFINE);
+		expr = coerce_to_boolean(pstate, expr, "DEFINE");
+
+		/* Build the defineClause entry directly from the transformed expr */
+		teDefine = makeTargetEntry((Expr *) expr,
+								   list_length(defineClause) + 1,
+								   pstrdup(restarget->name),
+								   true);
+
+		/* build transformed DEFINE clause (list of TargetEntry) */
+		defineClause = lappend(defineClause, teDefine);
 
 		/*
 		 * Pull out Var nodes from the transformed expression and ensure each
@@ -412,26 +412,9 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
 			}
 		}
 		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");
-
 	/*
 	 * Validate DEFINE expressions: nested PREV/NEXT, column references,
 	 * compound flatten, volatile callees -- all in a single walk per
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index cf158e1c043..80fabde514c 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -236,7 +236,7 @@ WINDOW w AS (
 );
 ERROR:  DEFINE variable "a" appears more than once
 LINE 7:     DEFINE A AS id > 0, A AS id < 10
-                   ^
+                                ^
 DROP TABLE rpr_dup;
 -- Boolean coercion
 CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
@@ -319,6 +319,82 @@ DROP CAST (truthyint AS boolean);
 DROP FUNCTION truthyint_to_bool(truthyint);
 DROP TYPE truthyint;
 DROP TABLE rpr_bool;
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS flag
+)
+ORDER BY id;
+ id | cnt 
+----+-----
+  1 |   1
+  2 |   0
+  3 |   1
+(3 rows)
+
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (UP+)
+    DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+ id | cnt 
+----+-----
+  1 |   0
+  2 |   3
+  3 |   0
+  4 |   0
+(4 rows)
+
+DROP TABLE rpr_nav;
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS n
+);
+ERROR:  argument of DEFINE must be type boolean, not type integer
+LINE 7:     DEFINE A AS n
+                        ^
+DROP TABLE rpr_noncoerce;
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS id > 0, B AS n
+);
+ERROR:  argument of DEFINE must be type boolean, not type integer
+LINE 7:     DEFINE A AS id > 0, B AS n
+                                     ^
+DROP TABLE rpr_noncoerce2;
 -- Complex expressions
 CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
 INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e71f0dd3680..21840aa77be 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -261,6 +261,65 @@ DROP TYPE truthyint;
 
 DROP TABLE rpr_bool;
 
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS flag
+)
+ORDER BY id;
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (UP+)
+    DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+DROP TABLE rpr_nav;
+
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS n
+);
+DROP TABLE rpr_noncoerce;
+
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS id > 0, B AS n
+);
+DROP TABLE rpr_noncoerce2;
+
 -- Complex expressions
 CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
 INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
-- 
2.50.1 (Apple Git-155)


From f3dda49e5a24347ff23da72f5fe80640291bc34c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 20:43:01 +0900
Subject: [PATCH 09/13] Replace a bare block with an else in the RPR DEFINE
 clause walker

define_walker() ended its phase handling with a bare braced block for
the DEFINE-body case, following two if blocks that respectively return
and raise an error.  Turn it into the else branch of an if / else if /
else chain.  No behavior change.
---
 src/backend/parser/parse_rpr.c | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 116cd206e39..7c201d55164 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -515,8 +515,7 @@ define_walker(Node *node, void *context)
 			ctx->nav_count++;
 			return expression_tree_walker(node, define_walker, ctx);
 		}
-
-		if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
+		else if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
 		{
 			/*
 			 * A navigation offset must be a run-time constant, so it cannot
@@ -528,13 +527,13 @@ define_walker(Node *node, void *context)
 					errdetail("A navigation offset must be a run-time constant."),
 					parser_errposition(ctx->pstate, nav->location));
 		}
-
-		/*
-		 * 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).
-		 */
+		else
 		{
+			/*
+			 * PHASE_BODY: this is an outer nav at top level.  Walk arg first
+			 * to collect nesting / column-ref state, then validate and (for
+			 * compound forms) flatten, then walk offset(s).
+			 */
 			DefineWalkCtx saved = *ctx;
 			bool		outer_phys = (nav->kind == RPR_NAV_PREV ||
 									  nav->kind == RPR_NAV_NEXT);
-- 
2.50.1 (Apple Git-155)


From 0e23c67f20f26c7f0e68f830ca5d8f8906a5e601 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 11:30:40 +0900
Subject: [PATCH 10/13] Rename loop index variables in row pattern deparse
 helpers

Give the index parameters and loop variables in the RPR pattern deparse
helpers more descriptive names: the construct-index parameter becomes
idx, the branch-number parameter becomes bno, and each helper's sole
loop variable becomes i.  Adjust the accompanying comments to match.

This is a cosmetic change with no effect on the deparsed output.
---
 src/backend/commands/explain.c | 68 +++++++++++++++++-----------------
 1 file changed, 34 insertions(+), 34 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 696bdb9c8b5..423cf352125 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -124,11 +124,11 @@ static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem);
 static char *deparse_rpr_pattern(RPRPattern *pattern);
 static int	deparse_rpr_seq(RPRPattern *pattern, int start, int limit,
 							StringInfo buf);
-static int	deparse_rpr_node(RPRPattern *pattern, int i, int limit,
+static int	deparse_rpr_node(RPRPattern *pattern, int idx, int limit,
 							 StringInfo buf);
 static int	rpr_match_end(RPRPattern *pattern, int beginIdx);
-static int	rpr_alt_scope_end(RPRPattern *pattern, int i);
-static int	rpr_next_branch(RPRPattern *pattern, int b, int altEnd);
+static int	rpr_alt_scope_end(RPRPattern *pattern, int idx);
+static int	rpr_next_branch(RPRPattern *pattern, int bno, int altEnd);
 static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
 							  ExplainState *es);
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -3014,8 +3014,8 @@ deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
 }
 
 /*
- * Deparse the single construct starting at index i, bounded by the inherited
- * limit.  Returns the index just past the construct.
+ * Deparse the single construct starting at index idx, bounded by the
+ * inherited limit.  Returns the index just past the construct.
  *
  * A VAR is its name plus quantifier.  A BEGIN opens a group spanning to its
  * matching END (rpr_match_end); when the group's sole child is an ALT that
@@ -3026,9 +3026,9 @@ deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
  * handed down by rpr_next_branch.
  */
 static int
-deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
+deparse_rpr_node(RPRPattern *pattern, int idx, int limit, StringInfo buf)
 {
-	RPRPatternElement *elem = &pattern->elements[i];
+	RPRPatternElement *elem = &pattern->elements[idx];
 
 	if (RPRElemIsVar(elem))
 	{
@@ -3036,27 +3036,27 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
 		appendStringInfoString(buf,
 							   quote_identifier(pattern->varNames[elem->varId]));
 		append_rpr_quantifier(buf, elem);
-		return i + 1;
+		return idx + 1;
 	}
 
 	if (RPRElemIsBegin(elem))
 	{
-		int			end = rpr_match_end(pattern, i);
+		int			end = rpr_match_end(pattern, idx);
 		bool		loneAlt;
 
-		loneAlt = (i + 1 < end &&
-				   RPRElemIsAlt(&pattern->elements[i + 1]) &&
-				   rpr_alt_scope_end(pattern, i + 1) == end);
+		loneAlt = (idx + 1 < end &&
+				   RPRElemIsAlt(&pattern->elements[idx + 1]) &&
+				   rpr_alt_scope_end(pattern, idx + 1) == end);
 
 		if (loneAlt)
 		{
 			/* The ALT child already parenthesizes the whole group body. */
-			(void) deparse_rpr_node(pattern, i + 1, end, buf);
+			(void) deparse_rpr_node(pattern, idx + 1, end, buf);
 		}
 		else
 		{
 			appendStringInfoChar(buf, '(');
-			(void) deparse_rpr_seq(pattern, i + 1, end, buf);
+			(void) deparse_rpr_seq(pattern, idx + 1, end, buf);
 			appendStringInfoChar(buf, ')');
 		}
 		append_rpr_quantifier(buf, &pattern->elements[end]);
@@ -3065,7 +3065,7 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
 
 	Assert(RPRElemIsAlt(elem));
 	{
-		int			altEnd = rpr_alt_scope_end(pattern, i);
+		int			altEnd = rpr_alt_scope_end(pattern, idx);
 		int			b;
 		bool		first = true;
 
@@ -3073,7 +3073,7 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
 			altEnd = limit;
 
 		appendStringInfoChar(buf, '(');
-		b = i + 1;
+		b = idx + 1;
 		while (b < altEnd)
 		{
 			int			nb = rpr_next_branch(pattern, b, altEnd);
@@ -3097,41 +3097,41 @@ static int
 rpr_match_end(RPRPattern *pattern, int beginIdx)
 {
 	RPRDepth	d = pattern->elements[beginIdx].depth;
-	int			j;
+	int			i;
 
-	for (j = beginIdx + 1; j < pattern->numElements; j++)
+	for (i = beginIdx + 1; i < pattern->numElements; i++)
 	{
-		RPRPatternElement *e = &pattern->elements[j];
+		RPRPatternElement *e = &pattern->elements[i];
 
 		if (RPRElemIsEnd(e) && e->depth == d)
-			return j;
+			return i;
 	}
 	pg_unreachable();			/* a BEGIN always has a matching END */
 }
 
 /*
- * Scope end of the construct at index i: the first following element whose
- * depth is no greater than i's own.  For an ALT marker this is the index just
- * past its last branch, since depth stays constant across branch boundaries.
- * FIN sits at depth 0, so a top-level ALT stops there.
+ * Scope end of the construct at index idx: the first following element whose
+ * depth is no greater than idx's own.  For an ALT marker this is the index
+ * just past its last branch, since depth stays constant across branch
+ * boundaries.  FIN sits at depth 0, so a top-level ALT stops there.
  */
 static int
-rpr_alt_scope_end(RPRPattern *pattern, int i)
+rpr_alt_scope_end(RPRPattern *pattern, int idx)
 {
-	RPRDepth	d = pattern->elements[i].depth;
-	int			k;
+	RPRDepth	d = pattern->elements[idx].depth;
+	int			i;
 
-	for (k = i + 1; k < pattern->numElements; k++)
+	for (i = idx + 1; i < pattern->numElements; i++)
 	{
-		if (pattern->elements[k].depth <= d)
-			return k;
+		if (pattern->elements[i].depth <= d)
+			return i;
 	}
 	return pattern->numElements;
 }
 
 /*
- * Boundary of the alternation branch starting at b (i.e. the start of the next
- * branch, or altEnd if b is the last branch).
+ * Boundary of the alternation branch starting at bno (i.e. the start of the
+ * next branch, or altEnd if bno is the last branch).
  *
  * The branch-start element's jump points at the next branch when this is not
  * the last branch.  jump is overloaded (a group BEGIN also uses it for its
@@ -3140,9 +3140,9 @@ rpr_alt_scope_end(RPRPattern *pattern, int i)
  * next redirected past the alternation, so it does not point at j.
  */
 static int
-rpr_next_branch(RPRPattern *pattern, int b, int altEnd)
+rpr_next_branch(RPRPattern *pattern, int bno, int altEnd)
 {
-	int			j = pattern->elements[b].jump;
+	int			j = pattern->elements[bno].jump;
 
 	if (j != RPR_ELEMIDX_INVALID && j < altEnd &&
 		pattern->elements[j - 1].next != j)
-- 
2.50.1 (Apple Git-155)


From 0bc60ef4dc9cba045f6ee82d8cfcb48e2f5cf712 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 13:04:00 +0900
Subject: [PATCH 11/13] Rename absorption "judgment point" to "comparison
 point" in comments

The absorption analysis comments described the ABSORBABLE element flag
as a "judgment point".  Use "comparison point" instead, which says more
directly what happens there: where consecutive iterations are compared.
This touches comments and the executor README only, across the planner,
executor, and EXPLAIN deparse, plus the rpr_base test comments; the
"equivalence judgment" wording and all identifiers are unchanged.

No change to behavior or to query output.
---
 src/backend/commands/explain.c         |  2 +-
 src/backend/executor/README.rpr        |  4 ++--
 src/backend/executor/execRPR.c         | 29 +++++++++++++-------------
 src/backend/optimizer/plan/rpr.c       | 14 ++++++-------
 src/include/optimizer/rpr.h            |  2 +-
 src/test/regress/expected/rpr_base.out |  6 +++---
 src/test/regress/sql/rpr_base.sql      |  6 +++---
 7 files changed, 32 insertions(+), 31 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 423cf352125..b3fc324718d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2937,7 +2937,7 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
 		appendStringInfoChar(buf, '?');
 	}
 
-	/* Append absorption markers: " for judgment point, ' for branch only */
+	/* Append absorption markers: " for comparison point, ' for branch only */
 	if (RPRElemIsAbsorbable(elem))
 	{
 		Assert(elem->max == RPR_QUANTITY_INF);
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 713ad84e1d9..50d1ff87f7e 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -285,7 +285,7 @@ Element flags (1 byte, bitmask):
         absorption.
 
   0x08  RPR_ELEM_ABSORBABLE         (VAR, END)
-        Absorption judgment point.  Where to compare consecutive
+        Absorption comparison point.  Where to compare consecutive
         iterations for absorption.
           - Simple unbounded VAR (A+): set on the VAR itself
           - Unbounded GROUP ((A B)+): set on the END element only
@@ -1393,7 +1393,7 @@ XII-5. Execution Optimization Summary
     counts) combination through multiple paths. Additionally, for
     group absorption, nfa_match performs inline advance from bounded
     VARs (count >= max) within the absorbable region (ABSORBABLE_BRANCH)
-    through END chains to reach the judgment point (ABSORBABLE END).
+    through END chains to reach the comparison point (ABSORBABLE END).
     This process can also produce duplicate states reaching the same END.
     nfa_add_state_unique() blocks duplicate addition of identical states
     in both cases.
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 099b81aeb81..90e3a068f04 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -578,7 +578,7 @@ nfa_update_absorption_flags(RPRNFAContext *ctx)
 	/*
 	 * Iterate through all states to check absorption status. Uses
 	 * state->isAbsorbable which tracks if state is in absorbable region. This
-	 * is different from RPRElemIsAbsorbable(elem) which checks judgment
+	 * is different from RPRElemIsAbsorbable(elem) which checks comparison
 	 * point.
 	 */
 	for (state = ctx->states; state != NULL; state = state->next)
@@ -625,8 +625,8 @@ nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *new
 		depth = elem->depth;
 
 		/*
-		 * Only compare at absorption judgment points (RPR_ELEM_ABSORBABLE).
-		 * Judgment points are where count-dominance guarantees the newer
+		 * Only compare at absorption comparison points (RPR_ELEM_ABSORBABLE).
+		 * Comparison points are where count-dominance guarantees the newer
 		 * context's future matches are a subset of the older's.
 		 */
 		if (!RPRElemIsAbsorbable(elem))
@@ -782,7 +782,8 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
  *     previous advance when count >= min was satisfied)
  *
  * For VARs that reached max count followed by END:
- *   - Advance through the END-element chain to the absorption judgment point
+ *   - Advance through the END-element chain to the absorption
+ *     comparison point
  *   - Only deterministic exits (count >= max, max != INF) are handled
  *   - Chains through END elements while count >= max (must-exit path)
  *
@@ -800,7 +801,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 	/*
 	 * Evaluate VAR elements against current row. For VARs that reach max
 	 * count with END next, advance through the chain of END elements inline
-	 * so absorb phase can compare states at judgment points.
+	 * so absorb phase can compare states at comparison points.
 	 */
 	for (state = ctx->states; state != NULL; state = nextState)
 	{
@@ -831,7 +832,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 				/*
 				 * For VAR at max count with END next, advance through END
-				 * chain to reach the absorption judgment point.  Only
+				 * chain to reach the absorption comparison point.  Only
 				 * deterministic exits (count >= max, max finite) are handled;
 				 * unbounded VARs stay for advance phase.
 				 *
@@ -841,10 +842,10 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 				 * to the next outer END.  The loop below walks this chain.
 				 *
 				 * ABSORBABLE_BRANCH marks elements inside the absorbable
-				 * region; ABSORBABLE marks the outermost judgment point where
-				 * count-dominance is evaluated.  We chain through BRANCH
-				 * elements until reaching the ABSORBABLE point or an element
-				 * that can still loop (count < max).
+				 * region; ABSORBABLE marks the outermost comparison point
+				 * where count-dominance is evaluated.  We chain through
+				 * BRANCH elements until reaching the ABSORBABLE point or an
+				 * element that can still loop (count < max).
 				 */
 				if (RPRElemIsAbsorbableBranch(elem) &&
 					!RPRElemIsAbsorbable(elem) &&
@@ -876,7 +877,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 					/*
 					 * Chain through END elements within the absorbable region
-					 * (ABSORBABLE_BRANCH) until reaching the judgment point
+					 * (ABSORBABLE_BRANCH) until reaching the comparison point
 					 * (ABSORBABLE).  Continue only on must-exit path (count
 					 * >= max) with END next.
 					 */
@@ -892,9 +893,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 						/*
 						 * Exit this intermediate group: clear its own count
 						 * (count-clear policy).  It sits below the absorbable
-						 * judgment point, so it is excluded from the
-						 * dominance comparison; the judgment point where the
-						 * chain stops keeps its count.
+						 * comparison point, so it is excluded from the
+						 * dominance comparison; the comparison point where
+						 * the chain stops keeps its count.
 						 */
 						state->counts[endDepth] = 0;
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 597a966c7b1..ebba8e50b1d 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -19,7 +19,7 @@
  *   complexity from O(n^2) to O(n) for patterns like A+ B.
  *
  *   The absorption analysis uses two element flags:
- *   - RPR_ELEM_ABSORBABLE: marks WHERE to compare (judgment point)
+ *   - RPR_ELEM_ABSORBABLE: marks WHERE to compare (comparison point)
  *   - RPR_ELEM_ABSORBABLE_BRANCH: marks the absorbable region
  *
  *   See computeAbsorbability() and the detailed comments before
@@ -1484,7 +1484,7 @@ finalizeRPRPattern(RPRPattern *result)
  *   than Ctx2's match (1 to current). So Ctx2 can be safely eliminated.
  *
  * Two Flags:
- *   1. RPR_ELEM_ABSORBABLE - "Absorption judgment point"
+ *   1. RPR_ELEM_ABSORBABLE - "Absorption comparison point"
  *      WHERE contexts can be compared for absorption.
  *      - Simple unbounded VAR (A+): the VAR element itself
  *      - Unbounded GROUP ((A B)+): the END element only
@@ -1506,20 +1506,20 @@ finalizeRPRPattern(RPRPattern *result)
  *                -> Both at END, comparable! Ctx1 absorbs Ctx2.
  *
  *   Contexts synchronize at END every group-length rows. Therefore:
- *   - ABSORBABLE marks END as judgment point (where to compare)
+ *   - ABSORBABLE marks END as comparison point (where to compare)
  *   - ABSORBABLE_BRANCH keeps state.isAbsorbable=true through A->B->END
  *
  * Pattern Examples:
  *
  *   Pattern: A+ B
- *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- judgment point
+ *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- comparison point
  *   Element 1 (B): (none)
  *   -> Compare at A every row. When contexts move to B, absorption stops.
  *
  *   Pattern: (A B)+ C
  *   Element 0 (A): ABSORBABLE_BRANCH
  *   Element 1 (B): ABSORBABLE_BRANCH
- *   Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH  <- judgment point
+ *   Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH  <- comparison point
  *   Element 3 (C): (none)
  *   -> Compare at END every 2 rows. When contexts move to C, absorption stops.
  *
@@ -1629,7 +1629,7 @@ isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx, RPRDepth scopeDepth)
  *      All children must have min == max (recursively for nested subgroups).
  *      This is equivalent to unrolling to {1,1} VARs, e.g., (A B B)+ C.
  *      All elements within the group get ABSORBABLE_BRANCH.
- *      Only the unbounded END gets ABSORBABLE (judgment point).
+ *      Only the unbounded END gets ABSORBABLE (comparison point).
  *      Examples:
  *        (A B{2})+ C          - B{2} has min==max, step=3
  *        (A (B C){2} D)+ E    - nested {2} subgroup, step=6
@@ -1791,7 +1791,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
  * decrease property required for safe absorption.
  *
  * This function sets two flags:
- *   RPR_ELEM_ABSORBABLE: Absorption judgment point
+ *   RPR_ELEM_ABSORBABLE: Absorption comparison point
  *     - Simple unbounded VAR: the VAR itself (e.g., A in A+)
  *     - Unbounded GROUP: the END element (e.g., END in (A B)+)
  *   RPR_ELEM_ABSORBABLE_BRANCH: All elements in absorbable region
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 23763442b65..83d3cd29b07 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -53,7 +53,7 @@
  * optimizer/plan/rpr.c.
  */
 #define RPR_ELEM_ABSORBABLE_BRANCH	0x04	/* element in absorbable region */
-#define RPR_ELEM_ABSORBABLE			0x08	/* absorption judgment point */
+#define RPR_ELEM_ABSORBABLE			0x08	/* absorption comparison point */
 
 /* Accessor macros for RPRPatternElement */
 #define RPRElemIsReluctant(e)			(((e)->flags & RPR_ELEM_RELUCTANT) != 0)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 80fabde514c..b385e972e7f 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -5472,9 +5472,9 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 -- Absorption Flag Display Tests
 -- ============================================================
 -- Tests absorption marker display in EXPLAIN output
--- Markers: ' = branch element, " = judgment point
+-- Markers: ' = branch element, " = comparison point
 -- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
--- Simple VAR: A+ -> a+" (judgment point)
+-- Simple VAR: A+ -> a+" (comparison point)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -5490,7 +5490,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
--- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 21840aa77be..af498fffb66 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -3254,16 +3254,16 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 -- Absorption Flag Display Tests
 -- ============================================================
 -- Tests absorption marker display in EXPLAIN output
--- Markers: ' = branch element, " = judgment point
+-- Markers: ' = branch element, " = comparison point
 -- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
 
--- Simple VAR: A+ -> a+" (judgment point)
+-- Simple VAR: A+ -> a+" (comparison point)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
 
--- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-- 
2.50.1 (Apple Git-155)


From 3cccad2ecc34be6d0b1e96afdca7468e4e9ab21f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 14:17:19 +0900
Subject: [PATCH 13/13] Document eval_nav_offset_helper's NULL/negative offset
 handling

eval_nav_offset_helper pre-evaluates a navigation offset at executor init
to size the frame trim, returning 0 for a NULL or negative offset rather
than rejecting it.  The comment did not say why, leaving the purpose of the
function and of those branches unclear.

Explain that a NULL or negative offset is caught per row on the navigation
path that consumes it, which errors out before navigation produces any
result, so the trim value computed here is never used.  The branches are
reachable -- a navigation offset can be a run-time constant such as a Param
-- and are already covered by the PREV(price, $1) tests in rpr.sql, so they
need neither a new test nor an assertion.
---
 src/backend/executor/nodeWindowAgg.c | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 819ad814bf5..eb1d616b49a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3985,10 +3985,15 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
 
 /*
  * eval_nav_offset_helper
- *		Evaluate an offset expression at executor init time for trim
- *		optimization.  Returns the offset value, or 0 for NULL/negative
- *		(these will cause a runtime error during actual navigation, so the
- *		trim value is irrelevant).
+ *		Pre-evaluate a navigation offset expression at executor init time, to
+ *		bound how far navigation can reach (which sizes the frame trim).
+ *		Returns the offset value, or 0 for a NULL or negative offset.
+ *
+ * The offset is not validated here.  A NULL or negative value is caught later,
+ * per row, on the navigation path that consumes it (see EEOP_RPR_NAV_SET in
+ * execExprInterp.c), which errors out before navigation produces any result;
+ * the trim sizing computed from such an offset is therefore never used, and 0
+ * is returned as a harmless placeholder.
  */
 static int64
 eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
-- 
2.50.1 (Apple Git-155)


From 9c7e5bc9dde1adafcff995370340975661890d3c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 13:43:58 +0900
Subject: [PATCH 12/13] Improve comments, documentation, and naming for row
 pattern recognition

A batch of clarity cleanups for row pattern recognition, none of which
change behavior or query output:

- Call the parser-built PATTERN structure a "parse tree" rather than an
  "AST", which is not PostgreSQL's usual term (parsenodes.h, parse_rpr.c,
  plan/rpr.c, and the executor README).
- Trim the transformDefineClause header comment, whose step list merely
  duplicated the inline comments.
- Drop a duplicated standard-citation sentence from the contain_rpr_walker
  header; transformWithClause already carries it.
- Fix a stale "ALT_START" reference to name the ALT marker, and normalize
  an "or -1" ParseLoc comment to the usual "or -1 if unknown".
- Move the EMPTY-alternative explanation in row_pattern_quantifier_opt
  into the action block, keeping the /*EMPTY*/ marker.
- Reword the Run Condition pushdown test comment to explain in SQL terms
  why count(*) is DECREASING over the required frame.
- Rename RPR_COUNT_MAX to RPR_COUNT_INF, defined from RPR_QUANTITY_INF.
  The old "MAX" name suggested a configurable repetition limit -- that is
  elem->max, a different thing -- which led to questions about erroring
  when a count reaches it.  The value is the int32 saturation ceiling, and
  a saturated count reads as "unbounded".
---
 src/backend/executor/README.rpr           | 44 +++++++++++------------
 src/backend/executor/execRPR.c            | 23 ++++++------
 src/backend/optimizer/plan/rpr.c          | 23 ++++++------
 src/backend/parser/gram.y                 |  7 ++--
 src/backend/parser/parse_cte.c            |  4 +--
 src/backend/parser/parse_rpr.c            | 19 ++--------
 src/include/nodes/parsenodes.h            | 12 +++----
 src/include/optimizer/rpr.h               |  9 ++++-
 src/test/regress/expected/rpr_explain.out | 12 ++++---
 src/test/regress/sql/rpr_explain.sql      | 12 ++++---
 10 files changed, 84 insertions(+), 81 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 50d1ff87f7e..9275e265d4b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -96,16 +96,16 @@ Chapter II  Overall Processing Pipeline
 
 RPR processing is divided into three phases:
 
-  +------------------------------------------------------------+
-  |  1. Parsing (Parser)                                       |
-  |     SQL text -> PATTERN AST + DEFINE expression tree       |
-  |                                                            |
-  |  2. Compilation (Optimizer/Planner)                        |
-  |     PATTERN AST -> optimization -> flat NFA element array  |
-  |                                                            |
-  |  3. Execution (Executor)                                   |
-  |     Row-by-row matching via NFA simulation                 |
-  +------------------------------------------------------------+
+  +--------------------------------------------------------------+
+  |  1. Parsing (Parser)                                         |
+  |     SQL text -> PATTERN parse tree + DEFINE expression tree  |
+  |                                                              |
+  |  2. Compilation (Optimizer/Planner)                          |
+  |     PATTERN parse tree -> optimization -> flat NFA elements  |
+  |                                                              |
+  |  3. Execution (Executor)                                     |
+  |     Row-by-row matching via NFA simulation                   |
+  +--------------------------------------------------------------+
 
 Each phase uses independent data structures, and the interfaces between
 phases are well-defined:
@@ -137,7 +137,7 @@ following:
 
   (3) DEFINE clause transformation (transformDefineClause)
 
-III-2. PATTERN AST (Abstract Syntax Tree)
+III-2. PATTERN parse tree
 
 The parser transforms the PATTERN clause into an RPRPatternNode tree.
 Each node has one of the following four types:
@@ -192,16 +192,16 @@ IV-1. Entry Point
 
 IV-2. The 6 Phases of buildRPRPattern()
 
-  Phase 1: AST optimization (optimizeRPRPattern)
+  Phase 1: parse tree optimization (optimizeRPRPattern)
   Phase 2: Statistics collection (scanRPRPattern)
   Phase 3: Memory allocation (makeRPRPattern)
   Phase 4: NFA element fill (fillRPRPattern)
   Phase 5: Finalization (finalizeRPRPattern)
   Phase 6: Absorbability analysis (computeAbsorbability)
 
-IV-3. Phase 1: AST Optimization
+IV-3. Phase 1: Parse Tree Optimization
 
-After copying the parser-generated AST, the following optimizations are
+After copying the parser-generated parse tree, the following optimizations are
 applied:
 
   (a) SEQ flattening: Unwrap nested SEQ nodes
@@ -236,7 +236,7 @@ applied:
 
 IV-4. Phase 4: NFA Element Array Generation
 
-Transforms the optimized AST into a flat array of RPRPatternElement.
+Transforms the optimized parse tree into a flat array of RPRPatternElement.
 This is the core data structure used for NFA simulation at runtime.
 
 RPRPatternElement struct (16 bytes):
@@ -298,7 +298,7 @@ Element flags (1 byte, bitmask):
 
 Example: PATTERN (A+ B | C)
 
-  AST: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
+  Parse tree: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
 
   Compilation result:
 
@@ -345,9 +345,9 @@ Example: PATTERN ((A B)+)
 
 IV-4a. Reluctant Flag (RPR_ELEM_RELUCTANT)
 
-The reluctant flag is set during Phase 4 (fillRPRPattern) when the AST node
-has reluctant == true. It reverses the priority of quantifier expansion at
-runtime:
+The reluctant flag is set during Phase 4 (fillRPRPattern) when the parse
+tree node has reluctant == true. It reverses the priority of quantifier
+expansion at runtime:
 
   Greedy (default):  try loop-back first, then exit  (prefer longer match)
   Reluctant:         try exit first, then loop-back   (prefer shorter match)
@@ -1363,9 +1363,9 @@ XII-5. Execution Optimization Summary
 
   -- Compile-time --
 
-  (1) AST Optimization (IV-3)
+  (1) Parse Tree Optimization (IV-3)
 
-    Simplifies the AST before converting the pattern to an NFA.
+    Simplifies the parse tree before converting the pattern to an NFA.
     Reduces the number of NFA elements through consecutive variable
     merging (A A -> A{2}), SEQ flattening, quantifier multiplication,
     and other transformations.
@@ -1468,7 +1468,7 @@ Appendix A. Key Function Index
   transformRPR                  parse_rpr.c           Parser entry point
   transformDefineClause         parse_rpr.c           DEFINE transformation
   buildRPRPattern               rpr.c                 NFA compilation main
-  optimizeRPRPattern            rpr.c                 AST optimization
+  optimizeRPRPattern            rpr.c                 parse tree optimization
   fillRPRPattern                rpr.c                 NFA element generation
   finalizeRPRPattern            rpr.c                 Finalization
   computeAbsorbability          rpr.c                 Absorption analysis
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 90e3a068f04..e5e30d79f01 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -821,8 +821,11 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 			if (matched)
 			{
-				/* Increment count */
-				if (count < RPR_COUNT_MAX)
+				/*
+				 * Increment count, saturating at RPR_COUNT_INF to avoid int32
+				 * overflow; a saturated count then compares as "unbounded".
+				 */
+				if (count < RPR_COUNT_INF)
 					count++;
 
 				/* Max constraint should not be exceeded */
@@ -857,7 +860,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 					int32		endCount = state->counts[endDepth];
 
 					/* Increment group count */
-					if (endCount < RPR_COUNT_MAX)
+					if (endCount < RPR_COUNT_INF)
 						endCount++;
 					Assert(endElem->max == RPR_QUANTITY_INF ||
 						   endCount <= endElem->max);
@@ -900,7 +903,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 						state->counts[endDepth] = 0;
 
 						/* Increment outer group count */
-						if (outerCount < RPR_COUNT_MAX)
+						if (outerCount < RPR_COUNT_INF)
 							outerCount++;
 						Assert(outerEnd->max == RPR_QUANTITY_INF ||
 							   outerCount <= outerEnd->max);
@@ -1180,7 +1183,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 
 			/* END->END: increment outer END's count */
 			if (RPRElemIsEnd(nextElem) &&
-				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
+				ffState->counts[nextElem->depth] < RPR_COUNT_INF)
 				ffState->counts[nextElem->depth]++;
 		}
 
@@ -1235,7 +1238,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			RPRElemIsAbsorbableBranch(nextElem);
 
 		/* END->END: increment outer END's count */
-		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
+		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_INF)
 			state->counts[nextElem->depth]++;
 
 		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
@@ -1262,7 +1265,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		nextElem = &elements[exitState->elemIdx];
 
 		/* END->END: increment outer END's count */
-		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
+		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_INF)
 			exitState->counts[nextElem->depth]++;
 
 		/* Prepare loop state */
@@ -1355,7 +1358,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			/* When exiting directly to an outer END, increment its count */
 			if (RPRElemIsEnd(nextElem))
 			{
-				if (cloneState->counts[nextElem->depth] < RPR_COUNT_MAX)
+				if (cloneState->counts[nextElem->depth] < RPR_COUNT_INF)
 					cloneState->counts[nextElem->depth]++;
 			}
 
@@ -1404,7 +1407,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			 */
 			if (RPRElemIsEnd(nextElem))
 			{
-				if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+				if (state->counts[nextElem->depth] < RPR_COUNT_INF)
 					state->counts[nextElem->depth]++;
 			}
 
@@ -1438,7 +1441,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* See comment above: increment outer END count for quantified VARs */
 		if (RPRElemIsEnd(nextElem))
 		{
-			if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+			if (state->counts[nextElem->depth] < RPR_COUNT_INF)
 				state->counts[nextElem->depth]++;
 		}
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index ebba8e50b1d..62292508aad 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -3,13 +3,13 @@
  * rpr.c
  *	  Row Pattern Recognition pattern compilation for planner
  *
- * This file contains functions for optimizing RPR pattern AST and
+ * This file contains functions for optimizing the RPR pattern parse tree and
  * compiling it to a flat element array for NFA execution by WindowAgg.
  *
  * Key components:
  *   1. Pattern Optimization: Simplifies patterns before compilation
  *      (e.g., flatten nested SEQ/ALT, merge consecutive vars)
- *   2. Pattern Compilation: Converts AST to flat element array for NFA
+ *   2. Pattern Compilation: Converts parse tree to flat element array for NFA
  *   3. Absorption Analysis: Computes flags for O(n^2)->O(n) optimization
  *
  * Context Absorption Optimization:
@@ -995,7 +995,7 @@ collectDefineVariables(List *defineVariableList, char **varNames)
 
 /*
  * scanRPRPatternRecursive
- *		Recursively scan pattern AST (pass 1 internal).
+ *		Recursively scan pattern parse tree (pass 1 internal).
  *
  * Collects unique variable names and counts elements while tracking depth.
  * Variables from DEFINE clause are already in varNames; this adds any
@@ -1092,7 +1092,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 
 /*
  * scanRPRPattern
- *		Scan pattern AST (pass 1 entry point).
+ *		Scan pattern parse tree (pass 1 entry point).
  *
  * Collects unique variable names (appending to those from DEFINE clause),
  * counts total elements (including FIN marker), and tracks maximum depth.
@@ -1297,7 +1297,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
  * fillRPRPatternAlt
  *		Fill an ALT pattern and its alternatives.
  *
- * Creates ALT_START marker, fills each alternative at increased depth,
+ * Creates the ALT marker, fills each alternative at increased depth,
  * sets jump pointers for backtracking, and next pointers for successful paths.
  *
  * Returns true if any branch is nullable (OR semantics: one nullable
@@ -1383,9 +1383,10 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 
 /*
  * fillRPRPattern
- *		Fill pattern elements array from AST (pass 2).
+ *		Fill pattern elements array from parse tree (pass 2).
  *
- * Recursively traverses AST and populates pre-allocated elements array.
+ * Recursively traverses the parse tree and populates pre-allocated elements
+ * array.
  * Dispatches to type-specific fill functions.
  *
  * Returns true if the pattern is nullable (can match zero rows).
@@ -1767,7 +1768,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
 	}
 	else
 	{
-		/* Should never reach END - structural invariant of pattern AST */
+		/* Should never reach END - structural invariant of pattern parse tree */
 		Assert(!RPRElemIsEnd(elem));
 
 		/* Non-ALT, non-BEGIN: check if unbounded start */
@@ -1894,13 +1895,13 @@ validate_rpr_define_volatility(List *defineClause)
 
 /*
  * buildRPRPattern
- *		Compile pattern AST to flat bytecode array.
+ *		Compile pattern parse tree to flat bytecode array.
  *
  * Compilation phases:
- *   1. Optimize AST (flatten, merge, deduplicate)
+ *   1. Optimize parse tree (flatten, merge, deduplicate)
  *   2. Scan: collect variables, count elements (pass 1)
  *   3. Allocate result structure
- *   4. Fill elements from AST (pass 2)
+ *   4. Fill elements from parse tree (pass 2)
  *   5. Finalize pattern structure
  *   6. Compute context absorption eligibility
  *
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4ae951aaeba..6eb01ea7f0b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17734,9 +17734,12 @@ row_pattern_primary:
 		;
 
 row_pattern_quantifier_opt:
-			/* EMPTY - no quantifier means exactly once; @$ is unused since
-			 * min=max=1 never produces an error */
+			/*EMPTY*/
 				{
+					/*
+					 * no quantifier means exactly once; @$ is unused since
+					 * min=max=1 never produces an error
+					 */
 					$$ = (Node *) makeRPRQuantifier(1, 1, false, @$);
 				}
 			| '*'
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index 3e493beba0b..c35feeae6fd 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -1303,9 +1303,7 @@ checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
 /*
  * contain_rpr_walker
  *	  Returns true if the raw parse tree contains any <row pattern common
- *	  syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.  Used
- *	  by transformWithClause() to enforce ISO/IEC 9075-2:2016 7.17 SR 3)f)
- *	  on WITH RECURSIVE elements.
+ *	  syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.
  */
 static bool
 contain_rpr_walker(Node *node, void *context)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 7c201d55164..ed12190cb06 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -8,7 +8,7 @@
  *   - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
  *   - Validates PATTERN variable count (max RPR_VARID_MAX + 1)
  *   - Transforms DEFINE clause
- *   - Stores the PATTERN AST and the SKIP TO/INITIAL flags
+ *   - Stores the PATTERN parse tree and the SKIP TO/INITIAL flags
  *
  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -67,7 +67,7 @@ static bool define_walker(Node *node, void *context);
  *   - Set AFTER MATCH SKIP TO flag
  *   - Set SEEK/INITIAL flag
  *   - Transforms DEFINE clause into TargetEntry list
- *   - Stores PATTERN AST for deparsing (optimization happens in planner)
+ *   - Stores PATTERN parse tree for deparsing (optimization happens in planner)
  *
  * Returns early if windef has no rpCommonSyntax (non-RPR window).
  */
@@ -180,7 +180,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	/* Transform DEFINE clause into list of TargetEntry's */
 	wc->defineClause = transformDefineClause(pstate, windef, targetlist);
 
-	/* Store PATTERN AST for deparsing */
+	/* Store PATTERN parse tree for deparsing */
 	wc->rpPattern = windef->rpCommonSyntax->rpPattern;
 }
 
@@ -264,19 +264,6 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  * transformDefineClause
  *		Process DEFINE clause and transform ResTarget into list of TargetEntry.
  *
- * First:
- *   1. Validates PATTERN variable count and collects RPR variable names
- *   2. Rejects DEFINE variables not used in PATTERN
- *   3. Checks for duplicate variable names in DEFINE clause
- *
- * Then for each DEFINE variable:
- *   4. Transforms expression via transformExpr() and coerces it to boolean
- *   5. Creates defineClause entry with proper resname (pattern variable name)
- *   6. Ensures referenced Var nodes are present in the query targetlist (via
- *      pull_var_clause)
- *
- * Finally 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.
  *
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 947e668020e..f28215b8e83 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -621,7 +621,7 @@ typedef enum RPRPatternNodeType
 } RPRPatternNodeType;
 
 /*
- * RPRPatternNode - Row Pattern Recognition pattern AST node
+ * RPRPatternNode - Row Pattern Recognition pattern parse tree node
  */
 typedef struct RPRPatternNode
 {
@@ -630,7 +630,7 @@ typedef struct RPRPatternNode
 	int32		min;			/* minimum repetitions (0 for *, ?) */
 	int32		max;			/* maximum repetitions (PG_INT32_MAX for *, +) */
 	bool		reluctant;		/* true for reluctant (non-greedy) */
-	ParseLoc	location;		/* token location, or -1 */
+	ParseLoc	location;		/* token location, or -1 if unknown */
 	char	   *varName;		/* VAR: variable name */
 	List	   *children;		/* SEQ, ALT, GROUP: child nodes */
 
@@ -652,10 +652,10 @@ typedef struct RPCommonSyntax
 	RPSkipTo	rpSkipTo;		/* Row Pattern AFTER MATCH SKIP type */
 	bool		initial;		/* true if <row pattern initial or seek> is
 								 * initial */
-	RPRPatternNode *rpPattern;	/* PATTERN clause AST */
+	RPRPatternNode *rpPattern;	/* PATTERN parse tree */
 	List	   *rpDefs;			/* row pattern definitions clause (list of
 								 * ResTarget) */
-	ParseLoc	location;		/* PATTERN keyword location, or -1 */
+	ParseLoc	location;		/* PATTERN keyword location, or -1 if unknown */
 } RPCommonSyntax;
 
 /*
@@ -1727,7 +1727,7 @@ typedef struct GroupingSet
  * options are never copied, per spec.
  * "defineClause" is Row Pattern Recognition DEFINE clause (list of
  * TargetEntry). TargetEntry.resname represents row pattern definition
- * variable name. "rpPattern" represents PATTERN clause as an AST tree
+ * variable name. "rpPattern" represents the PATTERN clause as a parse tree
  * (RPRPatternNode).
  *
  */
@@ -1763,7 +1763,7 @@ typedef struct WindowClause
 								 * initial */
 	/* Row Pattern DEFINE clause (list of TargetEntry) */
 	List	   *defineClause;
-	/* Row Pattern PATTERN clause AST */
+	/* Row Pattern PATTERN parse tree */
 	RPRPatternNode *rpPattern;
 } WindowClause;
 
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 83d3cd29b07..f5462ab2a7c 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -27,8 +27,15 @@
  * before release.
  */
 #define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
+
+/*
+ * RPR_COUNT_INF is the value a runtime repetition count saturates at to avoid
+ * int32 overflow (see the count++ guard in nfa_match).  It is defined as
+ * RPR_QUANTITY_INF so that a saturated count compares as "unbounded", just
+ * like an unbounded quantifier's max.
+ */
 #define RPR_QUANTITY_INF	PG_INT32_MAX	/* unbounded quantifier */
-#define RPR_COUNT_MAX		PG_INT32_MAX	/* max runtime count (NFA state) */
+#define RPR_COUNT_INF		RPR_QUANTITY_INF
 #define RPR_ELEMIDX_MAX		PG_INT16_MAX	/* max pattern elements */
 #define RPR_ELEMIDX_INVALID	((RPRElemIdx) -1)	/* invalid index */
 #define RPR_DEPTH_MAX		(PG_UINT8_MAX - 1)	/* max pattern nesting depth:
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index cc86d0aae30..8672b4c3055 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -5598,13 +5598,15 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
 -- find_window_run_conditions() pushes WHERE filters on monotonic window
 -- functions into WindowAgg as Run Conditions for early termination.
 -- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
--- FOLLOWING), the monotonic direction determines which operators trigger
--- Run Condition pushdown:
+-- FOLLOWING), the monotonic direction of the window function determines
+-- which comparison operators allow pushdown:
 --   INCREASING (<=): row_number, rank, dense_rank, percent_rank,
 --                    cume_dist, ntile
---   DECREASING (>):  count(*) (via int8inc, END_UNBOUNDED_FOLLOWING)
--- RPR window function results are match-dependent, not monotonic.
--- Test with count(*) > 0 as representative case.
+--   DECREASING (>):  count(*).  As the current row advances, the frame
+--                    (which ends at UNBOUNDED FOLLOWING) shrinks, so the
+--                    count decreases.
+-- RPR window function results are match-dependent, not monotonic, so this
+-- pushdown does not apply.  Test with count(*) > 0 as a representative case.
 --
 -- Without RPR: count(*) > 0 is pushed down as Run Condition
 EXPLAIN (COSTS OFF)
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 297ca78d54b..115402e304d 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -3174,13 +3174,15 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
 -- find_window_run_conditions() pushes WHERE filters on monotonic window
 -- functions into WindowAgg as Run Conditions for early termination.
 -- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
--- FOLLOWING), the monotonic direction determines which operators trigger
--- Run Condition pushdown:
+-- FOLLOWING), the monotonic direction of the window function determines
+-- which comparison operators allow pushdown:
 --   INCREASING (<=): row_number, rank, dense_rank, percent_rank,
 --                    cume_dist, ntile
---   DECREASING (>):  count(*) (via int8inc, END_UNBOUNDED_FOLLOWING)
--- RPR window function results are match-dependent, not monotonic.
--- Test with count(*) > 0 as representative case.
+--   DECREASING (>):  count(*).  As the current row advances, the frame
+--                    (which ends at UNBOUNDED FOLLOWING) shrinks, so the
+--                    count decreases.
+-- RPR window function results are match-dependent, not monotonic, so this
+-- pushdown does not apply.  Test with count(*) > 0 as a representative case.
 --
 
 -- Without RPR: count(*) > 0 is pushed down as Run Condition
-- 
2.50.1 (Apple Git-155)



Attachments:

  [text/plain] nocfbot-0001-drop-blank-line-churn.txt (3.3K, 3-nocfbot-0001-drop-blank-line-churn.txt)
  download | inline diff:
From 9d58cdf40d78efdb35131279a006b618213843db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 16 Jun 2026 23:26:16 +0900
Subject: [PATCH 01/13] Remove blank-line changes unrelated to row pattern
 recognition

The RPR patch added or dropped blank lines in five existing files with no
related code change.  Revert them so the files differ from the base only
where RPR actually touches them.
---
 src/backend/executor/nodeWindowAgg.c | 2 --
 src/backend/optimizer/plan/setrefs.c | 1 +
 src/backend/parser/parse_clause.c    | 1 +
 src/backend/utils/adt/ruleutils.c    | 1 -
 src/backend/utils/adt/windowfuncs.c  | 1 +
 5 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index cb6a484b7de..5b2385f1d8d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -177,7 +177,6 @@ typedef struct WindowStatePerAggData
 	bool		restart;		/* need to restart this agg in this cycle? */
 } WindowStatePerAggData;
 
-
 static void initialize_windowaggregate(WindowAggState *winstate,
 									   WindowStatePerFunc perfuncstate,
 									   WindowStatePerAgg peraggstate);
@@ -1086,7 +1085,6 @@ next_tuple:
 		ExecClearTuple(agg_row_slot);
 	}
 
-
 	/* The frame's end is not supposed to move backwards, ever */
 	Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto);
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 813a326bd78..6e4f3fd61e2 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -214,6 +214,7 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
 							   NodeTag elided_type, Bitmapset *relids);
 
+
 /*****************************************************************************
  *
  *		SUBPLAN REFERENCES
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 550ea4eb9c0..8eb367aa579 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -102,6 +102,7 @@ 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,
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 415da6417d4..4eb7e35bee4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7311,7 +7311,6 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
 		get_rule_orderby(wc->orderClause, targetList, false, context);
 		needspace = true;
 	}
-
 	/* framing clause is never inherited, so print unless it's default */
 	if (wc->frameOptions & FRAMEOPTION_NONDEFAULT)
 	{
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 3869f6c8994..d15aa0c75db 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -41,6 +41,7 @@ static bool rank_up(WindowObject winobj);
 static Datum leadlag_common(FunctionCallInfo fcinfo,
 							bool forward, bool withoffset, bool withdefault);
 
+
 /*
  * utility routine for *_rank functions.
  */
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0003-nav-by-name.txt (67.6K, 4-nocfbot-0003-nav-by-name.txt)
  download

  [text/plain] nocfbot-0002-drop-unused-includes.txt (2.6K, 5-nocfbot-0002-drop-unused-includes.txt)
  download | inline diff:
From 2025f549ef7c653b0e0e87f282ac8fa46feb2fc5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 08:16:57 +0900
Subject: [PATCH 02/13] Remove unnecessary includes from the row pattern
 recognition patch

Drop includes that are unused or covered by existing forward typedefs:
condition_variable.h, hsearch.h, queryenvironment.h from execnodes.h;
<limits.h> from rpr.c; windowapi.h from execRPR.h.  Switch rpr.h from
parsenodes.h to primnodes.h, where RPRNavExpr is actually defined.
---
 src/backend/optimizer/plan/rpr.c | 2 --
 src/include/executor/execRPR.h   | 1 -
 src/include/nodes/execnodes.h    | 3 ---
 src/include/optimizer/rpr.h      | 7 ++++---
 4 files changed, 4 insertions(+), 9 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 175777a8ffc..50bca59451f 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -37,8 +37,6 @@
 
 #include "postgres.h"
 
-#include <limits.h>
-
 #include "catalog/pg_proc.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
diff --git a/src/include/executor/execRPR.h b/src/include/executor/execRPR.h
index 7b2b0febb76..fb7dc63a4c6 100644
--- a/src/include/executor/execRPR.h
+++ b/src/include/executor/execRPR.h
@@ -15,7 +15,6 @@
 #define EXECRPR_H
 
 #include "nodes/execnodes.h"
-#include "windowapi.h"
 
 /* NFA context management */
 extern RPRNFAContext *ExecRPRStartContext(WindowAggState *winstate,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 792aa3f0d05..8060b018ef8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -38,9 +38,6 @@
 #include "nodes/plannodes.h"
 #include "partitioning/partdefs.h"
 #include "storage/buf.h"
-#include "storage/condition_variable.h"
-#include "utils/hsearch.h"
-#include "utils/queryenvironment.h"
 #include "utils/reltrigger.h"
 #include "utils/typcache.h"
 
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 802d2f1dd69..b4f87d8caa4 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -10,11 +10,12 @@
  *
  *-------------------------------------------------------------------------
  */
-#ifndef OPTIMIZER_RPR_H
-#define OPTIMIZER_RPR_H
+#ifndef RPR_H
+#define RPR_H
 
 #include "nodes/parsenodes.h"
 #include "nodes/plannodes.h"
+#include "nodes/primnodes.h"
 
 /* Limits and special values */
 /*
@@ -96,4 +97,4 @@ typedef struct NavTraversal
 
 extern bool nav_traversal_walker(Node *node, void *ctx);
 
-#endif							/* OPTIMIZER_RPR_H */
+#endif							/* RPR_H */
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0004-dedicated-define-exprcontext.txt (4.4K, 6-nocfbot-0004-dedicated-define-exprcontext.txt)
  download | inline diff:
From a70f1c1e753425adcec6021c07a0f5af6dc0968f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 15 Jun 2026 16:54:13 +0900
Subject: [PATCH 04/13] Use a dedicated ExprContext for RPR DEFINE clause
 evaluation

DEFINE clauses were evaluated in the per-output-tuple context.  Because
EEOP_RPR_NAV_RESTORE copies pass-by-reference navigation results into that
context, resetting it per row freed window function results before
ExecProject (a use-after-free), while not resetting it leaked across the
partition.  tmpcontext cannot be reused either, as ExecQualAndReset() resets
it mid-expression when NEXT re-enters spool_tuples.  Use a third ExprContext,
reset once per row and separate from both.
---
 src/backend/executor/execRPR.c       |  5 ++++-
 src/backend/executor/nodeWindowAgg.c | 25 ++++++++++++++++++++++---
 src/include/nodes/execnodes.h        |  1 +
 3 files changed, 27 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index b326a58bbf5..de78b06d277 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1592,10 +1592,13 @@ static void
 nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 							  int64 currentPos)
 {
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	ExprContext *econtext = winstate->rprContext;
 	int64		saved_match_start = winstate->nav_match_start;
 	int64		saved_pos = winstate->currentpos;
 
+	/* Release the previous evaluation's DEFINE expression memory */
+	ResetExprContext(econtext);
+
 	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
 	winstate->nav_match_start = ctx->matchStartRow;
 	winstate->currentpos = currentPos;
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 5b2385f1d8d..90f33bdee40 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2742,12 +2742,28 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 
 	/*
 	 * Create expression contexts.  We need two, one for per-input-tuple
-	 * processing and one for per-output-tuple processing.  We cheat a little
-	 * by using ExecAssignExprContext() to build both.
+	 * processing and one for per-output-tuple processing, plus an optional
+	 * third for row pattern recognition DEFINE evaluation (built just below
+	 * when a DEFINE clause is present).  We cheat a little by using
+	 * ExecAssignExprContext() to build them all.  Each call overwrites
+	 * ps_ExprContext, so the last call must establish the output context.
 	 */
 	ExecAssignExprContext(estate, &winstate->ss.ps);
 	tmpcontext = winstate->ss.ps.ps_ExprContext;
 	winstate->tmpcontext = tmpcontext;
+
+	/*
+	 * Row pattern recognition evaluates DEFINE clauses in a third context,
+	 * reset before each DEFINE evaluation pass.  It must be distinct from
+	 * tmpcontext and ps_ExprContext so its reset frees neither input nor
+	 * output tuple memory.
+	 */
+	if (node->defineClause != NIL)
+	{
+		ExecAssignExprContext(estate, &winstate->ss.ps);
+		winstate->rprContext = winstate->ss.ps.ps_ExprContext;
+	}
+
 	ExecAssignExprContext(estate, &winstate->ss.ps);
 
 	/* Create long-lived context for storage of partition-local memory etc */
@@ -4572,12 +4588,15 @@ static bool
 nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 {
 	WindowAggState *winstate = winobj->winstate;
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	ExprContext *econtext = winstate->rprContext;
 	int			numDefineVars = list_length(winstate->defineVariableList);
 	int			varIdx = 0;
 	TupleTableSlot *slot;
 	int64		saved_pos;
 
+	/* Release the previous row's DEFINE evaluation memory */
+	ResetExprContext(econtext);
+
 	/* Fetch current row into temp_slot_1 */
 	slot = winstate->temp_slot_1;
 	if (!window_gettupleslot(winobj, pos, slot))
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 8060b018ef8..5b4ac8a6c33 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2698,6 +2698,7 @@ typedef struct WindowAggState
 	MemoryContext aggcontext;	/* shared context for aggregate working data */
 	MemoryContext curaggcontext;	/* current aggregate's working data */
 	ExprContext *tmpcontext;	/* short-term evaluation context */
+	ExprContext *rprContext;	/* DEFINE clause evaluation context */
 
 	bool		all_first;		/* true if the scan is starting */
 	bool		partition_spooled;	/* true if all tuples in current partition
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0005-match-once-per-row.txt (19.8K, 7-nocfbot-0005-match-once-per-row.txt)
  download | inline diff:
From 0ed285dc51016e273694e9724e588493ee243564 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 16 Jun 2026 13:02:05 +0900
Subject: [PATCH 05/13] Drive RPR row pattern matching once per row

Row pattern matching only advanced when a window function read the frame,
so a row whose only window function skips the frame (e.g. nth_value() with
a NULL offset) left the match state behind the current row, producing
silently wrong results and a spurious "cannot fetch row before mark
position" error.

Advance the match once per row, before the window functions run, so it
tracks the row scan rather than frame access.  Extract the reduced-frame
loop into advance_reduced_frame_nfa() and the mark advance into
advance_nav_mark(), advancing the navigation mark from the frontier the
match reached rather than from the output row, so tuplestore_trim() frees
rows sooner.

Add regression coverage for the frame-skipping and PREV-only deferred-frame
cases, and assert the contexts' nondecreasing matchStartRow ordering.
---
 src/backend/executor/execRPR.c       |   8 +-
 src/backend/executor/nodeWindowAgg.c | 254 ++++++++++++++++-----------
 src/test/regress/expected/rpr.out    | 149 ++++++++++++++++
 src/test/regress/sql/rpr.sql         |  75 ++++++++
 4 files changed, 385 insertions(+), 101 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index de78b06d277..cea7e0b2973 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1666,7 +1666,13 @@ ExecRPRStartContext(WindowAggState *winstate, int64 startPos)
 		ctx->states->isAbsorbable = false;
 	}
 
-	/* Add to tail of active context list (doubly-linked, oldest-first) */
+	/*
+	 * Add to tail of active context list (doubly-linked, oldest-first).
+	 * matchStartRow is nondecreasing along the list, so the head holds the
+	 * smallest -- an ordering other code relies on.
+	 */
+	Assert(winstate->nfaContextTail == NULL ||
+		   startPos >= winstate->nfaContextTail->matchStartRow);
 	ctx->prev = winstate->nfaContextTail;
 	ctx->next = NULL;
 	if (winstate->nfaContextTail != NULL)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 90f33bdee40..2d97710da8a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -239,6 +239,10 @@ static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);
 
 static void clear_reduced_frame(WindowAggState *winstate);
 static int	get_reduced_frame_status(WindowAggState *winstate, int64 pos);
+static void advance_nav_mark(WindowAggState *winstate, int64 currentPos);
+static void advance_reduced_frame_nfa(WindowObject winobj,
+									  RPRNFAContext *targetCtx, int64 pos,
+									  bool hasLimitedFrame, int64 frameOffset);
 static void update_reduced_frame(WindowObject winobj, int64 pos);
 
 /* Forward declarations - NFA row evaluation */
@@ -2521,6 +2525,16 @@ ExecWindowAgg(PlanState *pstate)
 			{
 				if (winstate->rpSkipTo == ST_NEXT_ROW)
 					clear_reduced_frame(winstate);
+
+				/*
+				 * Drive the row pattern match every row, so it tracks the row
+				 * scan rather than frame access: a window function that skips
+				 * the frame (e.g. nth_value() with a NULL offset) must not
+				 * leave the match state behind currentpos.
+				 */
+				Assert(winstate->nav_winobj != NULL);
+				(void) row_is_in_reduced_frame(winstate->nav_winobj,
+											   winstate->currentpos);
 			}
 
 			/*
@@ -2562,43 +2576,6 @@ ExecWindowAgg(PlanState *pstate)
 		if (winstate->grouptail_ptr >= 0)
 			update_grouptailpos(winstate);
 
-		/*
-		 * Advance RPR navigation mark pointer if possible, so that
-		 * tuplestore_trim() can free rows no longer reachable by navigation.
-		 */
-		if (winstate->nav_winobj &&
-			winstate->rpPattern != NULL &&
-			winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED)
-		{
-			int64		navmarkpos;
-
-			/* Backward reach from PREV/LAST/compound PREV_LAST/NEXT_LAST */
-			if (winstate->currentpos > winstate->navMaxOffset)
-				navmarkpos = winstate->currentpos - winstate->navMaxOffset;
-			else
-				navmarkpos = 0;
-
-			/*
-			 * If FIRST is used, also consider match_start + navFirstOffset.
-			 * The oldest active context (nfaContext) has the smallest
-			 * matchStartRow.
-			 */
-			if (winstate->hasFirstNav &&
-				winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED &&
-				winstate->nfaContext != NULL)
-			{
-				int64		firstreach;
-
-				if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
-										 winstate->navFirstOffset,
-										 &firstreach))
-					navmarkpos = Min(navmarkpos, Max(firstreach, 0));
-			}
-
-			if (navmarkpos > winstate->nav_winobj->markpos)
-				WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
-		}
-
 		/*
 		 * Truncate any no-longer-needed rows from the tuplestore.
 		 */
@@ -4382,6 +4359,143 @@ get_reduced_frame_status(WindowAggState *winstate, int64 pos)
 	return RF_SKIPPED;
 }
 
+/*
+ * advance_nav_mark
+ *		Advance the RPR navigation mark, derived from the NFA frontier
+ *		(currentPos) but held back by the navigation's backward reach, so
+ *		tuplestore_trim() can free rows no longer reachable by navigation.
+ *
+ * The nav read pointer is independent of the aggregate and per-function read
+ * pointers, so moving its mark does not affect their fetches; it only bounds
+ * the DEFINE clause's own PREV/LAST/FIRST lookups.  Backward reach (PREV/LAST)
+ * is measured from the frontier.  FIRST reaches back from the head context's
+ * matchStartRow instead, so it is bounded separately; without FIRST the mark
+ * can follow the frontier freely.
+ */
+static void
+advance_nav_mark(WindowAggState *winstate, int64 currentPos)
+{
+	int64		navmarkpos;
+
+	/* No RPR navigation read pointer: nothing to advance */
+	if (winstate->nav_winobj == NULL)
+		return;
+
+	/* RETAIN_ALL disables trim for the backward (PREV/LAST) dimension */
+	if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_RETAIN_ALL)
+		return;
+
+	/* navMax is FIXED here: NEEDS_EVAL resolved, RETAIN_ALL returned */
+	Assert(winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED);
+
+	if (currentPos > winstate->navMaxOffset)
+		navmarkpos = currentPos - winstate->navMaxOffset;
+	else
+		navmarkpos = 0;
+
+	if (winstate->hasFirstNav && winstate->nfaContext != NULL)
+	{
+		int64		firstreach;
+
+		/* navFirst is always FIXED; it never takes RETAIN_ALL */
+		Assert(winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED);
+
+		/*
+		 * Head context has the smallest matchStartRow (contexts appended in
+		 * nondecreasing order), so bounding by it covers every FIRST reach.
+		 */
+		if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
+								 winstate->navFirstOffset,
+								 &firstreach))
+			navmarkpos = Min(navmarkpos, Max(firstreach, 0));
+	}
+
+	if (navmarkpos > winstate->nav_winobj->markpos)
+		WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
+}
+
+/*
+ * advance_reduced_frame_nfa
+ *		Drive the NFA forward until targetCtx completes or the partition ends.
+ *
+ * This is the match driver, extracted from update_reduced_frame(), which calls
+ * it to advance the match and then records the resolved result.  Row
+ * evaluations are shared across all active contexts.
+ */
+static void
+advance_reduced_frame_nfa(WindowObject winobj, RPRNFAContext *targetCtx,
+						  int64 pos, bool hasLimitedFrame, int64 frameOffset)
+{
+	WindowAggState *winstate = winobj->winstate;
+	int64		currentPos;
+	int64		startPos;
+
+	/*
+	 * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
+	 * pos since contexts are created at currentPos+1 during processing.
+	 * However, pos can exceed this when rows are skipped (e.g., unmatched
+	 * rows don't update nfaLastProcessedRow).
+	 */
+	startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
+
+	/*
+	 * Process rows until target context completes or we hit boundaries. Each
+	 * row evaluation is shared across all active contexts.
+	 */
+	for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
+	{
+		bool		rowExists;
+
+		/*
+		 * Evaluate variables for this row - done only once, shared by all
+		 * contexts.
+		 *
+		 * Set nav_match_start to the head context's matchStartRow for
+		 * FIRST/LAST navigation.  Match_start-dependent variables (FIRST,
+		 * LAST-with-offset) are re-evaluated per-context in ExecRPRProcessRow
+		 * when matchStartRow differs.
+		 */
+		winstate->nav_match_start = targetCtx->matchStartRow;
+		rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched);
+
+		/* No more rows in partition? Finalize all contexts */
+		if (!rowExists)
+		{
+			ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
+			/* Clean up dead contexts from finalization */
+			ExecRPRCleanupDeadContexts(winstate, targetCtx);
+			break;
+		}
+
+		/* Update last processed row */
+		winstate->nfaLastProcessedRow = currentPos;
+
+		/*--------------------------
+		 * Process all contexts for this row:
+		 *   1. Match all (convergence)
+		 *   2. Absorb redundant
+		 *   3. Advance all (divergence)
+		 */
+		ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
+
+		/*
+		 * Create a new context for the next potential start position. This
+		 * enables overlapping match detection for SKIP TO NEXT ROW.
+		 */
+		ExecRPRStartContext(winstate, currentPos + 1);
+
+		/*
+		 * Clean up dead contexts (failed with no active states and no match).
+		 * This removes contexts that failed during processing and counts them
+		 * appropriately as pruned or mismatched.
+		 */
+		ExecRPRCleanupDeadContexts(winstate, targetCtx);
+
+		/* Advance the nav mark to the frontier so trim can free old rows. */
+		advance_nav_mark(winstate, currentPos);
+	}
+}
+
 /*
  * update_reduced_frame
  *		Update reduced frame info using multi-context NFA pattern matching.
@@ -4401,8 +4515,6 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 {
 	WindowAggState *winstate = winobj->winstate;
 	RPRNFAContext *targetCtx;
-	int64		currentPos;
-	int64		startPos;
 	int			frameOptions = winstate->frameOptions;
 	bool		hasLimitedFrame;
 	int64		frameOffset = 0;
@@ -4468,67 +4580,9 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		goto register_result;
 	}
 
-	/*
-	 * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
-	 * pos since contexts are created at currentPos+1 during processing.
-	 * However, pos can exceed this when rows are skipped (e.g., unmatched
-	 * rows don't update nfaLastProcessedRow).
-	 */
-	startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
-
-	/*
-	 * Process rows until target context completes or we hit boundaries. Each
-	 * row evaluation is shared across all active contexts.
-	 */
-	for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
-	{
-		bool		rowExists;
-
-		/*
-		 * Evaluate variables for this row - done only once, shared by all
-		 * contexts.
-		 *
-		 * Set nav_match_start to the head context's matchStartRow for
-		 * FIRST/LAST navigation.  Match_start-dependent variables (FIRST,
-		 * LAST-with-offset) are re-evaluated per-context in ExecRPRProcessRow
-		 * when matchStartRow differs.
-		 */
-		winstate->nav_match_start = targetCtx->matchStartRow;
-		rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched);
-
-		/* No more rows in partition? Finalize all contexts */
-		if (!rowExists)
-		{
-			ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
-			/* Clean up dead contexts from finalization */
-			ExecRPRCleanupDeadContexts(winstate, targetCtx);
-			break;
-		}
-
-		/* Update last processed row */
-		winstate->nfaLastProcessedRow = currentPos;
-
-		/*--------------------------
-		 * Process all contexts for this row:
-		 *   1. Match all (convergence)
-		 *   2. Absorb redundant
-		 *   3. Advance all (divergence)
-		 */
-		ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
-
-		/*
-		 * Create a new context for the next potential start position. This
-		 * enables overlapping match detection for SKIP TO NEXT ROW.
-		 */
-		ExecRPRStartContext(winstate, currentPos + 1);
-
-		/*
-		 * Clean up dead contexts (failed with no active states and no match).
-		 * This removes contexts that failed during processing and counts them
-		 * appropriately as pruned or mismatched.
-		 */
-		ExecRPRCleanupDeadContexts(winstate, targetCtx);
-	}
+	/* Drive the NFA forward until pos's match is resolved. */
+	advance_reduced_frame_nfa(winobj, targetCtx, pos, hasLimitedFrame,
+							  frameOffset);
 
 register_result:
 	Assert(pos == targetCtx->matchStartRow);
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index dc5140fecc9..6ad830b9e36 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -3297,6 +3297,155 @@ WINDOW w AS (
   4 |  20 |           |   0
 (4 rows)
 
+--
+-- nth_value with a NULL offset
+--
+CREATE TABLE rpr_dormant (id int, price int);
+INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g;
+-- reference: first_value(id) is the start row of the match beginning at the
+-- current row, count(*) is that match's length over the reduced frame
+SELECT * FROM (
+  SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len
+  FROM rpr_dormant
+  WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+) s WHERE id > 50 ORDER BY id;
+ id | match_start | match_len 
+----+-------------+-----------
+ 51 |          51 |        10
+ 52 |             |         0
+ 53 |             |         0
+ 54 |             |         0
+ 55 |             |         0
+ 56 |             |         0
+ 57 |             |         0
+ 58 |             |         0
+ 59 |             |         0
+ 60 |             |         0
+(10 rows)
+
+-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv  
+----+-----
+ 51 | 510
+ 52 |    
+ 53 |    
+ 54 |    
+ 55 |    
+ 56 |    
+ 57 |    
+ 58 |    
+ 59 |    
+ 60 |    
+(10 rows)
+
+-- the same window with first_value and count alongside nth_value
+SELECT * FROM (
+  SELECT id, nv, fv, cnt FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv,
+               first_value(id) OVER w AS fv, count(*) OVER w AS cnt
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv  | fv | cnt 
+----+-----+----+-----
+ 51 | 510 | 51 |  10
+ 52 |     |    |   0
+ 53 |     |    |   0
+ 54 |     |    |   0
+ 55 |     |    |   0
+ 56 |     |    |   0
+ 57 |     |    |   0
+ 58 |     |    |   0
+ 59 |     |    |   0
+ 60 |     |    |   0
+(10 rows)
+
+-- the same nth_value with a non-navigation DEFINE
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > 0)
+  ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv 
+----+----
+ 51 |   
+ 52 |   
+ 53 |   
+ 54 |   
+ 55 |   
+ 56 |   
+ 57 |   
+ 58 |   
+ 59 |   
+ 60 |   
+(10 rows)
+
+-- the same nth_value with a PREV-only DEFINE (no FIRST navigation)
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(price, 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv  
+----+-----
+ 51 | 510
+ 52 |    
+ 53 |    
+ 54 |    
+ 55 |    
+ 56 |    
+ 57 |    
+ 58 |    
+ 59 |    
+ 60 |    
+(10 rows)
+
+-- nth_value with a NULL offset band in the middle of the partition
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id BETWEEN 38 AND 46 ORDER BY id;
+ id | nv 
+----+----
+ 38 |   
+ 39 |   
+ 40 |   
+ 41 |   
+ 42 |   
+ 43 |   
+ 44 |   
+ 45 |   
+ 46 |   
+(9 rows)
+
+DROP TABLE rpr_dormant;
 --
 -- NULL handling
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e3e9de789db..3363691c041 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1812,6 +1812,81 @@ WINDOW w AS (
     B AS val IS NULL
 );
 
+--
+-- nth_value with a NULL offset
+--
+
+CREATE TABLE rpr_dormant (id int, price int);
+INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g;
+
+-- reference: first_value(id) is the start row of the match beginning at the
+-- current row, count(*) is that match's length over the reduced frame
+SELECT * FROM (
+  SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len
+  FROM rpr_dormant
+  WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+) s WHERE id > 50 ORDER BY id;
+
+-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same window with first_value and count alongside nth_value
+SELECT * FROM (
+  SELECT id, nv, fv, cnt FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv,
+               first_value(id) OVER w AS fv, count(*) OVER w AS cnt
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same nth_value with a non-navigation DEFINE
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > 0)
+  ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same nth_value with a PREV-only DEFINE (no FIRST navigation)
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(price, 50))
+  ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- nth_value with a NULL offset band in the middle of the partition
+SELECT * FROM (
+  SELECT id, nv FROM (
+    SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv
+    FROM rpr_dormant
+    WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+      AFTER MATCH SKIP PAST LAST ROW
+      PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+  ) s
+) t WHERE id BETWEEN 38 AND 46 ORDER BY id;
+
+DROP TABLE rpr_dormant;
+
 --
 -- NULL handling
 --
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0006-tidy-plumbing.txt (26.2K, 8-nocfbot-0006-tidy-plumbing.txt)
  download | inline diff:
From b8484088192a3ea8d24bdbff4b4596450e1383c3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 13:12:36 +0900
Subject: [PATCH 06/13] Tidy up row pattern recognition plumbing

Several behavior-neutral cleanups to the row pattern recognition code,
with no change to planner or executor output:

- Remove the dead collectPatternVariables() and buildDefineVariableList()
  helpers.  The parser already guarantees that every DEFINE variable
  appears in PATTERN, so create_windowagg_plan() and cost_windowagg() walk
  the DEFINE clause directly instead.

- Drop the redundant rpSkipTo and defineClause arguments of
  make_windowagg(); both are read from the WindowClause.

- Mark RPRNavExpr.resulttype as query_jumble_ignore, matching the other
  derived result-type fields.

- Rename WindowAggState.defineClauseList to defineClauseExprs to reflect
  that it stores ExprState nodes.

- Replace a post-loop ListCell NULL test in remove_unused_subquery_outputs()
  with a boolean flag, flatten the nested block in show_window_def(), and
  flatten the DEFINE init loop in ExecInitWindowAgg().

- Minor regression-test comment cleanup.
---
 src/backend/commands/explain.c                | 94 +++++++++----------
 src/backend/executor/README.rpr               |  5 +-
 src/backend/executor/execRPR.c                |  2 +-
 src/backend/executor/nodeWindowAgg.c          | 41 ++++----
 src/backend/optimizer/path/allpaths.c         | 13 ++-
 src/backend/optimizer/path/costsize.c         | 22 +----
 src/backend/optimizer/plan/createplan.c       | 24 +++--
 src/backend/optimizer/plan/rpr.c              | 77 ---------------
 src/include/nodes/execnodes.h                 |  2 +-
 src/include/nodes/primnodes.h                 |  3 +-
 src/include/optimizer/rpr.h                   |  3 -
 src/test/regress/expected/rpr_integration.out | 29 ++----
 src/test/regress/sql/rpr_integration.sql      | 31 ++----
 13 files changed, 110 insertions(+), 236 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 70fd7f386a0..696bdb9c8b5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3212,77 +3212,73 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 	/* Show Row Pattern Recognition pattern if present */
 	if (wagg->rpPattern != NULL)
 	{
+		RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
+		int64		maxOffset = wagg->navMaxOffset;
+		RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
+		int64		firstOffset = wagg->navFirstOffset;
+
 		char	   *patternStr = deparse_rpr_pattern(wagg->rpPattern);
 
-		if (patternStr != NULL)
-		{
-			ExplainPropertyText("Pattern", patternStr, es);
-			pfree(patternStr);
-		}
+		ExplainPropertyText("Pattern", patternStr, es);
+
+		pfree(patternStr);
 
 		/*
 		 * Show navigation offsets for tuplestore trim.  For EXPLAIN ANALYZE,
 		 * use the executor-resolved values (which may differ from the plan
 		 * when NEEDS_EVAL was resolved to FIXED or RETAIN_ALL at init).
 		 */
+		if (es->analyze)
 		{
-			RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
-			int64		maxOffset = wagg->navMaxOffset;
-			RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
-			int64		firstOffset = wagg->navFirstOffset;
+			maxKind = planstate->navMaxOffsetKind;
+			maxOffset = planstate->navMaxOffset;
+			firstKind = planstate->navFirstOffsetKind;
+			firstOffset = planstate->navFirstOffset;
+		}
 
-			if (es->analyze)
-			{
-				maxKind = planstate->navMaxOffsetKind;
-				maxOffset = planstate->navMaxOffset;
-				firstKind = planstate->navFirstOffsetKind;
-				firstOffset = planstate->navFirstOffset;
-			}
+		switch (maxKind)
+		{
+			case RPR_NAV_OFFSET_NEEDS_EVAL:
+				ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+				break;
+			case RPR_NAV_OFFSET_RETAIN_ALL:
+				ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+				break;
+			case RPR_NAV_OFFSET_FIXED:
+				ExplainPropertyInteger("Nav Mark Lookback", NULL,
+									   maxOffset, es);
+				break;
+			default:
+				elog(ERROR, "unrecognized RPR nav offset kind: %d",
+					 maxKind);
+				break;
+		}
 
-			switch (maxKind)
+		if (wagg->hasFirstNav)
+		{
+			switch (firstKind)
 			{
 				case RPR_NAV_OFFSET_NEEDS_EVAL:
-					ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+					ExplainPropertyText("Nav Mark Lookahead", "runtime",
+										es);
 					break;
 				case RPR_NAV_OFFSET_RETAIN_ALL:
-					ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+					ExplainPropertyText("Nav Mark Lookahead", "retain all",
+										es);
 					break;
 				case RPR_NAV_OFFSET_FIXED:
-					ExplainPropertyInteger("Nav Mark Lookback", NULL,
-										   maxOffset, es);
+					if (firstOffset == PG_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",
-						 maxKind);
+						 firstKind);
 					break;
 			}
-
-			if (wagg->hasFirstNav)
-			{
-				switch (firstKind)
-				{
-					case RPR_NAV_OFFSET_NEEDS_EVAL:
-						ExplainPropertyText("Nav Mark Lookahead", "runtime",
-											es);
-						break;
-					case RPR_NAV_OFFSET_RETAIN_ALL:
-						ExplainPropertyText("Nav Mark Lookahead", "retain all",
-											es);
-						break;
-					case RPR_NAV_OFFSET_FIXED:
-						if (firstOffset == PG_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",
-							 firstKind);
-						break;
-				}
-			}
 		}
 	}
 }
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 00af86681b8..713ad84e1d9 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -188,7 +188,6 @@ Chapter IV  Compilation Phase
 IV-1. Entry Point
 
   create_windowagg_plan() (createplan.c)
-    +-- buildDefineVariableList()    Build variable name list from DEFINE
     +-- buildRPRPattern()           NFA compilation (6 phases)
 
 IV-2. The 6 Phases of buildRPRPattern()
@@ -1468,8 +1467,6 @@ Appendix A. Key Function Index
   --------------------------------------------------------------------------
   transformRPR                  parse_rpr.c           Parser entry point
   transformDefineClause         parse_rpr.c           DEFINE transformation
-  collectPatternVariables       rpr.c                 Variable collection
-  buildDefineVariableList       rpr.c                 DEFINE variable list
   buildRPRPattern               rpr.c                 NFA compilation main
   optimizeRPRPattern            rpr.c                 AST optimization
   fillRPRPattern                rpr.c                 NFA element generation
@@ -1538,7 +1535,7 @@ Appendix B. Data Structure Relationship Diagram
     |--- rpSkipTo: RPSkipTo (AFTER MATCH SKIP mode)
     |--- rpPattern: RPRPattern* (copied from plan)
     |--- defineVariableList: List<String> (variable names, DEFINE order)
-    |--- defineClauseList: List<ExprState>
+    |--- defineClauseExprs: List<ExprState>
     |--- nfaVarMatched: bool[] (per-row cache)
     |--- defineMatchStartDependent: Bitmapset* (match_start_dependent
     |        DEFINE vars; see VI-4)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index cea7e0b2973..def0b8423b3 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1606,7 +1606,7 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Invalidate nav_slot cache since match_start changed */
 	winstate->nav_slot_pos = -1;
 
-	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
 	{
 		int			varIdx = foreach_current_index(exprState);
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 2d97710da8a..5d4832d1db9 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3067,27 +3067,26 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 
 	/* Set up row pattern recognition DEFINE clause */
 	winstate->defineVariableList = NIL;
-	winstate->defineClauseList = NIL;
-	if (node->defineClause != NIL)
+	winstate->defineClauseExprs = NIL;
+
+	/*
+	 * Compile DEFINE clause expressions.  PREV/NEXT navigation is handled by
+	 * EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so no
+	 * varno rewriting is needed here.
+	 */
+	foreach_node(TargetEntry, te, node->defineClause)
 	{
-		/*
-		 * Compile DEFINE clause expressions.  PREV/NEXT navigation is handled
-		 * by EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so
-		 * no varno rewriting is needed here.
-		 */
-		foreach_node(TargetEntry, te, node->defineClause)
-		{
-			char	   *name = te->resname;
-			Expr	   *expr = te->expr;
-			ExprState  *exps;
-
-			winstate->defineVariableList =
-				lappend(winstate->defineVariableList,
-						makeString(pstrdup(name)));
-			exps = ExecInitExpr(expr, (PlanState *) winstate);
-			winstate->defineClauseList =
-				lappend(winstate->defineClauseList, exps);
-		}
+		char	   *name = te->resname;
+		ExprState  *exprstate;
+
+		winstate->defineVariableList =
+			lappend(winstate->defineVariableList,
+					makeString(pstrdup(name)));
+
+		exprstate = ExecInitExpr(te->expr, (PlanState *) winstate);
+
+		winstate->defineClauseExprs =
+			lappend(winstate->defineClauseExprs, exprstate);
 	}
 
 	/* Initialize NFA free lists for row pattern matching */
@@ -4669,7 +4668,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	/* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
 	winstate->nav_slot_pos = -1;
 
-	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
 	{
 		Datum		result;
 		bool		isnull;
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f3c9f3c0bd6..44864b3635b 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4807,20 +4807,19 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		 */
 		if (IsA(texpr, WindowFunc))
 		{
+			bool		is_rpr = false;
 			WindowFunc *wfunc = (WindowFunc *) texpr;
-			ListCell   *wlc;
 
-			foreach(wlc, subquery->windowClause)
+			foreach_node(WindowClause, wc, subquery->windowClause)
 			{
-				WindowClause *wc = lfirst_node(WindowClause, wlc);
-
-				if (wc->winref == wfunc->winref &&
-					wc->defineClause != NIL)
+				if (wc->winref == wfunc->winref && wc->defineClause != NIL)
 				{
+					is_rpr = true;
 					break;
 				}
 			}
-			if (wlc != NULL)
+
+			if (is_rpr)
 				continue;
 		}
 
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 82472c3fe96..32a4b172e3d 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3231,30 +3231,18 @@ cost_windowagg(Path *path, PlannerInfo *root,
 	 * so we'll just do this for now.
 	 *
 	 * Moreover, if row pattern recognition is used, we charge the DEFINE
-	 * expressions once per tuple for each variable that appears in PATTERN.
+	 * expressions once per tuple for each DEFINE variable.
 	 */
 	if (winclause->rpPattern)
 	{
-		List	   *pattern_vars;
 		QualCost	defcosts;
 
-		pattern_vars = collectPatternVariables(winclause->rpPattern);
-
-		foreach_node(String, pv, pattern_vars)
+		foreach_node(TargetEntry, def, winclause->defineClause)
 		{
-			char	   *ptname = strVal(pv);
-
-			foreach_node(TargetEntry, def, winclause->defineClause)
-			{
-				if (!strcmp(ptname, def->resname))
-				{
-					cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
-					startup_cost += defcosts.startup;
-					total_cost += defcosts.per_tuple * input_tuples;
-				}
-			}
+			cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
+			startup_cost += defcosts.startup;
+			total_cost += defcosts.per_tuple * input_tuples;
 		}
-		list_free_deep(pattern_vars);
 	}
 
 	foreach(lc, windowFuncs)
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cca4126e511..d96e76b0221 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -290,9 +290,8 @@ static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators,
 static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
 								 int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
-								 List *runCondition, RPSkipTo rpSkipTo,
+								 List *runCondition,
 								 RPRPattern *compiledPattern,
-								 List *defineClause,
 								 Bitmapset *defineMatchStartDependent,
 								 RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
 								 bool hasFirstNav,
@@ -2822,7 +2821,6 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 	Oid		   *ordCollations;
 	ListCell   *lc;
 	List	   *defineVariableList = NIL;
-	List	   *filteredDefineClause = NIL;
 	RPRPattern *compiledPattern = NULL;
 	Bitmapset  *matchStartDependent = NULL;
 	RPRNavOffsetKind navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
@@ -2889,8 +2887,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		 * rejects DEFINE variables not used in PATTERN, so no filtering is
 		 * needed.
 		 */
-		buildDefineVariableList(wc->defineClause, &defineVariableList);
-		filteredDefineClause = wc->defineClause;
+		foreach_node(TargetEntry, te, wc->defineClause)
+			defineVariableList = lappend(defineVariableList,
+										 makeString(pstrdup(te->resname)));
 
 		/*
 		 * Walk DEFINE once: collect nav offsets (for tuplestore trim) and the
@@ -2923,13 +2922,13 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  ordOperators,
 						  ordCollations,
 						  best_path->runCondition,
-						  wc->rpSkipTo,
 						  compiledPattern,
-						  filteredDefineClause,
 						  matchStartDependent,
-						  navMaxOffsetKind, navMaxOffset,
+						  navMaxOffsetKind,
+						  navMaxOffset,
 						  hasFirstNav,
-						  navFirstOffsetKind, navFirstOffset,
+						  navFirstOffsetKind,
+						  navFirstOffset,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -7000,9 +6999,8 @@ static WindowAgg *
 make_windowagg(List *tlist, WindowClause *wc,
 			   int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
 			   int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
-			   List *runCondition, RPSkipTo rpSkipTo,
+			   List *runCondition,
 			   RPRPattern *compiledPattern,
-			   List *defineClause,
 			   Bitmapset *defineMatchStartDependent,
 			   RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
 			   bool hasFirstNav,
@@ -7034,12 +7032,12 @@ make_windowagg(List *tlist, WindowClause *wc,
 	node->inRangeAsc = wc->inRangeAsc;
 	node->inRangeNullsFirst = wc->inRangeNullsFirst;
 	node->topWindow = topWindow;
-	node->rpSkipTo = rpSkipTo;
+	node->rpSkipTo = wc->rpSkipTo;
 
 	/* Store compiled pattern for NFA execution */
 	node->rpPattern = compiledPattern;
 
-	node->defineClause = defineClause;
+	node->defineClause = wc->defineClause;
 
 	/* Store pre-computed match_start dependency bitmapset */
 	node->defineMatchStartDependent = defineMatchStartDependent;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 50bca59451f..48b76a842ed 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -96,9 +96,6 @@ static void computeAbsorbabilityRecursive(RPRPattern *pattern,
 										  bool *hasAbsorbable);
 static void computeAbsorbability(RPRPattern *pattern);
 
-static void collectPatternVariablesRecursive(RPRPatternNode *node,
-											 List **varNames);
-
 /*
  * rprPatternEqual
  *		Compare two RPRPatternNode trees for equality.
@@ -1824,59 +1821,6 @@ computeAbsorbability(RPRPattern *pattern)
 	pattern->isAbsorbable = hasAbsorbable;
 }
 
-/*
- * collectPatternVariablesRecursive
- *		Recursively collect variable names from pattern AST.
- */
-static void
-collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
-{
-	Assert(node != NULL);
-
-	check_stack_depth();
-
-	switch (node->nodeType)
-	{
-		case RPR_PATTERN_VAR:
-			/* Add variable if not already in list */
-			foreach_node(String, varname, *varNames)
-			{
-				if (strcmp(strVal(varname), node->varName) == 0)
-					return;		/* Already collected */
-			}
-			*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
-			break;
-
-		case RPR_PATTERN_SEQ:
-		case RPR_PATTERN_ALT:
-		case RPR_PATTERN_GROUP:
-			foreach_node(RPRPatternNode, child, node->children)
-			{
-				collectPatternVariablesRecursive(child, varNames);
-			}
-			break;
-	}
-}
-
-/*
- * collectPatternVariables
- *		Collect unique variable names used in PATTERN clause.
- *
- * Returns a list of String nodes containing variable names.
- * Used to filter defineClause before varId assignment.
- */
-List *
-collectPatternVariables(RPRPatternNode *pattern)
-{
-	List	   *varNames = NIL;
-
-	/* Caller ensures pattern is not NULL */
-	Assert(pattern != NULL);
-
-	collectPatternVariablesRecursive(pattern, &varNames);
-	return varNames;
-}
-
 /*
  * rpr_volatile_func_checker
  *		check_functions_in_node callback: true if funcid is VOLATILE.
@@ -1953,27 +1897,6 @@ validate_rpr_define_volatility(List *defineClause)
 	}
 }
 
-/*
- * buildDefineVariableList
- *		Build defineVariableList from defineClause.
- *
- * The parser already ensures that all DEFINE variables appear in PATTERN,
- * so no filtering is needed here.
- *
- * Sets *defineVariableList to list of variable names (String nodes).
- */
-void
-buildDefineVariableList(List *defineClause, List **defineVariableList)
-{
-	*defineVariableList = NIL;
-
-	foreach_node(TargetEntry, te, defineClause)
-	{
-		*defineVariableList = lappend(*defineVariableList,
-									  makeString(pstrdup(te->resname)));
-	}
-}
-
 /*
  * buildRPRPattern
  *		Compile pattern AST to flat bytecode array.
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5b4ac8a6c33..59b8656c857 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2654,7 +2654,7 @@ typedef struct WindowAggState
 	struct RPRPattern *rpPattern;	/* compiled pattern for NFA execution */
 	List	   *defineVariableList; /* list of row pattern definition
 									 * variables (list of String) */
-	List	   *defineClauseList;	/* expression for row pattern definition
+	List	   *defineClauseExprs;	/* expression for row pattern definition
 									 * search conditions ExprState list */
 	RPRNFAContext *nfaContext;	/* active matching contexts (head) */
 	RPRNFAContext *nfaContextTail;	/* tail of active contexts (for reverse
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 7c1b3d1bb07..8576a3c9c5b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -687,7 +687,8 @@ typedef struct RPRNavExpr
 	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) */
+	/* result type (same as arg's type) */
+	Oid			resulttype pg_node_attr(query_jumble_ignore);
 	/* OID of collation of result */
 	Oid			resultcollid pg_node_attr(query_jumble_ignore);
 	/* token location, or -1 if unknown */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index b4f87d8caa4..23763442b65 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -67,10 +67,7 @@
 #define RPRElemIsFin(e)			((e)->varId == RPR_VARID_FIN)
 #define RPRElemCanSkip(e)		((e)->min == 0)
 
-extern List *collectPatternVariables(RPRPatternNode *pattern);
 extern void validate_rpr_define_volatility(List *defineClause);
-extern void buildDefineVariableList(List *defineClause,
-									List **defineVariableList);
 extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 								   RPSkipTo rpSkipTo, int frameOptions,
 								   bool hasMatchStartDependent);
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 80a32cca9ab..652989927d7 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -240,9 +240,9 @@ ORDER BY id;
 -- must therefore yield two separate WindowAgg nodes.  Inline specs
 -- are used here because dedup only applies to inline windows.
 -- Baseline: two inline RPR windows that are structurally identical
--- (same ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
--- parser into a single WindowAgg node, confirming that parser-level
--- dedup is active for RPR windows whose DEFINE matches.
+-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
+-- parser into a single WindowAgg node, confirming that parser-level dedup is
+-- active for RPR windows whose DEFINE matches.
 EXPLAIN (COSTS OFF)
 SELECT
     count(*) OVER (ORDER BY id
@@ -325,15 +325,10 @@ ORDER BY id;
 -- A5. Unused window removal prevention
 -- ============================================================
 -- Verify that remove_unused_subquery_outputs() does not drop an RPR
--- window function even when the outer query does not reference its
--- result.  The RPR WindowAgg node is responsible for performing pattern
--- matching, so removing the window function would silently skip the
--- pattern match even though the surrounding query still depends on
--- RPR semantics.
--- The outer query ignores the per-row window result, yet pattern
--- matching must still execute.  The plan must still contain a
--- WindowAgg node below the outer Aggregate; if the window were
--- removed, only Aggregate + Seq Scan would appear.
+-- window function when the outer query does not reference its result.
+-- The WindowAgg node performs the pattern match itself; without it,
+-- the match would be silently skipped.  The plan must contain a
+-- WindowAgg node beneath the outer Aggregate.
 EXPLAIN (COSTS OFF)
 SELECT count(*) FROM (
     SELECT count(*) OVER w FROM rpr_integ
@@ -587,10 +582,8 @@ WHERE cnt > 0;
 -- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on
 -- the outer WindowAgg, with no DEFINE-derived expression leaking in.
 -- Note: columns referenced by DEFINE (e.g., "val") may appear as
--- resjunk entries in upper WindowAgg targetlists -- that is a
--- harmless byproduct of the column guard's broad scope and does not
--- affect client output.  The claim here is limited to the full
--- DEFINE boolean expression.
+-- resjunk entries in upper WindowAgg targetlists -- but that is harmless.
+-- The claim here is limited to the full DEFINE boolean expression.
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT
     count(*) OVER w_rpr AS rpr_cnt,
@@ -620,10 +613,6 @@ WINDOW
                      Output: id, val
 (13 rows)
 
--- Executing the same query shows the client result is limited to
--- the two projected columns; "id" and "val" that appeared in the
--- upper WindowAgg Output line are resjunk-only and do not reach
--- the client.
 SELECT
     count(*) OVER w_rpr AS rpr_cnt,
     count(*) OVER w_normal AS normal_cnt
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 1d6075265bb..2b990b24704 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -166,9 +166,9 @@ ORDER BY id;
 -- are used here because dedup only applies to inline windows.
 
 -- Baseline: two inline RPR windows that are structurally identical
--- (same ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
--- parser into a single WindowAgg node, confirming that parser-level
--- dedup is active for RPR windows whose DEFINE matches.
+-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
+-- parser into a single WindowAgg node, confirming that parser-level dedup is
+-- active for RPR windows whose DEFINE matches.
 EXPLAIN (COSTS OFF)
 SELECT
     count(*) OVER (ORDER BY id
@@ -214,16 +214,10 @@ ORDER BY id;
 -- A5. Unused window removal prevention
 -- ============================================================
 -- Verify that remove_unused_subquery_outputs() does not drop an RPR
--- window function even when the outer query does not reference its
--- result.  The RPR WindowAgg node is responsible for performing pattern
--- matching, so removing the window function would silently skip the
--- pattern match even though the surrounding query still depends on
--- RPR semantics.
-
--- The outer query ignores the per-row window result, yet pattern
--- matching must still execute.  The plan must still contain a
--- WindowAgg node below the outer Aggregate; if the window were
--- removed, only Aggregate + Seq Scan would appear.
+-- window function when the outer query does not reference its result.
+-- The WindowAgg node performs the pattern match itself; without it,
+-- the match would be silently skipped.  The plan must contain a
+-- WindowAgg node beneath the outer Aggregate.
 EXPLAIN (COSTS OFF)
 SELECT count(*) FROM (
     SELECT count(*) OVER w FROM rpr_integ
@@ -399,11 +393,8 @@ WHERE cnt > 0;
 -- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on
 -- the outer WindowAgg, with no DEFINE-derived expression leaking in.
 -- Note: columns referenced by DEFINE (e.g., "val") may appear as
--- resjunk entries in upper WindowAgg targetlists -- that is a
--- harmless byproduct of the column guard's broad scope and does not
--- affect client output.  The claim here is limited to the full
--- DEFINE boolean expression.
-
+-- resjunk entries in upper WindowAgg targetlists -- but that is harmless.
+-- The claim here is limited to the full DEFINE boolean expression.
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT
     count(*) OVER w_rpr AS rpr_cnt,
@@ -417,10 +408,6 @@ WINDOW
     w_normal AS (ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
 
--- Executing the same query shows the client result is limited to
--- the two projected columns; "id" and "val" that appeared in the
--- upper WindowAgg Output line are resjunk-only and do not reach
--- the client.
 SELECT
     count(*) OVER w_rpr AS rpr_cnt,
     count(*) OVER w_normal AS normal_cnt
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0007-tidy-plumbing-more.txt (9.0K, 9-nocfbot-0007-tidy-plumbing-more.txt)
  download | inline diff:
From 4cf9108ce7796a3821873f8377c95e34799760f8 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 14:05:25 +0900
Subject: [PATCH 07/13] Further tidy up row pattern recognition plumbing

More behavior-neutral cleanups to the row pattern recognition code,
continuing the previous tidy-up and with no change to planner or
executor output:

- Drop the now-unused WindowClause argument of transformDefineClause()
  and flatten the redundant nested block in its DEFINE-variable loop.

- Replace manual ListCell iteration and index bookkeeping with
  foreach_node()/foreach_current_index() in
  validate_rpr_define_volatility() and nfa_evaluate_row(), and drop the
  redundant end-of-list break tests in nfa_evaluate_row() and
  nfa_reevaluate_dependent_vars() now that the loops walk
  defineClauseExprs directly.

- Test winstate->defineVariableList against NIL instead of
  list_length() > 0 in ExecInitWindowAgg().

- Remove the now-unused makefuncs.h include from plan/rpr.c.

- Fix the stale struct name in the RPCommonSyntax header comment.
---
 src/backend/executor/execRPR.c       |  3 -
 src/backend/executor/nodeWindowAgg.c |  9 +--
 src/backend/optimizer/plan/rpr.c     |  7 +--
 src/backend/parser/parse_rpr.c       | 84 +++++++++++++---------------
 src/include/nodes/parsenodes.h       |  2 +-
 5 files changed, 44 insertions(+), 61 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index def0b8423b3..099b81aeb81 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1618,9 +1618,6 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 			result = ExecEvalExpr(exprState, econtext, &isnull);
 			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
 		}
-
-		if (varIdx + 1 >= list_length(winstate->defineVariableList))
-			break;
 	}
 
 	/* Restore original match_start, currentpos, and invalidate cache */
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 5d4832d1db9..819ad814bf5 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3103,7 +3103,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	 * ordering (DEFINE order first), varId == defineIdx for all defined
 	 * variables, so no mapping is needed.
 	 */
-	if (list_length(winstate->defineVariableList) > 0)
+	if (winstate->defineVariableList != NIL)
 		winstate->nfaVarMatched = palloc0(sizeof(bool) *
 										  list_length(winstate->defineVariableList));
 	else
@@ -4642,8 +4642,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 {
 	WindowAggState *winstate = winobj->winstate;
 	ExprContext *econtext = winstate->rprContext;
-	int			numDefineVars = list_length(winstate->defineVariableList);
-	int			varIdx = 0;
 	TupleTableSlot *slot;
 	int64		saved_pos;
 
@@ -4670,6 +4668,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 
 	foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
 	{
+		int			varIdx = foreach_current_index(exprState);
 		Datum		result;
 		bool		isnull;
 
@@ -4677,10 +4676,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 		result = ExecEvalExpr(exprState, econtext, &isnull);
 
 		varMatched[varIdx] = (!isnull && DatumGetBool(result));
-
-		varIdx++;
-		if (varIdx >= numDefineVars)
-			break;
 	}
 
 	winstate->currentpos = saved_pos;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 48b76a842ed..597a966c7b1 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -40,7 +40,6 @@
 #include "catalog/pg_proc.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
-#include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/rpr.h"
 #include "tcop/tcopprot.h"
@@ -1887,12 +1886,8 @@ reject_volatile_in_define_walker(Node *node, void *context)
 void
 validate_rpr_define_volatility(List *defineClause)
 {
-	ListCell   *lc;
-
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = lfirst_node(TargetEntry, lc);
-
 		(void) reject_volatile_in_define_walker((Node *) te->expr, NULL);
 	}
 }
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 8ed01bb8f28..3e6b2e579a3 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -54,8 +54,8 @@ typedef struct
 /* 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 List *transformDefineClause(ParseState *pstate, WindowDef *windef,
+								   List **targetlist);
 static bool define_walker(Node *node, void *context);
 
 /*
@@ -178,7 +178,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	wc->initial = windef->rpCommonSyntax->initial;
 
 	/* Transform DEFINE clause into list of TargetEntry's */
-	wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist);
+	wc->defineClause = transformDefineClause(pstate, windef, targetlist);
 
 	/* Store PATTERN AST for deparsing */
 	wc->rpPattern = windef->rpCommonSyntax->rpPattern;
@@ -313,7 +313,7 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  * parse_expr.c via the p_rpr_pattern_vars check.
  */
 static List *
-transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+transformDefineClause(ParseState *pstate, WindowDef *windef,
 					  List **targetlist)
 {
 	List	   *restargets;
@@ -345,6 +345,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
 	{
 		TargetEntry *teDefine;
+		Node	   *expr;
+		List	   *vars;
 
 		name = restarget->name;
 
@@ -374,54 +376,48 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		 * the individual Var nodes it references are present in the
 		 * targetlist, so the planner can propagate the referenced columns.
 		 */
-		{
-			Node	   *expr;
-			List	   *vars;
+		expr = transformExpr(pstate, restarget->val,
+							 EXPR_KIND_RPR_DEFINE);
 
-			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_node(Var, var, vars)
+		{
+			bool		found = false;
 
-			/*
-			 * Pull out Var nodes from the transformed expression and ensure
-			 * each one is present in the targetlist.  This is needed so the
-			 * planner propagates the referenced columns through the plan
-			 * tree, making them available to the WindowAgg's DEFINE
-			 * evaluation.
-			 */
-			vars = pull_var_clause(expr, 0);
-			foreach_node(Var, var, vars)
+			foreach_node(TargetEntry, tle, *targetlist)
 			{
-				bool		found = false;
-
-				foreach_node(TargetEntry, tle, *targetlist)
+				if (IsA(tle->expr, Var) &&
+					((Var *) tle->expr)->varno == var->varno &&
+					((Var *) tle->expr)->varattno == var->varattno)
 				{
-					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);
+					found = true;
+					break;
 				}
 			}
-			list_free(vars);
+			if (!found)
+			{
+				TargetEntry *newtle;
 
-			/* Build the defineClause entry directly from the transformed expr */
-			teDefine = makeTargetEntry((Expr *) expr,
-									   list_length(defineClause) + 1,
-									   pstrdup(name),
-									   true);
+				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);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e309dfbdb66..947e668020e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -644,7 +644,7 @@ typedef struct RPRPatternNode
 } RPRPatternNode;
 
 /*
- * RowPatternCommonSyntax - raw representation of row pattern common syntax
+ * RPCommonSyntax - raw representation of row pattern common syntax
  */
 typedef struct RPCommonSyntax
 {
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0008-refactor-define.txt (15.5K, 10-nocfbot-0008-refactor-define.txt)
  download | inline diff:
From 7a69cb818e4d1c37998266a8c1883d5454a8d9f5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 14:55:51 +0900
Subject: [PATCH 08/13] Refactor transformDefineClause in row pattern
 recognition

Two related cleanups to DEFINE clause parse analysis, with no change to
planner or executor output beyond one error-cursor position:

- Hoist the "DEFINE variable not used in PATTERN" cross-check out of the
  recursive validateRPRPatternVarCount() into its caller.  The check only
  needs to run once, so the rpDefs argument and its NULL-sentinel gating
  are gone, and the recursive routine now only counts pattern variables.

- Reorder per-variable DEFINE processing to transformExpr ->
  coerce_to_boolean -> pull_var_clause and drop the separate second
  coercion pass, so pull_var_clause always operates on the final coerced
  expression and a type mismatch is reported before the targetlist is
  touched.  The duplicate-variable check moves to its own leading loop
  and now reports at the later (duplicate) definition.

Add regression coverage for DEFINE coercion and Var propagation: a
boolean-domain predicate (the one case where coerce_to_boolean is not a
no-op), a Var referenced only inside a navigation operation, and
rejection of a non-boolean DEFINE expression.
---
 src/backend/parser/parse_rpr.c         | 163 +++++++++++--------------
 src/test/regress/expected/rpr_base.out |  78 +++++++++++-
 src/test/regress/sql/rpr_base.sql      |  59 +++++++++
 3 files changed, 209 insertions(+), 91 deletions(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3e6b2e579a3..116cd206e39 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -53,7 +53,7 @@ typedef struct
 
 /* Forward declarations */
 static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
-									   List *rpDefs, List **varNames);
+									   List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowDef *windef,
 								   List **targetlist);
 static bool define_walker(Node *node, void *context);
@@ -192,15 +192,14 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  * Throws an error if the number of unique variables would require a varId
  * greater than RPR_VARID_MAX.
  *
- * If rpDefs is non-NULL, each DEFINE variable name is also validated against
- * varNames; any DEFINE name not present in PATTERN is rejected with an error.
- * varNames itself is not extended by this step -- it carries only PATTERN
- * variable names, which is what transformColumnRef checks via
- * p_rpr_pattern_vars to identify pattern variable qualifiers.
+ * varNames collects the unique PATTERN variable names, which is what
+ * transformColumnRef checks via p_rpr_pattern_vars to identify pattern
+ * variable qualifiers.  Cross-checking DEFINE variable names against this
+ * list is the caller's responsibility, since it only needs to run once.
  */
 static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
-						   List *rpDefs, List **varNames)
+						   List **varNames)
 {
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
@@ -255,39 +254,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			/* Recurse into children */
 			foreach_node(RPRPatternNode, child, node->children)
 			{
-				validateRPRPatternVarCount(pstate, child, NULL, varNames);
+				validateRPRPatternVarCount(pstate, child, varNames);
 			}
 			break;
 	}
-
-	/*
-	 * After the top-level call, validate that every DEFINE variable name is
-	 * present in the PATTERN variable list; reject names not used in PATTERN.
-	 * This is only done once at the outermost recursion level, detected by
-	 * rpDefs being non-NULL (recursive calls pass NULL).
-	 */
-	if (rpDefs)
-	{
-		foreach_node(ResTarget, rt, rpDefs)
-		{
-			bool		found = false;
-
-			foreach_node(String, varname, *varNames)
-			{
-				if (strcmp(strVal(varname), rt->name) == 0)
-				{
-					found = true;
-					break;
-				}
-			}
-			if (!found)
-				ereport(ERROR,
-						errcode(ERRCODE_SYNTAX_ERROR),
-						errmsg("DEFINE variable \"%s\" is not used in PATTERN",
-							   rt->name),
-						parser_errposition(pstate, rt->location));
-		}
-	}
 }
 
 /*
@@ -296,14 +266,16 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  *
  * First:
  *   1. Validates PATTERN variable count and collects RPR variable names
+ *   2. Rejects DEFINE variables not used in PATTERN
+ *   3. Checks for duplicate variable names in DEFINE clause
  *
  * 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
+ *   4. Transforms expression via transformExpr() and coerces it to boolean
+ *   5. Creates defineClause entry with proper resname (pattern variable name)
+ *   6. Ensures referenced Var nodes are present in the query targetlist (via
+ *      pull_var_clause)
+ *
+ * Finally 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.
@@ -316,9 +288,7 @@ static List *
 transformDefineClause(ParseState *pstate, WindowDef *windef,
 					  List **targetlist)
 {
-	List	   *restargets;
 	List	   *defineClause = NIL;
-	char	   *name;
 	List	   *patternVarNames = NIL;
 
 	/*
@@ -328,56 +298,86 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
 	Assert(windef->rpCommonSyntax->rpDefs != NULL);
 
 	/*
-	 * Validate PATTERN variable count, reject DEFINE variables not used in
-	 * PATTERN, and collect PATTERN variable names for transformColumnRef.
+	 * Validate PATTERN variable count and collect the PATTERN variable names
+	 * for transformColumnRef.
 	 */
 	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
-							   windef->rpCommonSyntax->rpDefs,
 							   &patternVarNames);
 	pstate->p_rpr_pattern_vars = patternVarNames;
 
+	/*
+	 * Reject any DEFINE variable whose name does not appear in PATTERN.  This
+	 * cross-check only needs to run once, so it lives here in the caller
+	 * rather than in the recursive validateRPRPatternVarCount().
+	 */
+	foreach_node(ResTarget, rt, windef->rpCommonSyntax->rpDefs)
+	{
+		bool		found = false;
+
+		foreach_node(String, varname, patternVarNames)
+		{
+			if (strcmp(strVal(varname), rt->name) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+		if (!found)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+						   rt->name),
+					parser_errposition(pstate, rt->location));
+	}
+
 	/*
 	 * Check for duplicate row pattern definition variables.  The standard
 	 * requires that no two row pattern definition variable names shall be
-	 * equivalent.
+	 * equivalent.  Report the error at the later (duplicate) definition.
 	 */
-	restargets = NIL;
 	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
 	{
-		TargetEntry *teDefine;
-		Node	   *expr;
-		List	   *vars;
-
-		name = restarget->name;
-
-		foreach_node(ResTarget, r, restargets)
+		foreach_node(ResTarget, prior, windef->rpCommonSyntax->rpDefs)
 		{
-			char	   *n;
-
-			n = r->name;
-
-			if (!strcmp(n, name))
+			if (prior == restarget)
+				break;
+			if (strcmp(prior->name, restarget->name) == 0)
 				ereport(ERROR,
 						errcode(ERRCODE_SYNTAX_ERROR),
 						errmsg("DEFINE variable \"%s\" appears more than once",
-							   name),
-						parser_errposition(pstate, exprLocation((Node *) r)));
+							   restarget->name),
+						parser_errposition(pstate,
+										   exprLocation((Node *) restarget)));
 		}
+	}
 
-		restargets = lappend(restargets, restarget);
+	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
+	{
+		TargetEntry *teDefine;
+		Node	   *expr;
+		List	   *vars;
 
 		/*
-		 * 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.
+		 * Transform the DEFINE expression and coerce it to boolean.  We must
+		 * NOT add the whole expression to the query targetlist, because it
+		 * may contain RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only
+		 * be evaluated inside the owning WindowAgg.  Coercing here, before
+		 * pull_var_clause, keeps pull_var_clause operating on the final
+		 * expression form and surfaces a type mismatch before the targetlist
+		 * is touched.
 		 */
 		expr = transformExpr(pstate, restarget->val,
 							 EXPR_KIND_RPR_DEFINE);
+		expr = coerce_to_boolean(pstate, expr, "DEFINE");
+
+		/* Build the defineClause entry directly from the transformed expr */
+		teDefine = makeTargetEntry((Expr *) expr,
+								   list_length(defineClause) + 1,
+								   pstrdup(restarget->name),
+								   true);
+
+		/* build transformed DEFINE clause (list of TargetEntry) */
+		defineClause = lappend(defineClause, teDefine);
 
 		/*
 		 * Pull out Var nodes from the transformed expression and ensure each
@@ -412,26 +412,9 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
 			}
 		}
 		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");
-
 	/*
 	 * Validate DEFINE expressions: nested PREV/NEXT, column references,
 	 * compound flatten, volatile callees -- all in a single walk per
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index cf158e1c043..80fabde514c 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -236,7 +236,7 @@ WINDOW w AS (
 );
 ERROR:  DEFINE variable "a" appears more than once
 LINE 7:     DEFINE A AS id > 0, A AS id < 10
-                   ^
+                                ^
 DROP TABLE rpr_dup;
 -- Boolean coercion
 CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
@@ -319,6 +319,82 @@ DROP CAST (truthyint AS boolean);
 DROP FUNCTION truthyint_to_bool(truthyint);
 DROP TYPE truthyint;
 DROP TABLE rpr_bool;
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS flag
+)
+ORDER BY id;
+ id | cnt 
+----+-----
+  1 |   1
+  2 |   0
+  3 |   1
+(3 rows)
+
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (UP+)
+    DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+ id | cnt 
+----+-----
+  1 |   0
+  2 |   3
+  3 |   0
+  4 |   0
+(4 rows)
+
+DROP TABLE rpr_nav;
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS n
+);
+ERROR:  argument of DEFINE must be type boolean, not type integer
+LINE 7:     DEFINE A AS n
+                        ^
+DROP TABLE rpr_noncoerce;
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS id > 0, B AS n
+);
+ERROR:  argument of DEFINE must be type boolean, not type integer
+LINE 7:     DEFINE A AS id > 0, B AS n
+                                     ^
+DROP TABLE rpr_noncoerce2;
 -- Complex expressions
 CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
 INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e71f0dd3680..21840aa77be 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -261,6 +261,65 @@ DROP TYPE truthyint;
 
 DROP TABLE rpr_bool;
 
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS flag
+)
+ORDER BY id;
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (UP+)
+    DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+DROP TABLE rpr_nav;
+
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS n
+);
+DROP TABLE rpr_noncoerce;
+
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS id > 0, B AS n
+);
+DROP TABLE rpr_noncoerce2;
+
 -- Complex expressions
 CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
 INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0009-define-walker-else.txt (1.8K, 11-nocfbot-0009-define-walker-else.txt)
  download | inline diff:
From f3dda49e5a24347ff23da72f5fe80640291bc34c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 20:43:01 +0900
Subject: [PATCH 09/13] Replace a bare block with an else in the RPR DEFINE
 clause walker

define_walker() ended its phase handling with a bare braced block for
the DEFINE-body case, following two if blocks that respectively return
and raise an error.  Turn it into the else branch of an if / else if /
else chain.  No behavior change.
---
 src/backend/parser/parse_rpr.c | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 116cd206e39..7c201d55164 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -515,8 +515,7 @@ define_walker(Node *node, void *context)
 			ctx->nav_count++;
 			return expression_tree_walker(node, define_walker, ctx);
 		}
-
-		if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
+		else if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
 		{
 			/*
 			 * A navigation offset must be a run-time constant, so it cannot
@@ -528,13 +527,13 @@ define_walker(Node *node, void *context)
 					errdetail("A navigation offset must be a run-time constant."),
 					parser_errposition(ctx->pstate, nav->location));
 		}
-
-		/*
-		 * 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).
-		 */
+		else
 		{
+			/*
+			 * PHASE_BODY: this is an outer nav at top level.  Walk arg first
+			 * to collect nesting / column-ref state, then validate and (for
+			 * compound forms) flatten, then walk offset(s).
+			 */
 			DefineWalkCtx saved = *ctx;
 			bool		outer_phys = (nav->kind == RPR_NAV_PREV ||
 									  nav->kind == RPR_NAV_NEXT);
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0010-rename-deparse-vars.txt (6.8K, 12-nocfbot-0010-rename-deparse-vars.txt)
  download | inline diff:
From 0e23c67f20f26c7f0e68f830ca5d8f8906a5e601 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 11:30:40 +0900
Subject: [PATCH 10/13] Rename loop index variables in row pattern deparse
 helpers

Give the index parameters and loop variables in the RPR pattern deparse
helpers more descriptive names: the construct-index parameter becomes
idx, the branch-number parameter becomes bno, and each helper's sole
loop variable becomes i.  Adjust the accompanying comments to match.

This is a cosmetic change with no effect on the deparsed output.
---
 src/backend/commands/explain.c | 68 +++++++++++++++++-----------------
 1 file changed, 34 insertions(+), 34 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 696bdb9c8b5..423cf352125 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -124,11 +124,11 @@ static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem);
 static char *deparse_rpr_pattern(RPRPattern *pattern);
 static int	deparse_rpr_seq(RPRPattern *pattern, int start, int limit,
 							StringInfo buf);
-static int	deparse_rpr_node(RPRPattern *pattern, int i, int limit,
+static int	deparse_rpr_node(RPRPattern *pattern, int idx, int limit,
 							 StringInfo buf);
 static int	rpr_match_end(RPRPattern *pattern, int beginIdx);
-static int	rpr_alt_scope_end(RPRPattern *pattern, int i);
-static int	rpr_next_branch(RPRPattern *pattern, int b, int altEnd);
+static int	rpr_alt_scope_end(RPRPattern *pattern, int idx);
+static int	rpr_next_branch(RPRPattern *pattern, int bno, int altEnd);
 static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
 							  ExplainState *es);
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -3014,8 +3014,8 @@ deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
 }
 
 /*
- * Deparse the single construct starting at index i, bounded by the inherited
- * limit.  Returns the index just past the construct.
+ * Deparse the single construct starting at index idx, bounded by the
+ * inherited limit.  Returns the index just past the construct.
  *
  * A VAR is its name plus quantifier.  A BEGIN opens a group spanning to its
  * matching END (rpr_match_end); when the group's sole child is an ALT that
@@ -3026,9 +3026,9 @@ deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
  * handed down by rpr_next_branch.
  */
 static int
-deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
+deparse_rpr_node(RPRPattern *pattern, int idx, int limit, StringInfo buf)
 {
-	RPRPatternElement *elem = &pattern->elements[i];
+	RPRPatternElement *elem = &pattern->elements[idx];
 
 	if (RPRElemIsVar(elem))
 	{
@@ -3036,27 +3036,27 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
 		appendStringInfoString(buf,
 							   quote_identifier(pattern->varNames[elem->varId]));
 		append_rpr_quantifier(buf, elem);
-		return i + 1;
+		return idx + 1;
 	}
 
 	if (RPRElemIsBegin(elem))
 	{
-		int			end = rpr_match_end(pattern, i);
+		int			end = rpr_match_end(pattern, idx);
 		bool		loneAlt;
 
-		loneAlt = (i + 1 < end &&
-				   RPRElemIsAlt(&pattern->elements[i + 1]) &&
-				   rpr_alt_scope_end(pattern, i + 1) == end);
+		loneAlt = (idx + 1 < end &&
+				   RPRElemIsAlt(&pattern->elements[idx + 1]) &&
+				   rpr_alt_scope_end(pattern, idx + 1) == end);
 
 		if (loneAlt)
 		{
 			/* The ALT child already parenthesizes the whole group body. */
-			(void) deparse_rpr_node(pattern, i + 1, end, buf);
+			(void) deparse_rpr_node(pattern, idx + 1, end, buf);
 		}
 		else
 		{
 			appendStringInfoChar(buf, '(');
-			(void) deparse_rpr_seq(pattern, i + 1, end, buf);
+			(void) deparse_rpr_seq(pattern, idx + 1, end, buf);
 			appendStringInfoChar(buf, ')');
 		}
 		append_rpr_quantifier(buf, &pattern->elements[end]);
@@ -3065,7 +3065,7 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
 
 	Assert(RPRElemIsAlt(elem));
 	{
-		int			altEnd = rpr_alt_scope_end(pattern, i);
+		int			altEnd = rpr_alt_scope_end(pattern, idx);
 		int			b;
 		bool		first = true;
 
@@ -3073,7 +3073,7 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
 			altEnd = limit;
 
 		appendStringInfoChar(buf, '(');
-		b = i + 1;
+		b = idx + 1;
 		while (b < altEnd)
 		{
 			int			nb = rpr_next_branch(pattern, b, altEnd);
@@ -3097,41 +3097,41 @@ static int
 rpr_match_end(RPRPattern *pattern, int beginIdx)
 {
 	RPRDepth	d = pattern->elements[beginIdx].depth;
-	int			j;
+	int			i;
 
-	for (j = beginIdx + 1; j < pattern->numElements; j++)
+	for (i = beginIdx + 1; i < pattern->numElements; i++)
 	{
-		RPRPatternElement *e = &pattern->elements[j];
+		RPRPatternElement *e = &pattern->elements[i];
 
 		if (RPRElemIsEnd(e) && e->depth == d)
-			return j;
+			return i;
 	}
 	pg_unreachable();			/* a BEGIN always has a matching END */
 }
 
 /*
- * Scope end of the construct at index i: the first following element whose
- * depth is no greater than i's own.  For an ALT marker this is the index just
- * past its last branch, since depth stays constant across branch boundaries.
- * FIN sits at depth 0, so a top-level ALT stops there.
+ * Scope end of the construct at index idx: the first following element whose
+ * depth is no greater than idx's own.  For an ALT marker this is the index
+ * just past its last branch, since depth stays constant across branch
+ * boundaries.  FIN sits at depth 0, so a top-level ALT stops there.
  */
 static int
-rpr_alt_scope_end(RPRPattern *pattern, int i)
+rpr_alt_scope_end(RPRPattern *pattern, int idx)
 {
-	RPRDepth	d = pattern->elements[i].depth;
-	int			k;
+	RPRDepth	d = pattern->elements[idx].depth;
+	int			i;
 
-	for (k = i + 1; k < pattern->numElements; k++)
+	for (i = idx + 1; i < pattern->numElements; i++)
 	{
-		if (pattern->elements[k].depth <= d)
-			return k;
+		if (pattern->elements[i].depth <= d)
+			return i;
 	}
 	return pattern->numElements;
 }
 
 /*
- * Boundary of the alternation branch starting at b (i.e. the start of the next
- * branch, or altEnd if b is the last branch).
+ * Boundary of the alternation branch starting at bno (i.e. the start of the
+ * next branch, or altEnd if bno is the last branch).
  *
  * The branch-start element's jump points at the next branch when this is not
  * the last branch.  jump is overloaded (a group BEGIN also uses it for its
@@ -3140,9 +3140,9 @@ rpr_alt_scope_end(RPRPattern *pattern, int i)
  * next redirected past the alternation, so it does not point at j.
  */
 static int
-rpr_next_branch(RPRPattern *pattern, int b, int altEnd)
+rpr_next_branch(RPRPattern *pattern, int bno, int altEnd)
 {
-	int			j = pattern->elements[b].jump;
+	int			j = pattern->elements[bno].jump;
 
 	if (j != RPR_ELEMIDX_INVALID && j < altEnd &&
 		pattern->elements[j - 1].next != j)
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0011-comparison-point-wording.txt (13.1K, 13-nocfbot-0011-comparison-point-wording.txt)
  download | inline diff:
From 0bc60ef4dc9cba045f6ee82d8cfcb48e2f5cf712 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 13:04:00 +0900
Subject: [PATCH 11/13] Rename absorption "judgment point" to "comparison
 point" in comments

The absorption analysis comments described the ABSORBABLE element flag
as a "judgment point".  Use "comparison point" instead, which says more
directly what happens there: where consecutive iterations are compared.
This touches comments and the executor README only, across the planner,
executor, and EXPLAIN deparse, plus the rpr_base test comments; the
"equivalence judgment" wording and all identifiers are unchanged.

No change to behavior or to query output.
---
 src/backend/commands/explain.c         |  2 +-
 src/backend/executor/README.rpr        |  4 ++--
 src/backend/executor/execRPR.c         | 29 +++++++++++++-------------
 src/backend/optimizer/plan/rpr.c       | 14 ++++++-------
 src/include/optimizer/rpr.h            |  2 +-
 src/test/regress/expected/rpr_base.out |  6 +++---
 src/test/regress/sql/rpr_base.sql      |  6 +++---
 7 files changed, 32 insertions(+), 31 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 423cf352125..b3fc324718d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2937,7 +2937,7 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
 		appendStringInfoChar(buf, '?');
 	}
 
-	/* Append absorption markers: " for judgment point, ' for branch only */
+	/* Append absorption markers: " for comparison point, ' for branch only */
 	if (RPRElemIsAbsorbable(elem))
 	{
 		Assert(elem->max == RPR_QUANTITY_INF);
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 713ad84e1d9..50d1ff87f7e 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -285,7 +285,7 @@ Element flags (1 byte, bitmask):
         absorption.
 
   0x08  RPR_ELEM_ABSORBABLE         (VAR, END)
-        Absorption judgment point.  Where to compare consecutive
+        Absorption comparison point.  Where to compare consecutive
         iterations for absorption.
           - Simple unbounded VAR (A+): set on the VAR itself
           - Unbounded GROUP ((A B)+): set on the END element only
@@ -1393,7 +1393,7 @@ XII-5. Execution Optimization Summary
     counts) combination through multiple paths. Additionally, for
     group absorption, nfa_match performs inline advance from bounded
     VARs (count >= max) within the absorbable region (ABSORBABLE_BRANCH)
-    through END chains to reach the judgment point (ABSORBABLE END).
+    through END chains to reach the comparison point (ABSORBABLE END).
     This process can also produce duplicate states reaching the same END.
     nfa_add_state_unique() blocks duplicate addition of identical states
     in both cases.
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 099b81aeb81..90e3a068f04 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -578,7 +578,7 @@ nfa_update_absorption_flags(RPRNFAContext *ctx)
 	/*
 	 * Iterate through all states to check absorption status. Uses
 	 * state->isAbsorbable which tracks if state is in absorbable region. This
-	 * is different from RPRElemIsAbsorbable(elem) which checks judgment
+	 * is different from RPRElemIsAbsorbable(elem) which checks comparison
 	 * point.
 	 */
 	for (state = ctx->states; state != NULL; state = state->next)
@@ -625,8 +625,8 @@ nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *new
 		depth = elem->depth;
 
 		/*
-		 * Only compare at absorption judgment points (RPR_ELEM_ABSORBABLE).
-		 * Judgment points are where count-dominance guarantees the newer
+		 * Only compare at absorption comparison points (RPR_ELEM_ABSORBABLE).
+		 * Comparison points are where count-dominance guarantees the newer
 		 * context's future matches are a subset of the older's.
 		 */
 		if (!RPRElemIsAbsorbable(elem))
@@ -782,7 +782,8 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
  *     previous advance when count >= min was satisfied)
  *
  * For VARs that reached max count followed by END:
- *   - Advance through the END-element chain to the absorption judgment point
+ *   - Advance through the END-element chain to the absorption
+ *     comparison point
  *   - Only deterministic exits (count >= max, max != INF) are handled
  *   - Chains through END elements while count >= max (must-exit path)
  *
@@ -800,7 +801,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 	/*
 	 * Evaluate VAR elements against current row. For VARs that reach max
 	 * count with END next, advance through the chain of END elements inline
-	 * so absorb phase can compare states at judgment points.
+	 * so absorb phase can compare states at comparison points.
 	 */
 	for (state = ctx->states; state != NULL; state = nextState)
 	{
@@ -831,7 +832,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 				/*
 				 * For VAR at max count with END next, advance through END
-				 * chain to reach the absorption judgment point.  Only
+				 * chain to reach the absorption comparison point.  Only
 				 * deterministic exits (count >= max, max finite) are handled;
 				 * unbounded VARs stay for advance phase.
 				 *
@@ -841,10 +842,10 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 				 * to the next outer END.  The loop below walks this chain.
 				 *
 				 * ABSORBABLE_BRANCH marks elements inside the absorbable
-				 * region; ABSORBABLE marks the outermost judgment point where
-				 * count-dominance is evaluated.  We chain through BRANCH
-				 * elements until reaching the ABSORBABLE point or an element
-				 * that can still loop (count < max).
+				 * region; ABSORBABLE marks the outermost comparison point
+				 * where count-dominance is evaluated.  We chain through
+				 * BRANCH elements until reaching the ABSORBABLE point or an
+				 * element that can still loop (count < max).
 				 */
 				if (RPRElemIsAbsorbableBranch(elem) &&
 					!RPRElemIsAbsorbable(elem) &&
@@ -876,7 +877,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 					/*
 					 * Chain through END elements within the absorbable region
-					 * (ABSORBABLE_BRANCH) until reaching the judgment point
+					 * (ABSORBABLE_BRANCH) until reaching the comparison point
 					 * (ABSORBABLE).  Continue only on must-exit path (count
 					 * >= max) with END next.
 					 */
@@ -892,9 +893,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 						/*
 						 * Exit this intermediate group: clear its own count
 						 * (count-clear policy).  It sits below the absorbable
-						 * judgment point, so it is excluded from the
-						 * dominance comparison; the judgment point where the
-						 * chain stops keeps its count.
+						 * comparison point, so it is excluded from the
+						 * dominance comparison; the comparison point where
+						 * the chain stops keeps its count.
 						 */
 						state->counts[endDepth] = 0;
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 597a966c7b1..ebba8e50b1d 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -19,7 +19,7 @@
  *   complexity from O(n^2) to O(n) for patterns like A+ B.
  *
  *   The absorption analysis uses two element flags:
- *   - RPR_ELEM_ABSORBABLE: marks WHERE to compare (judgment point)
+ *   - RPR_ELEM_ABSORBABLE: marks WHERE to compare (comparison point)
  *   - RPR_ELEM_ABSORBABLE_BRANCH: marks the absorbable region
  *
  *   See computeAbsorbability() and the detailed comments before
@@ -1484,7 +1484,7 @@ finalizeRPRPattern(RPRPattern *result)
  *   than Ctx2's match (1 to current). So Ctx2 can be safely eliminated.
  *
  * Two Flags:
- *   1. RPR_ELEM_ABSORBABLE - "Absorption judgment point"
+ *   1. RPR_ELEM_ABSORBABLE - "Absorption comparison point"
  *      WHERE contexts can be compared for absorption.
  *      - Simple unbounded VAR (A+): the VAR element itself
  *      - Unbounded GROUP ((A B)+): the END element only
@@ -1506,20 +1506,20 @@ finalizeRPRPattern(RPRPattern *result)
  *                -> Both at END, comparable! Ctx1 absorbs Ctx2.
  *
  *   Contexts synchronize at END every group-length rows. Therefore:
- *   - ABSORBABLE marks END as judgment point (where to compare)
+ *   - ABSORBABLE marks END as comparison point (where to compare)
  *   - ABSORBABLE_BRANCH keeps state.isAbsorbable=true through A->B->END
  *
  * Pattern Examples:
  *
  *   Pattern: A+ B
- *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- judgment point
+ *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- comparison point
  *   Element 1 (B): (none)
  *   -> Compare at A every row. When contexts move to B, absorption stops.
  *
  *   Pattern: (A B)+ C
  *   Element 0 (A): ABSORBABLE_BRANCH
  *   Element 1 (B): ABSORBABLE_BRANCH
- *   Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH  <- judgment point
+ *   Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH  <- comparison point
  *   Element 3 (C): (none)
  *   -> Compare at END every 2 rows. When contexts move to C, absorption stops.
  *
@@ -1629,7 +1629,7 @@ isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx, RPRDepth scopeDepth)
  *      All children must have min == max (recursively for nested subgroups).
  *      This is equivalent to unrolling to {1,1} VARs, e.g., (A B B)+ C.
  *      All elements within the group get ABSORBABLE_BRANCH.
- *      Only the unbounded END gets ABSORBABLE (judgment point).
+ *      Only the unbounded END gets ABSORBABLE (comparison point).
  *      Examples:
  *        (A B{2})+ C          - B{2} has min==max, step=3
  *        (A (B C){2} D)+ E    - nested {2} subgroup, step=6
@@ -1791,7 +1791,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
  * decrease property required for safe absorption.
  *
  * This function sets two flags:
- *   RPR_ELEM_ABSORBABLE: Absorption judgment point
+ *   RPR_ELEM_ABSORBABLE: Absorption comparison point
  *     - Simple unbounded VAR: the VAR itself (e.g., A in A+)
  *     - Unbounded GROUP: the END element (e.g., END in (A B)+)
  *   RPR_ELEM_ABSORBABLE_BRANCH: All elements in absorbable region
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 23763442b65..83d3cd29b07 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -53,7 +53,7 @@
  * optimizer/plan/rpr.c.
  */
 #define RPR_ELEM_ABSORBABLE_BRANCH	0x04	/* element in absorbable region */
-#define RPR_ELEM_ABSORBABLE			0x08	/* absorption judgment point */
+#define RPR_ELEM_ABSORBABLE			0x08	/* absorption comparison point */
 
 /* Accessor macros for RPRPatternElement */
 #define RPRElemIsReluctant(e)			(((e)->flags & RPR_ELEM_RELUCTANT) != 0)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 80fabde514c..b385e972e7f 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -5472,9 +5472,9 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 -- Absorption Flag Display Tests
 -- ============================================================
 -- Tests absorption marker display in EXPLAIN output
--- Markers: ' = branch element, " = judgment point
+-- Markers: ' = branch element, " = comparison point
 -- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
--- Simple VAR: A+ -> a+" (judgment point)
+-- Simple VAR: A+ -> a+" (comparison point)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -5490,7 +5490,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
--- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 21840aa77be..af498fffb66 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -3254,16 +3254,16 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 -- Absorption Flag Display Tests
 -- ============================================================
 -- Tests absorption marker display in EXPLAIN output
--- Markers: ' = branch element, " = judgment point
+-- Markers: ' = branch element, " = comparison point
 -- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
 
--- Simple VAR: A+ -> a+" (judgment point)
+-- Simple VAR: A+ -> a+" (comparison point)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
 
--- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0013-doc-nav-offset.txt (2.2K, 14-nocfbot-0013-doc-nav-offset.txt)
  download | inline diff:
From 3cccad2ecc34be6d0b1e96afdca7468e4e9ab21f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 14:17:19 +0900
Subject: [PATCH 13/13] Document eval_nav_offset_helper's NULL/negative offset
 handling

eval_nav_offset_helper pre-evaluates a navigation offset at executor init
to size the frame trim, returning 0 for a NULL or negative offset rather
than rejecting it.  The comment did not say why, leaving the purpose of the
function and of those branches unclear.

Explain that a NULL or negative offset is caught per row on the navigation
path that consumes it, which errors out before navigation produces any
result, so the trim value computed here is never used.  The branches are
reachable -- a navigation offset can be a run-time constant such as a Param
-- and are already covered by the PREV(price, $1) tests in rpr.sql, so they
need neither a new test nor an assertion.
---
 src/backend/executor/nodeWindowAgg.c | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 819ad814bf5..eb1d616b49a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3985,10 +3985,15 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
 
 /*
  * eval_nav_offset_helper
- *		Evaluate an offset expression at executor init time for trim
- *		optimization.  Returns the offset value, or 0 for NULL/negative
- *		(these will cause a runtime error during actual navigation, so the
- *		trim value is irrelevant).
+ *		Pre-evaluate a navigation offset expression at executor init time, to
+ *		bound how far navigation can reach (which sizes the frame trim).
+ *		Returns the offset value, or 0 for a NULL or negative offset.
+ *
+ * The offset is not validated here.  A NULL or negative value is caught later,
+ * per row, on the navigation path that consumes it (see EEOP_RPR_NAV_SET in
+ * execExprInterp.c), which errors out before navigation produces any result;
+ * the trim sizing computed from such an offset is therefore never used, and 0
+ * is returned as a harmless placeholder.
  */
 static int64
 eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0012-comment-doc-clarity.txt (23.5K, 15-nocfbot-0012-comment-doc-clarity.txt)
  download | inline diff:
From 9c7e5bc9dde1adafcff995370340975661890d3c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 13:43:58 +0900
Subject: [PATCH 12/13] Improve comments, documentation, and naming for row
 pattern recognition

A batch of clarity cleanups for row pattern recognition, none of which
change behavior or query output:

- Call the parser-built PATTERN structure a "parse tree" rather than an
  "AST", which is not PostgreSQL's usual term (parsenodes.h, parse_rpr.c,
  plan/rpr.c, and the executor README).
- Trim the transformDefineClause header comment, whose step list merely
  duplicated the inline comments.
- Drop a duplicated standard-citation sentence from the contain_rpr_walker
  header; transformWithClause already carries it.
- Fix a stale "ALT_START" reference to name the ALT marker, and normalize
  an "or -1" ParseLoc comment to the usual "or -1 if unknown".
- Move the EMPTY-alternative explanation in row_pattern_quantifier_opt
  into the action block, keeping the /*EMPTY*/ marker.
- Reword the Run Condition pushdown test comment to explain in SQL terms
  why count(*) is DECREASING over the required frame.
- Rename RPR_COUNT_MAX to RPR_COUNT_INF, defined from RPR_QUANTITY_INF.
  The old "MAX" name suggested a configurable repetition limit -- that is
  elem->max, a different thing -- which led to questions about erroring
  when a count reaches it.  The value is the int32 saturation ceiling, and
  a saturated count reads as "unbounded".
---
 src/backend/executor/README.rpr           | 44 +++++++++++------------
 src/backend/executor/execRPR.c            | 23 ++++++------
 src/backend/optimizer/plan/rpr.c          | 23 ++++++------
 src/backend/parser/gram.y                 |  7 ++--
 src/backend/parser/parse_cte.c            |  4 +--
 src/backend/parser/parse_rpr.c            | 19 ++--------
 src/include/nodes/parsenodes.h            | 12 +++----
 src/include/optimizer/rpr.h               |  9 ++++-
 src/test/regress/expected/rpr_explain.out | 12 ++++---
 src/test/regress/sql/rpr_explain.sql      | 12 ++++---
 10 files changed, 84 insertions(+), 81 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 50d1ff87f7e..9275e265d4b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -96,16 +96,16 @@ Chapter II  Overall Processing Pipeline
 
 RPR processing is divided into three phases:
 
-  +------------------------------------------------------------+
-  |  1. Parsing (Parser)                                       |
-  |     SQL text -> PATTERN AST + DEFINE expression tree       |
-  |                                                            |
-  |  2. Compilation (Optimizer/Planner)                        |
-  |     PATTERN AST -> optimization -> flat NFA element array  |
-  |                                                            |
-  |  3. Execution (Executor)                                   |
-  |     Row-by-row matching via NFA simulation                 |
-  +------------------------------------------------------------+
+  +--------------------------------------------------------------+
+  |  1. Parsing (Parser)                                         |
+  |     SQL text -> PATTERN parse tree + DEFINE expression tree  |
+  |                                                              |
+  |  2. Compilation (Optimizer/Planner)                          |
+  |     PATTERN parse tree -> optimization -> flat NFA elements  |
+  |                                                              |
+  |  3. Execution (Executor)                                     |
+  |     Row-by-row matching via NFA simulation                   |
+  +--------------------------------------------------------------+
 
 Each phase uses independent data structures, and the interfaces between
 phases are well-defined:
@@ -137,7 +137,7 @@ following:
 
   (3) DEFINE clause transformation (transformDefineClause)
 
-III-2. PATTERN AST (Abstract Syntax Tree)
+III-2. PATTERN parse tree
 
 The parser transforms the PATTERN clause into an RPRPatternNode tree.
 Each node has one of the following four types:
@@ -192,16 +192,16 @@ IV-1. Entry Point
 
 IV-2. The 6 Phases of buildRPRPattern()
 
-  Phase 1: AST optimization (optimizeRPRPattern)
+  Phase 1: parse tree optimization (optimizeRPRPattern)
   Phase 2: Statistics collection (scanRPRPattern)
   Phase 3: Memory allocation (makeRPRPattern)
   Phase 4: NFA element fill (fillRPRPattern)
   Phase 5: Finalization (finalizeRPRPattern)
   Phase 6: Absorbability analysis (computeAbsorbability)
 
-IV-3. Phase 1: AST Optimization
+IV-3. Phase 1: Parse Tree Optimization
 
-After copying the parser-generated AST, the following optimizations are
+After copying the parser-generated parse tree, the following optimizations are
 applied:
 
   (a) SEQ flattening: Unwrap nested SEQ nodes
@@ -236,7 +236,7 @@ applied:
 
 IV-4. Phase 4: NFA Element Array Generation
 
-Transforms the optimized AST into a flat array of RPRPatternElement.
+Transforms the optimized parse tree into a flat array of RPRPatternElement.
 This is the core data structure used for NFA simulation at runtime.
 
 RPRPatternElement struct (16 bytes):
@@ -298,7 +298,7 @@ Element flags (1 byte, bitmask):
 
 Example: PATTERN (A+ B | C)
 
-  AST: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
+  Parse tree: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
 
   Compilation result:
 
@@ -345,9 +345,9 @@ Example: PATTERN ((A B)+)
 
 IV-4a. Reluctant Flag (RPR_ELEM_RELUCTANT)
 
-The reluctant flag is set during Phase 4 (fillRPRPattern) when the AST node
-has reluctant == true. It reverses the priority of quantifier expansion at
-runtime:
+The reluctant flag is set during Phase 4 (fillRPRPattern) when the parse
+tree node has reluctant == true. It reverses the priority of quantifier
+expansion at runtime:
 
   Greedy (default):  try loop-back first, then exit  (prefer longer match)
   Reluctant:         try exit first, then loop-back   (prefer shorter match)
@@ -1363,9 +1363,9 @@ XII-5. Execution Optimization Summary
 
   -- Compile-time --
 
-  (1) AST Optimization (IV-3)
+  (1) Parse Tree Optimization (IV-3)
 
-    Simplifies the AST before converting the pattern to an NFA.
+    Simplifies the parse tree before converting the pattern to an NFA.
     Reduces the number of NFA elements through consecutive variable
     merging (A A -> A{2}), SEQ flattening, quantifier multiplication,
     and other transformations.
@@ -1468,7 +1468,7 @@ Appendix A. Key Function Index
   transformRPR                  parse_rpr.c           Parser entry point
   transformDefineClause         parse_rpr.c           DEFINE transformation
   buildRPRPattern               rpr.c                 NFA compilation main
-  optimizeRPRPattern            rpr.c                 AST optimization
+  optimizeRPRPattern            rpr.c                 parse tree optimization
   fillRPRPattern                rpr.c                 NFA element generation
   finalizeRPRPattern            rpr.c                 Finalization
   computeAbsorbability          rpr.c                 Absorption analysis
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 90e3a068f04..e5e30d79f01 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -821,8 +821,11 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 			if (matched)
 			{
-				/* Increment count */
-				if (count < RPR_COUNT_MAX)
+				/*
+				 * Increment count, saturating at RPR_COUNT_INF to avoid int32
+				 * overflow; a saturated count then compares as "unbounded".
+				 */
+				if (count < RPR_COUNT_INF)
 					count++;
 
 				/* Max constraint should not be exceeded */
@@ -857,7 +860,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 					int32		endCount = state->counts[endDepth];
 
 					/* Increment group count */
-					if (endCount < RPR_COUNT_MAX)
+					if (endCount < RPR_COUNT_INF)
 						endCount++;
 					Assert(endElem->max == RPR_QUANTITY_INF ||
 						   endCount <= endElem->max);
@@ -900,7 +903,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 						state->counts[endDepth] = 0;
 
 						/* Increment outer group count */
-						if (outerCount < RPR_COUNT_MAX)
+						if (outerCount < RPR_COUNT_INF)
 							outerCount++;
 						Assert(outerEnd->max == RPR_QUANTITY_INF ||
 							   outerCount <= outerEnd->max);
@@ -1180,7 +1183,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 
 			/* END->END: increment outer END's count */
 			if (RPRElemIsEnd(nextElem) &&
-				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
+				ffState->counts[nextElem->depth] < RPR_COUNT_INF)
 				ffState->counts[nextElem->depth]++;
 		}
 
@@ -1235,7 +1238,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			RPRElemIsAbsorbableBranch(nextElem);
 
 		/* END->END: increment outer END's count */
-		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
+		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_INF)
 			state->counts[nextElem->depth]++;
 
 		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
@@ -1262,7 +1265,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		nextElem = &elements[exitState->elemIdx];
 
 		/* END->END: increment outer END's count */
-		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
+		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_INF)
 			exitState->counts[nextElem->depth]++;
 
 		/* Prepare loop state */
@@ -1355,7 +1358,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			/* When exiting directly to an outer END, increment its count */
 			if (RPRElemIsEnd(nextElem))
 			{
-				if (cloneState->counts[nextElem->depth] < RPR_COUNT_MAX)
+				if (cloneState->counts[nextElem->depth] < RPR_COUNT_INF)
 					cloneState->counts[nextElem->depth]++;
 			}
 
@@ -1404,7 +1407,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			 */
 			if (RPRElemIsEnd(nextElem))
 			{
-				if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+				if (state->counts[nextElem->depth] < RPR_COUNT_INF)
 					state->counts[nextElem->depth]++;
 			}
 
@@ -1438,7 +1441,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* See comment above: increment outer END count for quantified VARs */
 		if (RPRElemIsEnd(nextElem))
 		{
-			if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+			if (state->counts[nextElem->depth] < RPR_COUNT_INF)
 				state->counts[nextElem->depth]++;
 		}
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index ebba8e50b1d..62292508aad 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -3,13 +3,13 @@
  * rpr.c
  *	  Row Pattern Recognition pattern compilation for planner
  *
- * This file contains functions for optimizing RPR pattern AST and
+ * This file contains functions for optimizing the RPR pattern parse tree and
  * compiling it to a flat element array for NFA execution by WindowAgg.
  *
  * Key components:
  *   1. Pattern Optimization: Simplifies patterns before compilation
  *      (e.g., flatten nested SEQ/ALT, merge consecutive vars)
- *   2. Pattern Compilation: Converts AST to flat element array for NFA
+ *   2. Pattern Compilation: Converts parse tree to flat element array for NFA
  *   3. Absorption Analysis: Computes flags for O(n^2)->O(n) optimization
  *
  * Context Absorption Optimization:
@@ -995,7 +995,7 @@ collectDefineVariables(List *defineVariableList, char **varNames)
 
 /*
  * scanRPRPatternRecursive
- *		Recursively scan pattern AST (pass 1 internal).
+ *		Recursively scan pattern parse tree (pass 1 internal).
  *
  * Collects unique variable names and counts elements while tracking depth.
  * Variables from DEFINE clause are already in varNames; this adds any
@@ -1092,7 +1092,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 
 /*
  * scanRPRPattern
- *		Scan pattern AST (pass 1 entry point).
+ *		Scan pattern parse tree (pass 1 entry point).
  *
  * Collects unique variable names (appending to those from DEFINE clause),
  * counts total elements (including FIN marker), and tracks maximum depth.
@@ -1297,7 +1297,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
  * fillRPRPatternAlt
  *		Fill an ALT pattern and its alternatives.
  *
- * Creates ALT_START marker, fills each alternative at increased depth,
+ * Creates the ALT marker, fills each alternative at increased depth,
  * sets jump pointers for backtracking, and next pointers for successful paths.
  *
  * Returns true if any branch is nullable (OR semantics: one nullable
@@ -1383,9 +1383,10 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 
 /*
  * fillRPRPattern
- *		Fill pattern elements array from AST (pass 2).
+ *		Fill pattern elements array from parse tree (pass 2).
  *
- * Recursively traverses AST and populates pre-allocated elements array.
+ * Recursively traverses the parse tree and populates pre-allocated elements
+ * array.
  * Dispatches to type-specific fill functions.
  *
  * Returns true if the pattern is nullable (can match zero rows).
@@ -1767,7 +1768,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
 	}
 	else
 	{
-		/* Should never reach END - structural invariant of pattern AST */
+		/* Should never reach END - structural invariant of pattern parse tree */
 		Assert(!RPRElemIsEnd(elem));
 
 		/* Non-ALT, non-BEGIN: check if unbounded start */
@@ -1894,13 +1895,13 @@ validate_rpr_define_volatility(List *defineClause)
 
 /*
  * buildRPRPattern
- *		Compile pattern AST to flat bytecode array.
+ *		Compile pattern parse tree to flat bytecode array.
  *
  * Compilation phases:
- *   1. Optimize AST (flatten, merge, deduplicate)
+ *   1. Optimize parse tree (flatten, merge, deduplicate)
  *   2. Scan: collect variables, count elements (pass 1)
  *   3. Allocate result structure
- *   4. Fill elements from AST (pass 2)
+ *   4. Fill elements from parse tree (pass 2)
  *   5. Finalize pattern structure
  *   6. Compute context absorption eligibility
  *
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4ae951aaeba..6eb01ea7f0b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17734,9 +17734,12 @@ row_pattern_primary:
 		;
 
 row_pattern_quantifier_opt:
-			/* EMPTY - no quantifier means exactly once; @$ is unused since
-			 * min=max=1 never produces an error */
+			/*EMPTY*/
 				{
+					/*
+					 * no quantifier means exactly once; @$ is unused since
+					 * min=max=1 never produces an error
+					 */
 					$$ = (Node *) makeRPRQuantifier(1, 1, false, @$);
 				}
 			| '*'
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index 3e493beba0b..c35feeae6fd 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -1303,9 +1303,7 @@ checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
 /*
  * contain_rpr_walker
  *	  Returns true if the raw parse tree contains any <row pattern common
- *	  syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.  Used
- *	  by transformWithClause() to enforce ISO/IEC 9075-2:2016 7.17 SR 3)f)
- *	  on WITH RECURSIVE elements.
+ *	  syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.
  */
 static bool
 contain_rpr_walker(Node *node, void *context)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 7c201d55164..ed12190cb06 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -8,7 +8,7 @@
  *   - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
  *   - Validates PATTERN variable count (max RPR_VARID_MAX + 1)
  *   - Transforms DEFINE clause
- *   - Stores the PATTERN AST and the SKIP TO/INITIAL flags
+ *   - Stores the PATTERN parse tree and the SKIP TO/INITIAL flags
  *
  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -67,7 +67,7 @@ static bool define_walker(Node *node, void *context);
  *   - Set AFTER MATCH SKIP TO flag
  *   - Set SEEK/INITIAL flag
  *   - Transforms DEFINE clause into TargetEntry list
- *   - Stores PATTERN AST for deparsing (optimization happens in planner)
+ *   - Stores PATTERN parse tree for deparsing (optimization happens in planner)
  *
  * Returns early if windef has no rpCommonSyntax (non-RPR window).
  */
@@ -180,7 +180,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	/* Transform DEFINE clause into list of TargetEntry's */
 	wc->defineClause = transformDefineClause(pstate, windef, targetlist);
 
-	/* Store PATTERN AST for deparsing */
+	/* Store PATTERN parse tree for deparsing */
 	wc->rpPattern = windef->rpCommonSyntax->rpPattern;
 }
 
@@ -264,19 +264,6 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  * transformDefineClause
  *		Process DEFINE clause and transform ResTarget into list of TargetEntry.
  *
- * First:
- *   1. Validates PATTERN variable count and collects RPR variable names
- *   2. Rejects DEFINE variables not used in PATTERN
- *   3. Checks for duplicate variable names in DEFINE clause
- *
- * Then for each DEFINE variable:
- *   4. Transforms expression via transformExpr() and coerces it to boolean
- *   5. Creates defineClause entry with proper resname (pattern variable name)
- *   6. Ensures referenced Var nodes are present in the query targetlist (via
- *      pull_var_clause)
- *
- * Finally 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.
  *
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 947e668020e..f28215b8e83 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -621,7 +621,7 @@ typedef enum RPRPatternNodeType
 } RPRPatternNodeType;
 
 /*
- * RPRPatternNode - Row Pattern Recognition pattern AST node
+ * RPRPatternNode - Row Pattern Recognition pattern parse tree node
  */
 typedef struct RPRPatternNode
 {
@@ -630,7 +630,7 @@ typedef struct RPRPatternNode
 	int32		min;			/* minimum repetitions (0 for *, ?) */
 	int32		max;			/* maximum repetitions (PG_INT32_MAX for *, +) */
 	bool		reluctant;		/* true for reluctant (non-greedy) */
-	ParseLoc	location;		/* token location, or -1 */
+	ParseLoc	location;		/* token location, or -1 if unknown */
 	char	   *varName;		/* VAR: variable name */
 	List	   *children;		/* SEQ, ALT, GROUP: child nodes */
 
@@ -652,10 +652,10 @@ typedef struct RPCommonSyntax
 	RPSkipTo	rpSkipTo;		/* Row Pattern AFTER MATCH SKIP type */
 	bool		initial;		/* true if <row pattern initial or seek> is
 								 * initial */
-	RPRPatternNode *rpPattern;	/* PATTERN clause AST */
+	RPRPatternNode *rpPattern;	/* PATTERN parse tree */
 	List	   *rpDefs;			/* row pattern definitions clause (list of
 								 * ResTarget) */
-	ParseLoc	location;		/* PATTERN keyword location, or -1 */
+	ParseLoc	location;		/* PATTERN keyword location, or -1 if unknown */
 } RPCommonSyntax;
 
 /*
@@ -1727,7 +1727,7 @@ typedef struct GroupingSet
  * options are never copied, per spec.
  * "defineClause" is Row Pattern Recognition DEFINE clause (list of
  * TargetEntry). TargetEntry.resname represents row pattern definition
- * variable name. "rpPattern" represents PATTERN clause as an AST tree
+ * variable name. "rpPattern" represents the PATTERN clause as a parse tree
  * (RPRPatternNode).
  *
  */
@@ -1763,7 +1763,7 @@ typedef struct WindowClause
 								 * initial */
 	/* Row Pattern DEFINE clause (list of TargetEntry) */
 	List	   *defineClause;
-	/* Row Pattern PATTERN clause AST */
+	/* Row Pattern PATTERN parse tree */
 	RPRPatternNode *rpPattern;
 } WindowClause;
 
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 83d3cd29b07..f5462ab2a7c 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -27,8 +27,15 @@
  * before release.
  */
 #define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
+
+/*
+ * RPR_COUNT_INF is the value a runtime repetition count saturates at to avoid
+ * int32 overflow (see the count++ guard in nfa_match).  It is defined as
+ * RPR_QUANTITY_INF so that a saturated count compares as "unbounded", just
+ * like an unbounded quantifier's max.
+ */
 #define RPR_QUANTITY_INF	PG_INT32_MAX	/* unbounded quantifier */
-#define RPR_COUNT_MAX		PG_INT32_MAX	/* max runtime count (NFA state) */
+#define RPR_COUNT_INF		RPR_QUANTITY_INF
 #define RPR_ELEMIDX_MAX		PG_INT16_MAX	/* max pattern elements */
 #define RPR_ELEMIDX_INVALID	((RPRElemIdx) -1)	/* invalid index */
 #define RPR_DEPTH_MAX		(PG_UINT8_MAX - 1)	/* max pattern nesting depth:
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index cc86d0aae30..8672b4c3055 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -5598,13 +5598,15 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
 -- find_window_run_conditions() pushes WHERE filters on monotonic window
 -- functions into WindowAgg as Run Conditions for early termination.
 -- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
--- FOLLOWING), the monotonic direction determines which operators trigger
--- Run Condition pushdown:
+-- FOLLOWING), the monotonic direction of the window function determines
+-- which comparison operators allow pushdown:
 --   INCREASING (<=): row_number, rank, dense_rank, percent_rank,
 --                    cume_dist, ntile
---   DECREASING (>):  count(*) (via int8inc, END_UNBOUNDED_FOLLOWING)
--- RPR window function results are match-dependent, not monotonic.
--- Test with count(*) > 0 as representative case.
+--   DECREASING (>):  count(*).  As the current row advances, the frame
+--                    (which ends at UNBOUNDED FOLLOWING) shrinks, so the
+--                    count decreases.
+-- RPR window function results are match-dependent, not monotonic, so this
+-- pushdown does not apply.  Test with count(*) > 0 as a representative case.
 --
 -- Without RPR: count(*) > 0 is pushed down as Run Condition
 EXPLAIN (COSTS OFF)
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 297ca78d54b..115402e304d 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -3174,13 +3174,15 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
 -- find_window_run_conditions() pushes WHERE filters on monotonic window
 -- functions into WindowAgg as Run Conditions for early termination.
 -- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
--- FOLLOWING), the monotonic direction determines which operators trigger
--- Run Condition pushdown:
+-- FOLLOWING), the monotonic direction of the window function determines
+-- which comparison operators allow pushdown:
 --   INCREASING (<=): row_number, rank, dense_rank, percent_rank,
 --                    cume_dist, ntile
---   DECREASING (>):  count(*) (via int8inc, END_UNBOUNDED_FOLLOWING)
--- RPR window function results are match-dependent, not monotonic.
--- Test with count(*) > 0 as representative case.
+--   DECREASING (>):  count(*).  As the current row advances, the frame
+--                    (which ends at UNBOUNDED FOLLOWING) shrinks, so the
+--                    count decreases.
+-- RPR window function results are match-dependent, not monotonic, so this
+-- pushdown does not apply.  Test with count(*) > 0 as a representative case.
 --
 
 -- Without RPR: count(*) > 0 is pushed down as Run Condition
-- 
2.50.1 (Apple Git-155)



view thread (141+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
  Subject: Re: Row pattern recognition
  In-Reply-To: <CAAAe_zCsaf=WedELLjqLe3BV_8dWiO1DPDGA9sXj4qhe+=-XXw@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox