Subject: Re: Row pattern recognition

From: Henson Choi <[email-masked]>
Date: Sun, 1 Mar 2026 01:59:56 +0900

Hi hackers,

I'd like to propose an extension to the context absorption optimization
for Row Pattern Recognition in the window clause: PREFIX pattern
absorption. This refines the "anchored pattern absorption" concept I
posted earlier [1], renaming it to "PREFIX pattern absorption" per
Ishii-san's feedback [2], and formalizing the absorption conditions
with count-dominance analysis.

It builds on the existing absorption framework to handle patterns where
a fixed-length prefix precedes the unbounded greedy repetition, using a
"shadow path" design.

[1]
https://www.postgresql.org/message-id/CAAAe_zAEg7sVM=[email-masked]
[2]
https://www.postgresql.org/message-id/[email-masked]

1. Problem Definition
=====================

1.1 Current Absorption Scope

Context absorption currently applies to patterns with an unbounded-start
structure -- where an unbounded greedy repetition appears at the very
beginning of the pattern.

Example: A+ B -- the first element A+ is an unbounded repetition, so an
older context always has a repeat count greater than or equal to any
newer context, making immediate absorption possible.

1.2 The PREFIX Pattern Problem

For patterns like START SECOND A+ B+, where a finite-length prefix
(PREFIX) precedes the unbounded repetition (A+), absorption does not
currently work.

Why:
- A new context starts at the START element, so it is not at the same
  pattern position as an older context in the A+ region.
- The allStatesAbsorbable condition is not satisfied, so absorption
  never fires.
- Contexts accumulate while traversing the PREFIX, causing O(n^2)
  regression.

2. Relationship to the Existing Framework
==========================================

2.1 Position in the Three-Phase Execution Model

The Match -> Absorb -> Advance model is preserved as-is. PREFIX
absorption is an extension of the Absorb phase's eligibility criteria,
not a new phase.

2.2 Prerequisites

The same four prerequisites as existing absorption apply:

1. Greedy quantifier -- A+ must be greedy.
2. SKIP TO PAST LAST ROW -- prior matches suppress subsequent start
   points.
3. UNBOUNDED FOLLOWING -- all contexts see the same future rows.
4. Match-start-independent DEFINE predicates -- including predicates of
   PREFIX elements.

2.3 Extension of the Dominance Order

In existing absorption, the dominance condition is:

    C_old and C_new are in the absorbable region of the same element,
    and C_old.counts >= C_new.counts.

In PREFIX patterns this condition cannot be directly satisfied: C_new is
in the PREFIX region while C_old is in the BODY region. We therefore
need a mechanism to track what C_new's state *would be* if it had
already reached the BODY region.

3. Design: Shadow Path
=======================

3.1 Core Idea

Each context logically executes two parallel paths:

- Original path: executes the full pattern in order (PREFIX -> BODY).
  This is the path that produces the actual match result and evaluates
  PREFIX predicates.

- Shadow path: skips the PREFIX and starts directly in the BODY region
  (A+).
  - Used solely for absorption eligibility decisions.
  - Does not produce match results.
  - Actually evaluates the BODY element's DEFINE predicates to track
    counts accurately.
  - The shadow is placed at the BODY start state upon creation and
    immediately advances, so its initial count is 1.
  - Not created if there is no preceding context (nothing to be
    absorbed into).
  - Discarded when the preceding context dies.
  - Discarded when the shadow leaves the absorbable region (BODY->TAIL
    transition or match failure).

3.2 Absorption Conditions

For C_old to absorb C_new, all of the following must hold:

1. C_old is past the PREFIX and in the absorbable region (BODY).
   - hasAbsorbableState = true (existing flag)

2. C_new was created at or after the row where C_old entered the
   absorbable region.
   - "Same row" means: in the Match phase C_old enters BODY and C_new
     is created; the subsequent Absorb phase of that same row can then
     perform the absorption check.

3. C_new's shadow path is alive in the absorbable region.
   - allStatesAbsorbable = true (evaluated on the shadow)

4. C_old.counts[depth] >= C_new.shadow.counts[depth]
   - Same count-dominance condition as existing absorption (equality
     counts as dominance).
   - The shadow's count increases as it processes rows, so the
     comparison uses actual counts at absorption time.

Condition 2 is the key insight. Only contexts created after C_old has
passed through the PREFIX into the absorbable region are eligible for
absorption. Contexts created while C_old is still in the PREFIX cannot
be absorbed.

3.3 Common Fate

Soundness argument for absorption:

    C_old's original path and C_new's shadow path are at the same BODY
    element (A+). By match-start independence, the DEFINE predicates of
    both paths produce identical results on the same row. If C_new's
    shadow can match at some future row r, then C_old can also complete
    a match at row r (it has matched at least as many times, sees the
    same future rows, and DEFINE evaluations are identical). Under SKIP
    TO PAST LAST ROW, C_old's match includes C_new's start point, so
    removing C_new does not lose any reported match.

3.4 Complexity

With PREFIX absorption, the maximum number of simultaneously active
contexts is bounded by PREFIX_length + 1. Every C_new created after
C_old enters BODY is absorbed immediately, so the only contexts that can
coexist are those still traversing the PREFIX plus the single C_old in
BODY. Since PREFIX length is a per-pattern constant, overall execution
complexity is O(n).

3.5 Removal of Non-Absorbable Contexts

Contexts that do not satisfy condition 2 -- i.e., those created before
C_old entered BODY -- cannot be removed by PREFIX absorption.

These contexts are removed by the existing SKIP TO PAST LAST ROW
mechanism, which is separate from PREFIX absorption. When C_old
successfully matches, all subsequent contexts whose start points fall
within C_old's match range are removed as overlaps. Since contexts
created during the PREFIX region have start points within C_old's match
range, they are automatically removed upon C_old's match completion.

The number of such contexts is at most PREFIX_length, which does not
affect the O(n) complexity (see Section 3.4).

4. Compile-Time Analysis
=========================

4.1 Pattern Structure Recognition

At compile time, traverse the pattern elements from the start until the
first unbounded greedy repetition is found. All preceding fixed-length
elements form the PREFIX; the unbounded element is the BODY; everything
after it is the TAIL. If the first element is already unbounded, the
pattern has no PREFIX and existing absorption applies directly.

    Pattern: E1 E2 ... Ek  A+          B+ ...
             \__________/  \__/         \___/
              PREFIX        BODY         TAIL
                           (absorbable)  (non-absorbable)

5. Worked Example
==================

Pattern: START SECOND A+ B+
PREFIX: START, SECOND (length 2)
BODY: A+ (absorbable)
TAIL: B+ (non-absorbable)

Data: r0-r3 satisfy A only, r4-r5 satisfy B only, r6 satisfies neither
A nor B.

Row  C1(orig)   C2(orig)   C2(shadow)  C3(orig)   C3(shadow)  Active
r0   START       -           -           -           -          1
r1   SECOND      START       A(cnt=1)    -           -          2
r2   A(cnt=1)    SECOND      A(cnt=2)    START       A(cnt=1)   3
     ^ C1 reaches BODY
     -> C3: shadow A(cnt=1), created at C1's BODY entry
        -> absorbed (1 >= 1)
     -> C2: created before C1's BODY entry
        -> not absorbable                                       2
r3   A(cnt=2)    A(cnt=1)    A(cnt=3)    -           -          2
r4   B(cnt=1)    B(cnt=1)    (discard)   -           -          2
     ^ TAIL      ^ TAIL      ^ shadow A+->B+ -> TAIL -> discard
r5   B(cnt=2)    B(cnt=2)    -           -           -          2
r6   neither A nor B -> B+ ends -> C1 match (r0-r5)             0
     C2 removed by SKIP TO PAST LAST ROW overlap
     (C2 start r1 falls within C1 match range r0-r5)

Max concurrent contexts: 3 = PREFIX_length(2) + 1

Key observations:
- C3 is immediately removed at r2 by PREFIX absorption (shadow-path-
  based).
- C2's shadow is discarded at r4 when A+->B+ transition moves it into
  TAIL.
- C2 is not absorbable (condition 2 not met), survives until r6, then
  removed by SKIP TO PAST LAST ROW overlap when C1 matches
  (Section 3.5).

This is a design proposal at this stage. Thoughts and feedback welcome.

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Sun, 1 Mar 2026 20:02:38 +0900

Hi Tatsuo,

Attached are two more incremental patches (nocfbot-0011, nocfbot-0012)
on top of v43, continuing the nocfbot-0001..0010 series.

nocfbot-0011: Add RPR DEFINE expression cost to WindowAgg cost estimation

  Revised version of your cost estimation patch [1].  Fixes the
  `(char *)` cast and moves DEFINE cost outside the windowFuncs loop.

nocfbot-0012: Reject qualified column references in RPR DEFINE clause

  Revised version of your qualified column reference patch [2].
  Exposes `patternVarNames` via `ParseState.p_rpr_pattern_vars` to
  distinguish pattern variable qualifiers ("not supported") from
  FROM-clause range variable qualifiers ("not allowed").

  Variables appearing only in DEFINE (not in PATTERN) are also
  collected into `p_rpr_pattern_vars` so they get the "not supported"
  message, but the RPR_VARID_MAX count check still applies only to
  PATTERN variables.

[1]
https://www.postgresql.org/message-id/[email-masked]
[2]
https://www.postgresql.org/message-id/[email-masked]

Attachment:
  nocfbot-0011-define-cost.txt
  nocfbot-0012-qualified-refs.txt

Best regards,
Henson
From 613d2b9cc310f9e55c71ef16555b9c42401c46b9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 1 Mar 2026 19:09:27 +0900
Subject: [PATCH] Add RPR DEFINE expression cost to WindowAgg cost estimation

---
 src/backend/optimizer/path/costsize.c | 29 +++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 89ca4e08bf1..a10fb802d0e 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -103,6 +103,7 @@
 #include "optimizer/placeholder.h"
 #include "optimizer/plancat.h"
 #include "optimizer/restrictinfo.h"
+#include "optimizer/rpr.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
 #include "utils/selfuncs.h"
@@ -3227,7 +3228,35 @@ cost_windowagg(Path *path, PlannerInfo *root,
 	 * many rows the window function will fetch, it's hard to do better.  In
 	 * any case, it's a good estimate for all the built-in window functions,
 	 * 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.
 	 */
+	if (winclause->rpPattern)
+	{
+		List	   *pattern_vars;
+		ListCell   *lc2;
+		QualCost	defcosts;
+
+		pattern_vars = collectPatternVariables(winclause->rpPattern);
+
+		foreach(lc2, pattern_vars)
+		{
+			char	   *ptname = strVal(lfirst(lc2));
+
+			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;
+				}
+			}
+		}
+		list_free_deep(pattern_vars);
+	}
+
 	foreach(lc, windowFuncs)
 	{
 		WindowFunc *wfunc = lfirst_node(WindowFunc, lc);
-- 
2.50.1 (Apple Git-155)

From f508104267e0c399fa48373e80ac51c8f4996103 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 1 Mar 2026 19:24:53 +0900
Subject: [PATCH] Reject qualified column references in RPR DEFINE clause

---
 src/backend/parser/parse_expr.c        | 36 ++++++++++++++
 src/backend/parser/parse_rpr.c         | 69 ++++++++++++++++++++------
 src/include/parser/parse_node.h        |  1 +
 src/test/regress/expected/rpr_base.out | 60 +++++++++++++++++-----
 src/test/regress/sql/rpr_base.sql      | 52 ++++++++++++++-----
 5 files changed, 178 insertions(+), 40 deletions(-)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 219681d6e88..99b7e4763aa 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -613,6 +613,42 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 			return node;
 	}
 
+	/*
+	 * Qualified column references in DEFINE are not supported.  This covers
+	 * both FROM-clause range variables (prohibited by §6.5) and pattern
+	 * variable qualified names (e.g. UP.price), which are valid per §4.16
+	 * but not yet implemented.
+	 */
+	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+		list_length(cref->fields) != 1)
+	{
+		char	   *qualifier = strVal(linitial(cref->fields));
+		ListCell   *lc;
+		bool		is_pattern_var = false;
+
+		foreach(lc, pstate->p_rpr_pattern_vars)
+		{
+			if (strcmp(strVal(lfirst(lc)), qualifier) == 0)
+			{
+				is_pattern_var = true;
+				break;
+			}
+		}
+
+		if (is_pattern_var)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("pattern variable qualified column reference \"%s\" is not supported in DEFINE clause",
+							NameListToString(cref->fields)),
+					 parser_errposition(pstate, cref->location)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("range variable qualified column reference \"%s\" is not allowed in DEFINE clause",
+							NameListToString(cref->fields)),
+					 parser_errposition(pstate, cref->location)));
+	}
+
 	/*----------
 	 * The allowed syntaxes are:
 	 *
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 027a3d2500a..dff91e439d2 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -37,7 +37,7 @@
 
 /* Forward declarations */
 static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
-									   List **varNames);
+									   List *rpDefs, List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
 								   WindowDef *windef, List **targetlist);
 
@@ -160,11 +160,15 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  * Recursively traverses the pattern tree, collecting unique variable names.
  * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
  *
+ * If rpDefs is non-NULL, DEFINE variable names are also collected into
+ * varNames so that transformColumnRef can distinguish pattern variable
+ * qualifiers from FROM-clause range variables.
+ *
  * varNames is both input and output: existing names are preserved, new ones added.
  */
 static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
-						   List **varNames)
+						   List *rpDefs, List **varNames)
 {
 	ListCell   *lc;
 
@@ -209,18 +213,48 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			foreach(lc, node->children)
 			{
 				validateRPRPatternVarCount(pstate, (RPRPatternNode *) lfirst(lc),
-										   varNames);
+										   NULL, varNames);
 			}
 			break;
 	}
+
+	/*
+	 * After the top-level call, also collect DEFINE variable names that are
+	 * not already in the list.  This is only done once at the outermost
+	 * recursion level, detected by rpDefs being non-NULL (recursive calls
+	 * pass NULL).
+	 */
+	if (rpDefs)
+	{
+		foreach(lc, rpDefs)
+		{
+			ResTarget  *rt = (ResTarget *) lfirst(lc);
+			ListCell   *lc2;
+			bool		found = false;
+
+			foreach(lc2, *varNames)
+			{
+				if (strcmp(strVal(lfirst(lc2)), rt->name) == 0)
+				{
+					found = true;
+					break;
+				}
+			}
+			if (!found)
+				*varNames = lappend(*varNames,
+									makeString(pstrdup(rt->name)));
+		}
+	}
 }
 
 /*
  * transformDefineClause
  *		Process DEFINE clause and transform ResTarget into list of TargetEntry.
  *
- * For each DEFINE variable:
- *   1. Validates PATTERN variable count via validateRPRPatternVarCount
+ * First:
+ *   1. Validates PATTERN variable count and collects RPR variable names
+ *
+ * Then for each DEFINE variable:
  *   2. Checks for duplicate variable names in DEFINE clause
  *   3. Transforms expressions and adds to targetlist via findTargetlistEntrySQL99
  *   4. Creates defineClause entry with proper resname (pattern variable name)
@@ -230,9 +264,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
  * Variables in DEFINE but not in PATTERN are filtered out by the planner.
  *
- * XXX we only support column reference in row pattern definition search
- * condition, e.g. "price". <row pattern definition variable name>.<column
- * reference> is not supported, e.g. "A.price".
+ * XXX Pattern variable qualified column references in DEFINE (e.g.
+ * "A.price") are not yet supported.  Currently rejected by
+ * transformColumnRef in parse_expr.c via the p_rpr_pattern_vars check.
  */
 static List *
 transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
@@ -253,9 +287,14 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	 */
 	Assert(windef->rpCommonSyntax->rpDefs != NULL);
 
-	/* Validate PATTERN variable count (max RPR_VARID_MAX) */
+	/*
+	 * Validate PATTERN variable count and collect all RPR variable names
+	 * (PATTERN + DEFINE) for use in transformColumnRef.
+	 */
 	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
+							   windef->rpCommonSyntax->rpDefs,
 							   &patternVarNames);
+	pstate->p_rpr_pattern_vars = patternVarNames;
 
 	/*
 	 * Check for duplicate row pattern definition variables.  The standard
@@ -290,13 +329,12 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		restargets = lappend(restargets, restarget);
 
 		/*
-		 * Add DEFINE expression (Restarget->val) to the targetlist as a
-		 * TargetEntry if it does not exist yet. Planner will add the column
-		 * ref var node to the outer plan's target list later on. This makes
-		 * DEFINE expression could access the outer tuple while evaluating
-		 * PATTERN.
+		 * Transform the DEFINE expression (restarget->val) and add it to the
+		 * targetlist as a TargetEntry if not already present, so the planner
+		 * can propagate the referenced columns to the outer plan's
+		 * targetlist.
 		 *
-		 * Note: findTargetlistEntrySQL99 does Expr transformation and clobber
+		 * Note: findTargetlistEntrySQL99 transforms and clobbers
 		 * restarget->val.
 		 */
 
@@ -347,6 +385,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		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
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 4dc7e5738ae..f55d4c14b0a 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -208,6 +208,7 @@ struct ParseState
 	ParseNamespaceItem *p_grouping_nsitem;	/* NSItem for grouping, or NULL */
 	List	   *p_windowdefs;	/* raw representations of window clauses */
 	ParseExprKind p_expr_kind;	/* what kind of expression we're parsing */
+	List	   *p_rpr_pattern_vars; /* RPR variable names for DEFINE clause */
 	int			p_next_resno;	/* next targetlist resno to assign */
 	List	   *p_multiassign_exprs;	/* junk tlist entries for multiassign */
 	List	   *p_locking_clause;	/* raw FOR UPDATE/FOR SHARE info */
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 43eea32130f..ab878443379 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2682,9 +2682,7 @@ LINE 6:     DEFINE A AS val > 0
             ^
 -- Expected: Syntax error
 -- Qualified column references (NOT SUPPORTED)
--- Pattern variables in DEFINE clause cannot use qualified references (A.price)
--- This gives a confusing error about missing FROM-clause entry
--- Qualified reference in DEFINE clause
+-- Pattern variable qualified name: not supported (valid per §4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2693,10 +2691,45 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
-ERROR:  missing FROM-clause entry for table "a"
+ERROR:  pattern variable qualified column reference "a.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS A.val > 0
                         ^
--- Expected: ERROR: missing FROM-clause entry for table "a"
+-- PATTERN-only variable qualified name: not supported even without DEFINE entry
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ B+)
+    DEFINE A AS B.val > 0
+);
+ERROR:  pattern variable qualified column reference "b.val" is not supported in DEFINE clause
+LINE 7:     DEFINE A AS B.val > 0
+                        ^
+-- DEFINE-only variable qualified name: still a pattern variable, not a range variable
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0, B AS B.val > 0
+);
+ERROR:  pattern variable qualified column reference "b.val" is not supported in DEFINE clause
+LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
+                                      ^
+-- FROM-clause range variable qualified name: not allowed (prohibited by §6.5)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS rpr_err.val > 0
+);
+ERROR:  range variable qualified column reference "rpr_err.val" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS rpr_err.val > 0
+                        ^
 -- Semantic errors
 -- Undefined column in DEFINE
 SELECT COUNT(*) OVER w
@@ -4674,8 +4707,8 @@ WINDOW w AS (
     ORDER BY t1.id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     PATTERN (A+ B)
-    DEFINE A AS t1.val1 > 20,
-           B AS t2.val2 > 200
+    DEFINE A AS val1 > 20,
+           B AS val2 > 200
 )
 ORDER BY t1.id;
  id | val1 | val2 | cnt 
@@ -4709,17 +4742,18 @@ ORDER BY t1.id, t2.id;
 (4 rows)
 
 -- Self-Join with RPR
-SELECT a.id, a.val1, b.val1 as val1_next,
+SELECT id, val1, val1_next,
        COUNT(*) OVER w as cnt
-FROM rpr_join1 a
-INNER JOIN rpr_join1 b ON a.id + 1 = b.id
+FROM (SELECT a.id, a.val1, b.val1 as val1_next
+      FROM rpr_join1 a
+      INNER JOIN rpr_join1 b ON a.id + 1 = b.id) sub
 WINDOW w AS (
-    ORDER BY a.id
+    ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     PATTERN (X+)
-    DEFINE X AS a.val1 < b.val1
+    DEFINE X AS val1 < val1_next
 )
-ORDER BY a.id;
+ORDER BY id;
  id | val1 | val1_next | cnt 
 ----+------+-----------+-----
   1 |   10 |        20 |   4
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 992c081205a..97b62a73a0e 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1891,10 +1891,8 @@ WINDOW w AS (
 -- Expected: Syntax error
 
 -- Qualified column references (NOT SUPPORTED)
--- Pattern variables in DEFINE clause cannot use qualified references (A.price)
--- This gives a confusing error about missing FROM-clause entry
 
--- Qualified reference in DEFINE clause
+-- Pattern variable qualified name: not supported (valid per §4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -1903,7 +1901,36 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
--- Expected: ERROR: missing FROM-clause entry for table "a"
+
+-- PATTERN-only variable qualified name: not supported even without DEFINE entry
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ B+)
+    DEFINE A AS B.val > 0
+);
+
+-- DEFINE-only variable qualified name: still a pattern variable, not a range variable
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0, B AS B.val > 0
+);
+
+-- FROM-clause range variable qualified name: not allowed (prohibited by §6.5)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS rpr_err.val > 0
+);
 
 -- Semantic errors
 
@@ -2983,8 +3010,8 @@ WINDOW w AS (
     ORDER BY t1.id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     PATTERN (A+ B)
-    DEFINE A AS t1.val1 > 20,
-           B AS t2.val2 > 200
+    DEFINE A AS val1 > 20,
+           B AS val2 > 200
 )
 ORDER BY t1.id;
 
@@ -3005,17 +3032,18 @@ ORDER BY t1.id, t2.id;
 
 -- Self-Join with RPR
 
-SELECT a.id, a.val1, b.val1 as val1_next,
+SELECT id, val1, val1_next,
        COUNT(*) OVER w as cnt
-FROM rpr_join1 a
-INNER JOIN rpr_join1 b ON a.id + 1 = b.id
+FROM (SELECT a.id, a.val1, b.val1 as val1_next
+      FROM rpr_join1 a
+      INNER JOIN rpr_join1 b ON a.id + 1 = b.id) sub
 WINDOW w AS (
-    ORDER BY a.id
+    ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     PATTERN (X+)
-    DEFINE X AS a.val1 < b.val1
+    DEFINE X AS val1 < val1_next
 )
-ORDER BY a.id;
+ORDER BY id;
 
 DROP TABLE rpr_join1, rpr_join2;
 
-- 
2.50.1 (Apple Git-155)

---

From: Henson Choi <[email-masked]>
Date: Mon, 2 Mar 2026 10:32:46 +0900

Hi Tatsuo,

> Thanks. I will try them out. Once they look good, shall I release v44
> patch sets? Or do you have a plan to add more patches?

The NFA executor code looks stable to me at this point. I do have some
additional optimization ideas around absorption, but I think it's better
new features should be added more carefully, or deferred until after the
commit.

Please go ahead and release v44. Having a fresh patch set will also
make it easier to proceed with the next round of test modifications
and improvements.

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 20:32:51 +0900

Hi,Tatsuo

While reviewing the RPR test cases, I noticed that a subquery filter
on RPR window function results silently returns wrong results.

For example, given this query:

  SELECT * FROM (
      SELECT id, val, COUNT(*) OVER w as cnt
      FROM rpr_copy
      WINDOW w AS (
          ORDER BY id
          ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          PATTERN (A B?)
          DEFINE A AS val > 10, B AS val > 20
      )
  ) sub
  WHERE cnt > 0
  ORDER BY id;

This should return 2 rows, but returns 0 rows instead.

The EXPLAIN plan shows that "cnt > 0" is pushed down into the
WindowAgg node as a Run Condition:

  WindowAgg
    Run Condition: (count(*) OVER w > 0)    <-- pushed down
    ->  Sort
          ->  Seq Scan on rpr_copy

I will investigate the cause and work on a fix.

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Wed, 4 Mar 2026 16:29:35 +0900

Hi Tatsuo,

CFBot complains v44-0007 patch corrupted.
> https://cfbot.cputube.org/patch_4460.log

I downloaded v44-0007 from the mailing list archive and applied it
successfully in my local environment. Everything works fine here.

> So it seems the file was truncated somwhere between me and PostgreSQL
> ML archiving.
>

It seems CFBot does not retry once it fails, so the initial truncation
error has become permanent.

> Can you check the size of the file attached to your mail?

I didn't check the raw attachment size, but the patch applied cleanly
without any issues.

To work around this, how about splitting the test patch (0007) into
separate patches for .sql and .out files? This would reduce the
individual file sizes and help avoid truncation during archiving.

Best regards,
Henson

---

From: 장성준 <[email-masked]>
Date: Thu, 5 Mar 2026 13:16:21 +0900

Hi hackers

I converted PostgreSQL RPR regression test queries to Oracle
MATCH_RECOGNIZE syntax and executed them on both systems to perform
cross-validation.

The tests were based on the following PostgreSQL regression test files:

  rpr_base.sql
  rpr_nfa.sql

PostgreSQL bug: zero-min reluctant quantifier

During cross-validation a PostgreSQL bug was discovered involving
reluctant quantifiers whose minimum repetition is 0.

Example pattern:

  PATTERN (A*?)
  DEFINE A AS val > 0

Result comparison:

pattern   PostgreSQL (cnt)   Oracle (cnt)
A*?       1,1,1              0,0,0
A??       1,1,1              0,0,0
A+?       1,1,1              1,1,1

For reluctant quantifiers with min=0 (such as *? and ??), PostgreSQL
always consumes at least one row, while Oracle allows a zero-length
match. When min>=1 (e.g., A+?), both systems behave the same.

This behavior was consistently observed across the converted tests.

Design difference: unused DEFINE variables

Example:

  PATTERN (A+)
  DEFINE A AS id > 0, B AS id > 5

PostgreSQL executes the query successfully and ignores the unused
variable B.

Oracle raises:

  ORA-62503: illegal variable definition

Oracle limitations observed

Bounded quantifier limit

A{200}  -> works
A{201}  -> ORA-62518

Oracle appears to limit the upper bound of bounded quantifiers to 200,
while PostgreSQL does not impose this restriction.

Nested nullable quantifiers

Examples:

  (A*)*
  (A*)+
  (((A)*)*)*

  (A?|B){1,2}
  ((A?){2,3}){2,3}
  (A?){n,m}
  (A? B?){2,3}

Oracle raises:

  ORA-62513

when a nullable subpattern is wrapped by an outer quantifier, while
PostgreSQL executes these patterns successfully.

These results come from running the converted PostgreSQL RPR regression
tests on Oracle for comparison.

Best regards,
SugJun

---

From: Henson Choi <[email-masked]>
Date: Thu, 5 Mar 2026 13:59:41 +0900

Hi Tatsuo,

> match. When min>=1 (e.g., A+?), both systems behave the same.

This is indeed a bug. Thanks SugJun for finding it. I'll fix this in
the next patch.

>   ORA-62503: illegal variable definition

Currently PostgreSQL silently removes unused DEFINE variables during
optimization. Do you think we should raise an error instead, as Oracle
does?

> while PostgreSQL does not impose this restriction.

I don't think we need to impose an artificial limit like Oracle's 200.
What do you think?

> PostgreSQL executes these patterns successfully.

This seems like an Oracle limitation rather than a standard requirement.

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Fri, 6 Mar 2026 13:38:30 +0900

Hi Tatsuo,

I reviewed the planner's Window clause optimizations and found
additional issues with RPR windows.  Thanks to SungJun's Oracle
cross-validation work, all converted regression tests now pass
on both systems.  With these fixes, I'd like to move on to
non-functional quality testing next.

Here are eight incremental patches on top of v44.

nocfbot-0001: Fix elog message to use lowercase per PostgreSQL convention

  Minor style fix: lowercase the elog error message in
  nodeWindowAgg.c to follow PostgreSQL coding convention.

nocfbot-0002: Fix RPR reluctant quantifier flag lost during VIEW
serialization

  The reluctant flag on quantifiers was lost when a VIEW containing
  RPR was serialized and restored via ruleutils.c.  This patch fixes
  gram.y, rpr.c, ruleutils.c, and parsenodes.h so the flag survives
  the round-trip.

nocfbot-0003: Expand RPR test coverage and improve test comments

  Reorganizes test cases across rpr.sql, rpr_base.sql, rpr_explain.sql,
  and rpr_nfa.sql.  Moves base functionality tests from rpr.sql to
  rpr_base.sql and NFA-specific tests to rpr_nfa.sql.  Adds better
  section comments throughout.  Duplicate tests in rpr.sql were
  removed.  No reduction in test coverage.

nocfbot-0004: Keep RPR test objects for pg_upgrade/pg_dump testing

  Adjusts rpr_base.sql and rpr_explain.sql so that test tables and
  views are not dropped at the end of the test.  This allows
  pg_upgrade and pg_dump regression testing to cover RPR objects.

nocfbot-0005: Disable run condition pushdown for RPR windows

  Revised version of your run condition fix [1].  Existing test
  expected output updated accordingly.

nocfbot-0006: Disable frame optimization for RPR windows

  Prevents the planner from optimizing the window frame for RPR
  windows.  The frame clause in RPR has different semantics (it
  bounds the pattern search space), so standard frame optimizations
  must be disabled.  Adds EXPLAIN tests to verify.  Please review
  this one -- it's a newly discovered issue.

nocfbot-0007: Add stock scenario tests for RPR pattern matching

  Adds stock trading scenario tests using realistic synthetic data
  (stock.data with 1632 rows).  Tests V-shape recovery, W-shape,
  consecutive rises, and other common pattern matching use cases.

nocfbot-0008: Fix zero-min reluctant quantifier to produce zero-length match

  Fixes the bug reported by SungJun [2]: reluctant quantifiers with
  min=0 (A*?, A??) were incorrectly consuming at least one row instead
  of producing a zero-length match.  The NFA can now internally
  distinguish between a zero-length match and no match, so it will
  be ready when additional infrastructure (e.g. MATCH_NUMBER) is
  added.  Now matches Oracle behavior.

[1]
https://www.postgresql.org/message-id/[email-masked]
[2]
https://www.postgresql.org/message-id/[email-masked]

Best regards,
Henson
From f4abb464c187ef6d1334d856db881d40069531da Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 15:49:54 +0900
Subject: [PATCH 1/8] Fix elog message to use lowercase per PostgreSQL
 convention

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 701e83b519b..4441b4d9c51 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4710,7 +4710,7 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 			break;
 
 		default:
-			elog(ERROR, "Unrecognized state: %d at: " INT64_FORMAT,
+			elog(ERROR, "unrecognized state: %d at: " INT64_FORMAT,
 				 state, pos);
 			break;
 	}
-- 
2.50.1 (Apple Git-155)

From 2bc7f29dcfceefc83fd787eb138c96247f565360 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 15:50:48 +0900
Subject: [PATCH 2/8] Fix RPR reluctant quantifier flag lost during VIEW
 serialization

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 8792d8174fa..009c0f5019d 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -198,7 +198,7 @@ flattenSeqChildren(List *children)
 
 		/* GROUP{1,1} should have been unwrapped by optimizeGroupPattern */
 		Assert(!(opt->nodeType == RPR_PATTERN_GROUP &&
-				 opt->min == 1 && opt->max == 1 && opt->reluctant < 0));
+				 opt->min == 1 && opt->max == 1 && opt->reluctant == false));
 
 		if (opt->nodeType == RPR_PATTERN_SEQ)
 		{
@@ -234,7 +234,7 @@ mergeConsecutiveVars(List *children)
 	{
 		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 
-		if (child->nodeType == RPR_PATTERN_VAR && child->reluctant < 0)
+		if (child->nodeType == RPR_PATTERN_VAR && child->reluctant == false)
 		{
 			/* ----------------------
 			 * Can merge consecutive VAR nodes if:
@@ -253,7 +253,7 @@ mergeConsecutiveVars(List *children)
 				 * Merge: accumulate min/max into prev. prev is guaranteed to
 				 * be a non-reluctant VAR by the outer condition.
 				 */
-				Assert(prev->nodeType == RPR_PATTERN_VAR && prev->reluctant < 0);
+				Assert(prev->nodeType == RPR_PATTERN_VAR && prev->reluctant == false);
 
 				prev->min += child->min;
 
@@ -308,7 +308,7 @@ mergeConsecutiveGroups(List *children)
 	{
 		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 
-		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant < 0)
+		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant == false)
 		{
 			/* ----------------------
 			 * Can merge consecutive GROUP nodes if:
@@ -327,7 +327,7 @@ mergeConsecutiveGroups(List *children)
 				 * Merge: accumulate min/max into prev. prev is guaranteed to
 				 * be a non-reluctant GROUP by the outer condition.
 				 */
-				Assert(prev->nodeType == RPR_PATTERN_GROUP && prev->reluctant < 0);
+				Assert(prev->nodeType == RPR_PATTERN_GROUP && prev->reluctant == false);
 
 				prev->min += child->min;
 
@@ -385,7 +385,7 @@ mergeConsecutiveAlts(List *children)
 	{
 		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 
-		if (child->nodeType == RPR_PATTERN_ALT && child->reluctant < 0)
+		if (child->nodeType == RPR_PATTERN_ALT && child->reluctant == false)
 		{
 			if (prev != NULL &&
 				rprPatternChildrenEqual(prev->children, child->children))
@@ -406,7 +406,8 @@ mergeConsecutiveAlts(List *children)
 						group->nodeType = RPR_PATTERN_GROUP;
 						group->min = count;
 						group->max = count;
-						group->reluctant = -1;
+						group->reluctant = false;
+						group->reluctant_location = -1;
 						group->location = -1;
 						group->children = list_make1(prev);
 						mergedChildren = lappend(mergedChildren, group);
@@ -430,7 +431,8 @@ mergeConsecutiveAlts(List *children)
 					group->nodeType = RPR_PATTERN_GROUP;
 					group->min = count;
 					group->max = count;
-					group->reluctant = -1;
+					group->reluctant = false;
+					group->reluctant_location = -1;
 					group->location = -1;
 					group->children = list_make1(prev);
 					mergedChildren = lappend(mergedChildren, group);
@@ -454,7 +456,8 @@ mergeConsecutiveAlts(List *children)
 			group->nodeType = RPR_PATTERN_GROUP;
 			group->min = count;
 			group->max = count;
-			group->reluctant = -1;
+			group->reluctant = false;
+			group->reluctant_location = -1;
 			group->location = -1;
 			group->children = list_make1(prev);
 			mergedChildren = lappend(mergedChildren, group);
@@ -498,7 +501,7 @@ mergeGroupPrefixSuffix(List *children)
 		 * children. GROUP's content may be wrapped in a SEQ - unwrap for
 		 * comparison.
 		 */
-		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant < 0)
+		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant == false)
 		{
 			List	   *groupContent = child->children;
 			int			groupChildCount;
@@ -773,14 +776,14 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 	/* Parser always creates GROUP with exactly one child */
 	Assert(list_length(pattern->children) == 1);
 
-	if (pattern->reluctant >= 0)
+	if (pattern->reluctant)
 		return pattern;
 
 	child = (RPRPatternNode *) linitial(pattern->children);
 
 	if ((child->nodeType != RPR_PATTERN_VAR &&
 		 child->nodeType != RPR_PATTERN_GROUP) ||
-		child->reluctant >= 0)
+		child->reluctant)
 		return pattern;
 
 	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
@@ -862,11 +865,12 @@ tryUnwrapGroup(RPRPatternNode *pattern)
 	 * the child and unwrap.  E.g., (A)?? -> A??, (A)+? -> A+?
 	 */
 	if (child->nodeType == RPR_PATTERN_VAR &&
-		child->min == 1 && child->max == 1 && child->reluctant < 0)
+		child->min == 1 && child->max == 1 && child->reluctant == false)
 	{
 		child->min = pattern->min;
 		child->max = pattern->max;
 		child->reluctant = pattern->reluctant;
+		child->reluctant_location = pattern->reluctant_location;
 		return child;
 	}
 
@@ -1150,7 +1154,7 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
 	elem->next = RPR_ELEMIDX_INVALID;
 	elem->jump = RPR_ELEMIDX_INVALID;
-	if (node->reluctant >= 0)
+	if (node->reluctant)
 		elem->flags |= RPR_ELEM_RELUCTANT;
 	(*idx)++;
 
@@ -1189,7 +1193,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
 		elem->next = RPR_ELEMIDX_INVALID;	/* set by finalize */
 		elem->jump = RPR_ELEMIDX_INVALID;	/* set after END */
-		if (node->reluctant >= 0)
+		if (node->reluctant)
 			elem->flags |= RPR_ELEM_RELUCTANT;
 		(*idx)++;
 		groupStartIdx = *idx;	/* children start after BEGIN */
@@ -1214,7 +1218,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		endElem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
 		endElem->next = RPR_ELEMIDX_INVALID;
 		endElem->jump = groupStartIdx;	/* loop to first child */
-		if (node->reluctant >= 0)
+		if (node->reluctant)
 			endElem->flags |= RPR_ELEM_RELUCTANT;
 
 		/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b55a11cc837..f1a71cd036b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -16967,7 +16967,8 @@ row_pattern_alt:
 						n->children = list_make2($1, $3);
 						n->min = 1;
 						n->max = 1;
-						n->reluctant = -1;
+						n->reluctant = false;
+						n->reluctant_location = -1;
 						n->location = @1;
 						$$ = (Node *) n;
 					}
@@ -16994,7 +16995,8 @@ row_pattern_seq:
 						n->children = list_make2($1, $2);
 						n->min = 1;
 						n->max = 1;
-						n->reluctant = -1;
+						n->reluctant = false;
+						n->reluctant_location = -1;
 						n->location = @1;
 						$$ = (Node *) n;
 					}
@@ -17010,6 +17012,7 @@ row_pattern_term:
 					n->min = q->min;
 					n->max = q->max;
 					n->reluctant = q->reluctant;
+					n->reluctant_location = q->reluctant_location;
 					$$ = (Node *) n;
 				}
 		;
@@ -17022,7 +17025,8 @@ row_pattern_primary:
 					n->varName = $1;
 					n->min = 1;
 					n->max = 1;
-					n->reluctant = -1;
+					n->reluctant = false;
+					n->reluctant_location = -1;
 					n->children = NIL;
 					n->location = @1;
 					$$ = (Node *) n;
@@ -17035,7 +17039,8 @@ row_pattern_primary:
 					n->children = list_make1(inner);
 					n->min = 1;
 					n->max = 1;
-					n->reluctant = -1;
+					n->reluctant = false;
+					n->reluctant_location = -1;
 					n->location = @1;
 					$$ = (Node *) n;
 				}
@@ -20400,14 +20405,15 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
  *		Create an RPRPatternNode with specified quantifier bounds.
  */
 static RPRPatternNode *
-makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
+makeRPRQuantifier(int min, int max, ParseLoc reluctant_location, int location,
 				  core_yyscan_t yyscanner)
 {
 	RPRPatternNode *n = makeNode(RPRPatternNode);
 
 	n->min = min;
 	n->max = max;
-	n->reluctant = reluctant;
+	n->reluctant = (reluctant_location >= 0);
+	n->reluctant_location = reluctant_location;
 	n->location = location;
 	return n;
 }
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f5bb81e8c05..928bed2e9fb 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -6778,7 +6778,7 @@ append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
 	else
 		appendStringInfo(buf, "{%d,%d}", node->min, node->max);
 
-	if (node->reluctant >= 0)
+	if (node->reluctant)
 	{
 		if (!has_quantifier)
 			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8af527b57d3..22e856c671d 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -608,8 +608,8 @@ typedef struct RPRPatternNode
 	RPRPatternNodeType nodeType;	/* VAR, SEQ, ALT, GROUP */
 	int			min;			/* minimum repetitions (0 for *, ?) */
 	int			max;			/* maximum repetitions (INT_MAX for *, +) */
-	ParseLoc	reluctant;		/* location of '?' for reluctant, -1 for
-								 * greedy */
+	bool		reluctant;		/* true for reluctant (non-greedy) */
+	ParseLoc	reluctant_location; /* location of '?' token, or -1 */
 	ParseLoc	location;		/* token location, or -1 */
 	char	   *varName;		/* VAR: variable name */
 	List	   *children;		/* SEQ, ALT, GROUP: child nodes */
-- 
2.50.1 (Apple Git-155)

From 350f3234f318059826a2b2bd96e91e4d7fa22253 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 17:06:59 +0900
Subject: [PATCH 3/8] Expand RPR test coverage and improve test comments

diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 8af44392700..35b90a04492 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1,5 +1,10 @@
 --
--- Test for row pattern definition clause
+-- Test for row pattern recognition: WINDOW clause integration and
+-- real-world scenario tests using stock price data.
+--
+-- Parser/planner tests: rpr_base.sql
+-- NFA engine tests: rpr_nfa.sql
+-- EXPLAIN statistics tests: rpr_explain.sql
 --
 CREATE TEMP TABLE stock (
        company TEXT,
@@ -51,6 +56,9 @@ SELECT * FROM stock;
  company2 | 07-10-2023 |  1300
 (20 rows)
 
+--
+-- Basic pattern matching with PREV/NEXT
+--
 -- basic test using PREV
 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
  nth_value(tdate, 2) OVER w AS nth_second
@@ -426,7 +434,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
  company2 | 07-10-2023 |  1300 |             |           
 (20 rows)
 
--- basic test with none-greedy pattern
+-- basic test with fixed-length pattern (A A A = exactly 3)
 SELECT company, tdate, price, count(*) OVER w
  FROM stock
  WINDOW w AS (
@@ -752,7 +760,9 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
  company2 | 07-10-2023 |  1300 |             |           
 (20 rows)
 
--- using AFTER MATCH SKIP TO NEXT ROW
+-- using AFTER MATCH SKIP TO NEXT ROW (same pattern as above;
+-- match length is always 2, so result is identical to SKIP PAST LAST ROW.
+-- SKIP TO NEXT ROW's distinct effect is tested in backtracking section.)
 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
  FROM stock
  WINDOW w AS (
@@ -789,6 +799,179 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
  company2 | 07-10-2023 |  1300 |             |           
 (20 rows)
 
+-- PREV returns NULL at partition's first row (null_slot path)
+SELECT company, tdate, price, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (BOUNDARY REST+)
+ DEFINE
+  BOUNDARY AS PREV(price) IS NULL,
+  REST AS PREV(price) IS NOT NULL
+);
+ company  |   tdate    | price | count 
+----------+------------+-------+-------
+ company1 | 07-01-2023 |   100 |    10
+ company1 | 07-02-2023 |   200 |     0
+ company1 | 07-03-2023 |   150 |     0
+ company1 | 07-04-2023 |   140 |     0
+ company1 | 07-05-2023 |   150 |     0
+ company1 | 07-06-2023 |    90 |     0
+ company1 | 07-07-2023 |   110 |     0
+ company1 | 07-08-2023 |   130 |     0
+ company1 | 07-09-2023 |   120 |     0
+ company1 | 07-10-2023 |   130 |     0
+ company2 | 07-01-2023 |    50 |    10
+ company2 | 07-02-2023 |  2000 |     0
+ company2 | 07-03-2023 |  1500 |     0
+ company2 | 07-04-2023 |  1400 |     0
+ company2 | 07-05-2023 |  1500 |     0
+ company2 | 07-06-2023 |    60 |     0
+ company2 | 07-07-2023 |  1100 |     0
+ company2 | 07-08-2023 |  1300 |     0
+ company2 | 07-09-2023 |  1200 |     0
+ company2 | 07-10-2023 |  1300 |     0
+(20 rows)
+
+-- NEXT returns NULL at partition's last row (null_slot path)
+SELECT company, tdate, price, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ BOUNDARY)
+ DEFINE
+  A AS NEXT(price) IS NOT NULL,
+  BOUNDARY AS NEXT(price) IS NULL
+);
+ company  |   tdate    | price | count 
+----------+------------+-------+-------
+ company1 | 07-01-2023 |   100 |    10
+ company1 | 07-02-2023 |   200 |     0
+ company1 | 07-03-2023 |   150 |     0
+ company1 | 07-04-2023 |   140 |     0
+ company1 | 07-05-2023 |   150 |     0
+ company1 | 07-06-2023 |    90 |     0
+ company1 | 07-07-2023 |   110 |     0
+ company1 | 07-08-2023 |   130 |     0
+ company1 | 07-09-2023 |   120 |     0
+ company1 | 07-10-2023 |   130 |     0
+ company2 | 07-01-2023 |    50 |    10
+ company2 | 07-02-2023 |  2000 |     0
+ company2 | 07-03-2023 |  1500 |     0
+ company2 | 07-04-2023 |  1400 |     0
+ company2 | 07-05-2023 |  1500 |     0
+ company2 | 07-06-2023 |    60 |     0
+ company2 | 07-07-2023 |  1100 |     0
+ company2 | 07-08-2023 |  1300 |     0
+ company2 | 07-09-2023 |  1200 |     0
+ company2 | 07-10-2023 |  1300 |     0
+(20 rows)
+
+-- DESC order: PREV refers to the row with later date
+SELECT company, tdate, price, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate DESC
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START DOWN+ UP+)
+ DEFINE
+  START AS TRUE,
+  DOWN AS price < PREV(price),
+  UP AS price > PREV(price)
+);
+ company  |   tdate    | price | count 
+----------+------------+-------+-------
+ company1 | 07-10-2023 |   130 |     3
+ company1 | 07-09-2023 |   120 |     0
+ company1 | 07-08-2023 |   130 |     0
+ company1 | 07-07-2023 |   110 |     3
+ company1 | 07-06-2023 |    90 |     0
+ company1 | 07-05-2023 |   150 |     0
+ company1 | 07-04-2023 |   140 |     0
+ company1 | 07-03-2023 |   150 |     0
+ company1 | 07-02-2023 |   200 |     0
+ company1 | 07-01-2023 |   100 |     0
+ company2 | 07-10-2023 |  1300 |     3
+ company2 | 07-09-2023 |  1200 |     0
+ company2 | 07-08-2023 |  1300 |     0
+ company2 | 07-07-2023 |  1100 |     3
+ company2 | 07-06-2023 |    60 |     0
+ company2 | 07-05-2023 |  1500 |     0
+ company2 | 07-04-2023 |  1400 |     0
+ company2 | 07-03-2023 |  1500 |     0
+ company2 | 07-02-2023 |  2000 |     0
+ company2 | 07-01-2023 |    50 |     0
+(20 rows)
+
+-- Multiple partitions with unequal sizes
+WITH multi_part AS (
+ SELECT * FROM (VALUES
+  ('a', 1, 10), ('a', 2, 20), ('a', 3, 15),
+  ('b', 1, 5),
+  ('c', 1, 100), ('c', 2, 200), ('c', 3, 150), ('c', 4, 140), ('c', 5, 300)
+ ) AS t(grp, id, val)
+)
+SELECT grp, id, val, count(*) OVER w
+FROM multi_part
+WINDOW w AS (
+ PARTITION BY grp
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+  A AS val <= NEXT(val),
+  B AS val > PREV(val) OR val < PREV(val)
+);
+ grp | id | val | count 
+-----+----+-----+-------
+ a   |  1 |  10 |     3
+ a   |  2 |  20 |     0
+ a   |  3 |  15 |     0
+ b   |  1 |   5 |     0
+ c   |  1 | 100 |     5
+ c   |  2 | 200 |     0
+ c   |  3 | 150 |     0
+ c   |  4 | 140 |     0
+ c   |  5 | 300 |     0
+(9 rows)
+
+-- FLOAT/NUMERIC DEFINE conditions
+WITH float_data AS (
+ SELECT * FROM (VALUES
+  (1, 1.0::float8), (2, 1.5), (3, 1.4999), (4, 1.50001), (5, 0.1)
+ ) AS t(id, val)
+)
+SELECT id, val, count(*) OVER w
+FROM float_data
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+  A AS TRUE,
+  B AS val > PREV(val) * 0.99
+);
+ id |   val   | count 
+----+---------+-------
+  1 |       1 |     4
+  2 |     1.5 |     0
+  3 |  1.4999 |     0
+  4 | 1.50001 |     0
+  5 |     0.1 |     0
+(5 rows)
+
+--
+-- SKIP TO / Backtracking / Frame boundary
+--
 -- match everything
 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
  FROM stock
@@ -1128,6 +1311,47 @@ DOWN AS price < PREV(price)
  company2 | 07-10-2023 |  1300 |             |            |      |      |      |                       |     0
 (20 rows)
 
+-- row_number() within RPR reduced frame
+SELECT company, tdate, price, row_number() OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+ company  |   tdate    | price | row_number | count 
+----------+------------+-------+------------+-------
+ company1 | 07-01-2023 |   100 |          1 |     4
+ company1 | 07-02-2023 |   200 |          2 |     0
+ company1 | 07-03-2023 |   150 |          3 |     0
+ company1 | 07-04-2023 |   140 |          4 |     0
+ company1 | 07-05-2023 |   150 |          5 |     0
+ company1 | 07-06-2023 |    90 |          6 |     4
+ company1 | 07-07-2023 |   110 |          7 |     0
+ company1 | 07-08-2023 |   130 |          8 |     0
+ company1 | 07-09-2023 |   120 |          9 |     0
+ company1 | 07-10-2023 |   130 |         10 |     0
+ company2 | 07-01-2023 |    50 |          1 |     4
+ company2 | 07-02-2023 |  2000 |          2 |     0
+ company2 | 07-03-2023 |  1500 |          3 |     0
+ company2 | 07-04-2023 |  1400 |          4 |     0
+ company2 | 07-05-2023 |  1500 |          5 |     0
+ company2 | 07-06-2023 |    60 |          6 |     4
+ company2 | 07-07-2023 |  1100 |          7 |     0
+ company2 | 07-08-2023 |  1300 |          8 |     0
+ company2 | 07-09-2023 |  1200 |          9 |     0
+ company2 | 07-10-2023 |  1300 |         10 |     0
+(20 rows)
+
+--
+-- SQL Integration: JOIN, CTE, LATERAL
+--
 -- JOIN case
 CREATE TEMP TABLE t1 (i int, v1 int);
 CREATE TEMP TABLE t2 (j int, v2 int);
@@ -1262,6 +1486,9 @@ SELECT id, i, j, count(*) OVER w
   1 | 10 | 20 |     0
 (10 rows)
 
+--
+-- Large-scale / scalability tests
+--
 -- Smoke test for larger partitions.
 WITH s AS (
  SELECT v, count(*) OVER w AS c
@@ -1329,7 +1556,7 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
 (1 row)
 
 --
--- Using IGNORE NULLS
+-- IGNORE NULLS
 --
 -- no NULL rows case. The result should be identical with "basic test using PREV"
 SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w,
@@ -1371,7 +1598,7 @@ SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w,
 (20 rows)
 
 -- nth_value with IGNORE NULLS option wants to find the second row but
--- due a NULL in the midlle, it returns the third row.
+-- due to a NULL in the middle, it returns the third row.
 WITH data AS (
  SELECT * FROM (VALUES
   (10, 1), (11, NULL), (12, 3), (13, 4)
@@ -1395,8 +1622,8 @@ WITH data AS (
 (4 rows)
 
 -- nth_value with IGNORE NULLS option wants to find the third row but
--- due a NULL in the midlle, it reaches the end of reduced frame and
--- return NULL
+-- due to a NULL in the middle, it reaches the end of reduced frame and
+-- returns NULL
 WITH data AS (
  SELECT * FROM (VALUES
   (10, 1), (11, NULL), (12, 3), (13, 4)
@@ -1459,2555 +1686,156 @@ WINDOW w AS (
  company2 | 07-10-2023 |  1300 |         
 (20 rows)
 
--- View and pg_get_viewdef tests.
-CREATE TEMP VIEW v_window AS
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
- nth_value(tdate, 2) OVER w AS nth_second
- FROM stock
- WINDOW w AS (
- PARTITION BY company
+-- IGNORE NULLS + first_value where first value in reduced frame is NULL
+WITH data AS (
+ SELECT * FROM (VALUES
+  (1, NULL), (2, NULL), (3, 30), (4, 40)
+ ) AS t(id, val))
+SELECT id, val,
+ first_value(val) IGNORE NULLS OVER w AS fv_ignull,
+ count(*) OVER w
+FROM data
+WINDOW w AS (
+ ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price)
-);
-SELECT * FROM v_window;
- company  |   tdate    | price | first_value | last_value | nth_second 
-----------+------------+-------+-------------+------------+------------
- company1 | 07-01-2023 |   100 |         100 |        140 | 07-02-2023
- company1 | 07-02-2023 |   200 |             |            | 
- company1 | 07-03-2023 |   150 |             |            | 
- company1 | 07-04-2023 |   140 |             |            | 
- company1 | 07-05-2023 |   150 |             |            | 
- company1 | 07-06-2023 |    90 |          90 |        120 | 07-07-2023
- company1 | 07-07-2023 |   110 |             |            | 
- company1 | 07-08-2023 |   130 |             |            | 
- company1 | 07-09-2023 |   120 |             |            | 
- company1 | 07-10-2023 |   130 |             |            | 
- company2 | 07-01-2023 |    50 |          50 |       1400 | 07-02-2023
- company2 | 07-02-2023 |  2000 |             |            | 
- company2 | 07-03-2023 |  1500 |             |            | 
- company2 | 07-04-2023 |  1400 |             |            | 
- company2 | 07-05-2023 |  1500 |             |            | 
- company2 | 07-06-2023 |    60 |          60 |       1200 | 07-07-2023
- company2 | 07-07-2023 |  1100 |             |            | 
- company2 | 07-08-2023 |  1300 |             |            | 
- company2 | 07-09-2023 |  1200 |             |            | 
- company2 | 07-10-2023 |  1300 |             |            | 
-(20 rows)
-
-SELECT pg_get_viewdef('v_window');
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     first_value(price) OVER w AS first_value,                                        +
-     last_value(price) OVER w AS last_value,                                          +
-     nth_value(tdate, 2) OVER w AS nth_second                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (start up+ down+)                                                          +
-   DEFINE                                                                             +
-   start AS true,                                                                     +
-   up AS (price > prev(price)),                                                       +
-   down AS (price < prev(price)) );
-(1 row)
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
+);
+ id | val | fv_ignull | count 
+----+-----+-----------+-------
+  1 |     |        30 |     4
+  2 |     |           |     0
+  3 |  30 |           |     0
+  4 |  40 |           |     0
+(4 rows)
 
---
--- Pattern optimization tests
--- VIEW shows original pattern, EXPLAIN shows optimized pattern
---
--- Test: duplicate alternatives removal (A | B | A)+ -> (A | B)+
-CREATE TEMP VIEW v_opt_dup AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
+-- IGNORE NULLS + all values NULL in reduced frame
+WITH data AS (
+ SELECT * FROM (VALUES
+  (1, NULL), (2, NULL), (3, NULL)
+ ) AS t(id, val))
+SELECT id, val,
+ first_value(val) IGNORE NULLS OVER w AS fv_ignull,
+ last_value(val) IGNORE NULLS OVER w AS lv_ignull,
+ count(*) OVER w
+FROM data
+WINDOW w AS (
+ ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A | B | A)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
 );
-SELECT pg_get_viewdef('v_opt_dup');  -- original: ((a | b | a)+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a | b | a)+)                                                             +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
+ id | val | fv_ignull | lv_ignull | count 
+----+-----+-----------+-----------+-------
+  1 |     |           |           |     3
+  2 |     |           |           |     0
+  3 |     |           |           |     0
+(3 rows)
 
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup;  -- optimized: ((a | b)+)
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_dup
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a | b)+
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
+--
+-- last_value IGNORE NULLS with reduced frame containing all NULLs
+-- Exercises ignorenulls_getfuncarginframe SEEK_TAIL out-of-frame path
+-- when notnull_relpos >= num_reduced_frame.
+--
+CREATE TEMP TABLE rpr_nullval (id INT, val INT);
+INSERT INTO rpr_nullval VALUES (1, 10), (2, NULL), (3, NULL), (4, 20);
+SELECT id, val,
+       last_value(val) IGNORE NULLS OVER w AS lv_ignull,
+       count(*) OVER w AS cnt
+FROM rpr_nullval
+WINDOW w AS (
+  ORDER BY id
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  PATTERN (A B+)
+  DEFINE
+    A AS val IS NOT NULL,
+    B AS val IS NULL
+);
+ id | val | lv_ignull | cnt 
+----+-----+-----------+-----
+  1 |  10 |        10 |   3
+  2 |     |           |   0
+  3 |     |           |   0
+  4 |  20 |           |   0
+(4 rows)
 
--- Test: duplicate group removal ((A | B)+ | (A | B)+) -> (A | B)+
-CREATE TEMP VIEW v_opt_dup_group AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A | B)+ | (A | B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
+--
+-- NULL handling
+--
+CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER);
+INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100);
+INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL);  -- NULL in middle
+INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200);
+INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150);
+SELECT company, tdate, price, count(*) OVER w AS match_count
+FROM stock_null
+WINDOW w AS (
+  PARTITION BY company
+  ORDER BY tdate
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  PATTERN (START UP DOWN)
+  DEFINE START AS TRUE, UP AS price > PREV(price), DOWN AS price <
+PREV(price)
 );
-SELECT pg_get_viewdef('v_opt_dup_group');  -- original: ((a | b)+ | (a | b)+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a | b)+ | (a | b)+)                                                      +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup_group;  -- optimized: ((a | b)+)
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_dup_group
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a | b)+
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
+ company |   tdate    | price | match_count 
+---------+------------+-------+-------------
+ c1      | 07-01-2023 |   100 |           0
+ c1      | 07-02-2023 |       |           0
+ c1      | 07-03-2023 |   200 |           0
+ c1      | 07-04-2023 |   150 |           0
+(4 rows)
 
--- Test: consecutive vars merge (A A A) -> A{3}
-CREATE TEMP VIEW v_opt_merge AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
+-- Consecutive NULLs: PREV navigates through NULL values
+CREATE TEMP TABLE rpr_consec_null (id INT, val INT);
+INSERT INTO rpr_consec_null VALUES
+ (1, 100), (2, NULL), (3, NULL), (4, NULL), (5, 200), (6, 300);
+-- PREV(val) IS NULL succeeds for both null_slot (first row) and actual NULL
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_consec_null
+WINDOW w AS (
+ ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A A A)
- DEFINE
-  A AS price >= 140 AND price <= 150
-);
-SELECT pg_get_viewdef('v_opt_merge');  -- original: (a a a)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a a a)                                                                    +
-   DEFINE                                                                             +
-   a AS ((price >= 140) AND (price <= 150)) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge;  -- optimized: a{3}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a{3}
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+ C)
+ DEFINE
+  A AS val IS NULL,
+  B AS val IS NULL AND PREV(val) IS NULL,
+  C AS val IS NOT NULL
+);
+ id | val | cnt 
+----+-----+-----
+  1 | 100 |   0
+  2 |     |   4
+  3 |     |   0
+  4 |     |   0
+  5 | 200 |   0
+  6 | 300 |   0
+(6 rows)
 
--- Test: quantified vars merge (A A+ A) -> A{3,}
-CREATE TEMP VIEW v_opt_merge_quant AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
+-- NEXT(val) through consecutive NULLs
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_consec_null
+WINDOW w AS (
+ ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A A+ A)
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_merge_quant');  -- original: (a a+ a)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a a+ a)                                                                   +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_quant;  -- optimized: a{3,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_quant
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a{3,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: merge two unbounded (A+ A+) -> A{2,}
-CREATE TEMP VIEW v_opt_merge_unbounded AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A+ A+)
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_merge_unbounded');  -- original: (a+ a+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a+ a+)                                                                    +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_unbounded;  -- optimized: a{2,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_unbounded
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a{2,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: merge with zero-min (A* A+) -> A+
-CREATE TEMP VIEW v_opt_merge_star AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A* A+)
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_merge_star');  -- original: (a* a+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a* a+)                                                                    +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_star;  -- optimized: a+
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_star
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a+"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: complex merge (A A{2} A+ A{3}) -> A{7,}
-CREATE TEMP VIEW v_opt_merge_complex AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A A{2} A+ A{3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_merge_complex');  -- original: (a a{2} a+ a{3})
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a a{2} a+ a{3})                                                           +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_complex;  -- optimized: a{7,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_complex
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a{7,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: group merge ((A B) (A B)+) -> (A B){2,}
-CREATE TEMP VIEW v_opt_merge_group AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A B) (A B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_merge_group');  -- original: ((a b) (a b)+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a b) (a b)+)                                                             +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group;  -- expected: (a b){2,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_group
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a' b'){2,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: group merge A B (A B)+ -> (A B){2,}
-CREATE TEMP VIEW v_opt_merge_group2 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A B (A B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_merge_group2');  -- original: (a b (a b)+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a b (a b)+)                                                               +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group2;  -- expected: (a b){2,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_group2
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a' b'){2,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: group merge (A B) (A B)+ (A B) -> (A B){3,}
-CREATE TEMP VIEW v_opt_merge_group3 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A B) (A B)+ (A B))
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_merge_group3');  -- original: ((a b) (a b)+ (a b))
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a b) (a b)+ (a b))                                                       +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group3;  -- expected: (a b){3,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_group3
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a' b'){3,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: group merge A B A B (A B)+ A B A B -> (A B){5,}
-CREATE TEMP VIEW v_opt_merge_group4 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A B A B (A B)+ A B A B)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_merge_group4');  -- original: (a b a b (a b)+ a b a b)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a b a b (a b)+ a b a b)                                                   +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group4;  -- expected: (a b){5,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_group4
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a' b'){5,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: group merge C A B (A B)+ A B C -> C (A B){3,} C
-CREATE TEMP VIEW v_opt_merge_group5 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (C A B (A B)+ A B C)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100,
-  C AS price > 200
-);
-SELECT pg_get_viewdef('v_opt_merge_group5');  -- original: (c a b (a b)+ a b c)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (c a b (a b)+ a b c)                                                       +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100),                                                               +
-   c AS (price > 200) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group5;  -- expected: c (a b){3,} c
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_group5
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: c (a b){3,} c
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: consecutive GROUP merge (A B)+ (A B)+ -> (A B){2,}
-CREATE TEMP VIEW v_opt_merge_consec_group AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A B)+ (A B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_merge_consec_group');  -- original: ((a b)+ (a b)+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a b)+ (a b)+)                                                            +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_consec_group;  -- expected: (a b){2,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_consec_group
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a' b'){2,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: consecutive GROUP merge with different quantifiers (A B){2} (A B){3} -> (A B){5}
-CREATE TEMP VIEW v_opt_merge_consec_group2 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A B){2} (A B){3})
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_merge_consec_group2');  -- original: ((a b){2} (a b){3})
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a b){2} (a b){3})                                                        +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_consec_group2;  -- expected: (a b){5}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_merge_consec_group2
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a b){5}
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test {n} quantifier display
-CREATE TEMP VIEW v_quantifier_n AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A{3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_quantifier_n');
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a{3})                                                                     +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
--- Test {n,} quantifier display
-CREATE TEMP VIEW v_quantifier_n_plus AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A{2,})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_quantifier_n_plus');
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a{2,})                                                                    +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
--- Test: flatten nested SEQ (A (B C)) -> A B C
-CREATE TEMP VIEW v_opt_flatten_seq AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A (B C))
- DEFINE
-  A AS price > 100,
-  B AS price > 150,
-  C AS price < 150
-);
-SELECT pg_get_viewdef('v_opt_flatten_seq');  -- original: (a (b c))
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a (b c))                                                                  +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price > 150),                                                                +
-   c AS (price < 150) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_seq;  -- optimized: a b c
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_flatten_seq
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a b c
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: flatten nested ALT (A | (B | C)) -> (A | B | C)
-CREATE TEMP VIEW v_opt_flatten_alt AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A | (B | C))+)
- DEFINE
-  A AS price > 200,
-  B AS price > 100,
-  C AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_flatten_alt');  -- original: ((a | (b | c))+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a | (b | c))+)                                                           +
-   DEFINE                                                                             +
-   a AS (price > 200),                                                                +
-   b AS (price > 100),                                                                +
-   c AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_alt;  -- optimized: ((a | b | c))+
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_flatten_alt
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a | b | c)+
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: unwrap GROUP{1,1} ((A)) -> A
-CREATE TEMP VIEW v_opt_unwrap_group AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (((A)))
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_unwrap_group');  -- original: (((a)))
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (((a)))                                                                    +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_group;  -- optimized: a
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_unwrap_group
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: quantifier multiplication (A{2}){3} -> A{6}
-CREATE TEMP VIEW v_opt_quant_mult AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A{2}){3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_quant_mult');  -- original: ((a{2}){3})
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a{2}){3})                                                                +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult;  -- optimized: a{6}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_quant_mult
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a{6}
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: quantifier multiplication (A{2,4}){3} -> A{6,12}
-CREATE TEMP VIEW v_opt_quant_mult_range AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A{2,4}){3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_quant_mult_range');  -- original: ((a{2,4}){3})
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a{2,4}){3})                                                              +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range;  -- optimized: a{6,12}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_quant_mult_range
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a{6,12}
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: quantifier multiplication blocked (A{2}){3,5} -> no change
--- outer range with child exact > 1 causes gaps (6,8,10 not 6,7,8,9,10)
-CREATE TEMP VIEW v_opt_quant_mult_range2 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A{2}){3,5})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_quant_mult_range2');  -- original: ((a{2}){3,5})
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a{2}){3,5})                                                              +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range2;  -- NOT optimized: (a{2}){3,5}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_quant_mult_range2
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a{2}){3,5}
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: quantifier multiplication blocked by INF (A+){3} -> no change
-CREATE TEMP VIEW v_opt_quant_mult_inf AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A+){3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_quant_mult_inf');  -- original: ((a+){3})
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a+){3})                                                                  +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_inf;  -- no multiply: (a+){3}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_quant_mult_inf
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a+"){3}
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: unwrap single-item ALT after duplicate removal (A | A) -> A
-CREATE TEMP VIEW v_opt_unwrap_alt AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A | A)+)
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_unwrap_alt');  -- original: ((a | a)+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN ((a | a)+)                                                                 +
-   DEFINE                                                                             +
-   a AS (price > 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_alt;  -- optimized: a+
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_unwrap_alt
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a+"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: GROUP{1,1} to SEQ with flatten ((A B)(C D)) -> A B C D
-CREATE TEMP VIEW v_opt_group_to_seq AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (((A B)(C D)))
- DEFINE
-  A AS price > 200,
-  B AS price > 150,
-  C AS price > 100,
-  D AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_group_to_seq');  -- original: (((a b)(c d)))
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (((a b) (c d)))                                                            +
-   DEFINE                                                                             +
-   a AS (price > 200),                                                                +
-   b AS (price > 150),                                                                +
-   c AS (price > 100),                                                                +
-   d AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_group_to_seq;  -- optimized: a b c d
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_group_to_seq
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: a b c d
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: combined consecutive GROUP + prefix merge A B (A B)+ (A B)+ -> (A B){3,}
-CREATE TEMP VIEW v_opt_combined_merge AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A B (A B)+ (A B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_combined_merge');  -- original: (a b (a b)+ (a b)+)
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (a b (a b)+ (a b)+)                                                        +
-   DEFINE                                                                             +
-   a AS (price > 100),                                                                +
-   b AS (price <= 100) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_combined_merge;  -- expected: (a b){3,}
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_combined_merge
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: (a' b'){3,}"
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: nested ALT pattern - bug reproduction
-CREATE TEMP VIEW v_opt_nested_alt AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (((A B) | C) D | A B C)
- DEFINE
-  A AS price <= 100,
-  B AS price <= 150,
-  C AS price <= 200,
-  D AS price > 200
-);
-SELECT pg_get_viewdef('v_opt_nested_alt');
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (((a b) | c) d | a b c)                                                    +
-   DEFINE                                                                             +
-   a AS (price <= 100),                                                               +
-   b AS (price <= 150),                                                               +
-   c AS (price <= 200),                                                               +
-   d AS (price > 200) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_nested_alt;
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_nested_alt
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: ((a b | c) d | a b c)
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
--- Test: nested ALT with unbounded - A+ inside
-CREATE TEMP VIEW v_opt_nested_alt2 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (((A+ B) | C) D | A B C)
- DEFINE
-  A AS price <= 100,
-  B AS price <= 150,
-  C AS price <= 200,
-  D AS price > 200
-);
-SELECT pg_get_viewdef('v_opt_nested_alt2');
-                                    pg_get_viewdef                                     
----------------------------------------------------------------------------------------
-  SELECT company,                                                                     +
-     tdate,                                                                           +
-     price,                                                                           +
-     count(*) OVER w AS count                                                         +
-    FROM stock                                                                        +
-   WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
-   AFTER MATCH SKIP PAST LAST ROW                                                     +
-   INITIAL                                                                            +
-   PATTERN (((a+ b) | c) d | a b c)                                                   +
-   DEFINE                                                                             +
-   a AS (price <= 100),                                                               +
-   b AS (price <= 150),                                                               +
-   c AS (price <= 200),                                                               +
-   d AS (price > 200) );
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_nested_alt2;
-                                             QUERY PLAN                                             
-----------------------------------------------------------------------------------------------------
- Subquery Scan on v_opt_nested_alt2
-   ->  WindowAgg
-         Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-         Pattern: ((a+" b | c) d | a b c)
-         ->  Sort
-               Sort Key: stock.company
-               ->  Seq Scan on stock
-(7 rows)
-
---
--- Error cases
---
--- row pattern definition variable name must not appear more than once
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price),
-  UP AS price > PREV(price)
-);
-ERROR:  row pattern definition variable name "up" appears more than once in DEFINE clause
-LINE 11:   UP AS price > PREV(price),
-           ^
--- subqueries in DEFINE clause are not supported
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START LOWPRICE)
- DEFINE
-  START AS TRUE,
-  LOWPRICE AS price < (SELECT 100)
-);
-ERROR:  cannot use subquery in DEFINE expression
-LINE 11:   LOWPRICE AS price < (SELECT 100)
-                               ^
--- aggregates in DEFINE clause are not supported
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START LOWPRICE)
- DEFINE
-  START AS TRUE,
-  LOWPRICE AS price < count(*)
-);
-ERROR:  aggregate functions are not allowed in DEFINE
-LINE 11:   LOWPRICE AS price < count(*)
-                               ^
--- FRAME must start at current row when row pattern recognition is used
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price)
-);
-ERROR:  FRAME must start at CURRENT ROW when row pattern recognition is used
-LINE 6:  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
-         ^
-DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
-HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- EXCLUDE is not permitted
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- EXCLUDE CURRENT ROW
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price)
-);
-ERROR:  EXCLUDE options are not permitted when row pattern recognition is used
-LINE 7:  EXCLUDE CURRENT ROW
-         ^
-DETAIL:  Frame definition includes EXCLUDE CURRENT ROW.
-HINT:  Remove the EXCLUDE clause from the window definition.
--- SEEK is not supported
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- AFTER MATCH SKIP TO NEXT ROW
- SEEK
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price)
-);
-ERROR:  SEEK is not supported
-LINE 8:  SEEK
-         ^
-HINT:  Use INITIAL instead.
--- PREV's argument must have at least 1 column reference
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- AFTER MATCH SKIP TO NEXT ROW
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(1),
-  DOWN AS price < PREV(1)
-);
-ERROR:  row pattern navigation operation's argument must include at least one column reference
--- Unsupported quantifier
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- AFTER MATCH SKIP TO NEXT ROW
- INITIAL
- PATTERN (START UP~ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(1),
-  DOWN AS price < PREV(1)
-);
-ERROR:  unsupported quantifier "~"
-LINE 9:  PATTERN (START UP~ DOWN+)
-                          ^
-HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
--- PREV(1) missing column reference (error)
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- AFTER MATCH SKIP TO NEXT ROW
- INITIAL
- PATTERN (START UP+? DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(1),
-  DOWN AS price < PREV(1)
-);
-ERROR:  row pattern navigation operation's argument must include at least one column reference
--- Maximum pattern variables is 251 (RPR_VARID_MAX)
--- Error: 252 variables exceeds limit of 251
-DO $$
-DECLARE
-    pattern_vars text;
-    define_vars text;
-    query text;
-BEGIN
-    SELECT string_agg('v' || lpad(i::text, 3, '0'), ' '),
-           string_agg('v' || lpad(i::text, 3, '0') || ' AS TRUE', ', ')
-    INTO pattern_vars, define_vars
-    FROM generate_series(1, 252) i;
-
-    query := format('SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (%s)
-        DEFINE %s)', pattern_vars, define_vars);
-
-    EXECUTE query;
-END;
-$$;
-ERROR:  too many pattern variables
-DETAIL:  Maximum is 251.
-CONTEXT:  SQL statement "SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (v001 v002 v003 v004 v005 v006 v007 v008 v009 v010 v011 v012 v013 v014 v015 v016 v017 v018 v019 v020 v021 v022 v023 v024 v025 v026 v027 v028 v029 v030 v031 v032 v033 v034 v035 v036 v037 v038 v039 v040 v041 v042 v043 v044 v045 v046 v047 v048 v049 v050 v051 v052 v053 v054 v055 v056 v057 v058 v059 v060 v061 v062 v063 v064 v065 v066 v067 v068 v069 v070 v071 v072 v073 v074 v075 v076 v077 v078 v079 v080 v081 v082 v083 v084 v085 v086 v087 v088 v089 v090 v091 v092 v093 v094 v095 v096 v097 v098 v099 v100 v101 v102 v103 v104 v105 v106 v107 v108 v109 v110 v111 v112 v113 v114 v115 v116 v117 v118 v119 v120 v121 v122 v123 v124 v125 v126 v127 v128 v129 v130 v131 v132 v133 v134 v135 v136 v137 v138 v139 v140 v141 v142 v143 v144 v145 v146 v147 v148 v149 v150 v151 v152 v153 v154 v155 v156 v157 v158 v159 v160 v161 v162 v163 v164 v165 v166 v167 v168 v169 v170 v171 v172 v173 v174 v175 v176 v177 v178 v179 v180 v181 v182 v183 v184 v185 v186 v187 v188 v189 v190 v191 v192 v193 v194 v195 v196 v197 v198 v199 v200 v201 v202 v203 v204 v205 v206 v207 v208 v209 v210 v211 v212 v213 v214 v215 v216 v217 v218 v219 v220 v221 v222 v223 v224 v225 v226 v227 v228 v229 v230 v231 v232 v233 v234 v235 v236 v237 v238 v239 v240 v241 v242 v243 v244 v245 v246 v247 v248 v249 v250 v251 v252)
-        DEFINE v001 AS TRUE, v002 AS TRUE, v003 AS TRUE, v004 AS TRUE, v005 AS TRUE, v006 AS TRUE, v007 AS TRUE, v008 AS TRUE, v009 AS TRUE, v010 AS TRUE, v011 AS TRUE, v012 AS TRUE, v013 AS TRUE, v014 AS TRUE, v015 AS TRUE, v016 AS TRUE, v017 AS TRUE, v018 AS TRUE, v019 AS TRUE, v020 AS TRUE, v021 AS TRUE, v022 AS TRUE, v023 AS TRUE, v024 AS TRUE, v025 AS TRUE, v026 AS TRUE, v027 AS TRUE, v028 AS TRUE, v029 AS TRUE, v030 AS TRUE, v031 AS TRUE, v032 AS TRUE, v033 AS TRUE, v034 AS TRUE, v035 AS TRUE, v036 AS TRUE, v037 AS TRUE, v038 AS TRUE, v039 AS TRUE, v040 AS TRUE, v041 AS TRUE, v042 AS TRUE, v043 AS TRUE, v044 AS TRUE, v045 AS TRUE, v046 AS TRUE, v047 AS TRUE, v048 AS TRUE, v049 AS TRUE, v050 AS TRUE, v051 AS TRUE, v052 AS TRUE, v053 AS TRUE, v054 AS TRUE, v055 AS TRUE, v056 AS TRUE, v057 AS TRUE, v058 AS TRUE, v059 AS TRUE, v060 AS TRUE, v061 AS TRUE, v062 AS TRUE, v063 AS TRUE, v064 AS TRUE, v065 AS TRUE, v066 AS TRUE, v067 AS TRUE, v068 AS TRUE, v069 AS TRUE, v070 AS TRUE, v071 AS TRUE, v072 AS TRUE, v073 AS TRUE, v074 AS TRUE, v075 AS TRUE, v076 AS TRUE, v077 AS TRUE, v078 AS TRUE, v079 AS TRUE, v080 AS TRUE, v081 AS TRUE, v082 AS TRUE, v083 AS TRUE, v084 AS TRUE, v085 AS TRUE, v086 AS TRUE, v087 AS TRUE, v088 AS TRUE, v089 AS TRUE, v090 AS TRUE, v091 AS TRUE, v092 AS TRUE, v093 AS TRUE, v094 AS TRUE, v095 AS TRUE, v096 AS TRUE, v097 AS TRUE, v098 AS TRUE, v099 AS TRUE, v100 AS TRUE, v101 AS TRUE, v102 AS TRUE, v103 AS TRUE, v104 AS TRUE, v105 AS TRUE, v106 AS TRUE, v107 AS TRUE, v108 AS TRUE, v109 AS TRUE, v110 AS TRUE, v111 AS TRUE, v112 AS TRUE, v113 AS TRUE, v114 AS TRUE, v115 AS TRUE, v116 AS TRUE, v117 AS TRUE, v118 AS TRUE, v119 AS TRUE, v120 AS TRUE, v121 AS TRUE, v122 AS TRUE, v123 AS TRUE, v124 AS TRUE, v125 AS TRUE, v126 AS TRUE, v127 AS TRUE, v128 AS TRUE, v129 AS TRUE, v130 AS TRUE, v131 AS TRUE, v132 AS TRUE, v133 AS TRUE, v134 AS TRUE, v135 AS TRUE, v136 AS TRUE, v137 AS TRUE, v138 AS TRUE, v139 AS TRUE, v140 AS TRUE, v141 AS TRUE, v142 AS TRUE, v143 AS TRUE, v144 AS TRUE, v145 AS TRUE, v146 AS TRUE, v147 AS TRUE, v148 AS TRUE, v149 AS TRUE, v150 AS TRUE, v151 AS TRUE, v152 AS TRUE, v153 AS TRUE, v154 AS TRUE, v155 AS TRUE, v156 AS TRUE, v157 AS TRUE, v158 AS TRUE, v159 AS TRUE, v160 AS TRUE, v161 AS TRUE, v162 AS TRUE, v163 AS TRUE, v164 AS TRUE, v165 AS TRUE, v166 AS TRUE, v167 AS TRUE, v168 AS TRUE, v169 AS TRUE, v170 AS TRUE, v171 AS TRUE, v172 AS TRUE, v173 AS TRUE, v174 AS TRUE, v175 AS TRUE, v176 AS TRUE, v177 AS TRUE, v178 AS TRUE, v179 AS TRUE, v180 AS TRUE, v181 AS TRUE, v182 AS TRUE, v183 AS TRUE, v184 AS TRUE, v185 AS TRUE, v186 AS TRUE, v187 AS TRUE, v188 AS TRUE, v189 AS TRUE, v190 AS TRUE, v191 AS TRUE, v192 AS TRUE, v193 AS TRUE, v194 AS TRUE, v195 AS TRUE, v196 AS TRUE, v197 AS TRUE, v198 AS TRUE, v199 AS TRUE, v200 AS TRUE, v201 AS TRUE, v202 AS TRUE, v203 AS TRUE, v204 AS TRUE, v205 AS TRUE, v206 AS TRUE, v207 AS TRUE, v208 AS TRUE, v209 AS TRUE, v210 AS TRUE, v211 AS TRUE, v212 AS TRUE, v213 AS TRUE, v214 AS TRUE, v215 AS TRUE, v216 AS TRUE, v217 AS TRUE, v218 AS TRUE, v219 AS TRUE, v220 AS TRUE, v221 AS TRUE, v222 AS TRUE, v223 AS TRUE, v224 AS TRUE, v225 AS TRUE, v226 AS TRUE, v227 AS TRUE, v228 AS TRUE, v229 AS TRUE, v230 AS TRUE, v231 AS TRUE, v232 AS TRUE, v233 AS TRUE, v234 AS TRUE, v235 AS TRUE, v236 AS TRUE, v237 AS TRUE, v238 AS TRUE, v239 AS TRUE, v240 AS TRUE, v241 AS TRUE, v242 AS TRUE, v243 AS TRUE, v244 AS TRUE, v245 AS TRUE, v246 AS TRUE, v247 AS TRUE, v248 AS TRUE, v249 AS TRUE, v250 AS TRUE, v251 AS TRUE, v252 AS TRUE)"
-PL/pgSQL function inline_code_block line 17 at EXECUTE
--- Error: 253 variables exceeds limit of 251
-DO $$
-DECLARE
-    pattern_vars text;
-    define_vars text;
-    query text;
-BEGIN
-    SELECT string_agg('v' || lpad(i::text, 3, '0'), ' '),
-           string_agg('v' || lpad(i::text, 3, '0') || ' AS TRUE', ', ')
-    INTO pattern_vars, define_vars
-    FROM generate_series(1, 253) i;
-
-    query := format('SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (%s)
-        DEFINE %s)', pattern_vars, define_vars);
-
-    EXECUTE query;
-END;
-$$;
-ERROR:  too many pattern variables
-DETAIL:  Maximum is 251.
-CONTEXT:  SQL statement "SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (v001 v002 v003 v004 v005 v006 v007 v008 v009 v010 v011 v012 v013 v014 v015 v016 v017 v018 v019 v020 v021 v022 v023 v024 v025 v026 v027 v028 v029 v030 v031 v032 v033 v034 v035 v036 v037 v038 v039 v040 v041 v042 v043 v044 v045 v046 v047 v048 v049 v050 v051 v052 v053 v054 v055 v056 v057 v058 v059 v060 v061 v062 v063 v064 v065 v066 v067 v068 v069 v070 v071 v072 v073 v074 v075 v076 v077 v078 v079 v080 v081 v082 v083 v084 v085 v086 v087 v088 v089 v090 v091 v092 v093 v094 v095 v096 v097 v098 v099 v100 v101 v102 v103 v104 v105 v106 v107 v108 v109 v110 v111 v112 v113 v114 v115 v116 v117 v118 v119 v120 v121 v122 v123 v124 v125 v126 v127 v128 v129 v130 v131 v132 v133 v134 v135 v136 v137 v138 v139 v140 v141 v142 v143 v144 v145 v146 v147 v148 v149 v150 v151 v152 v153 v154 v155 v156 v157 v158 v159 v160 v161 v162 v163 v164 v165 v166 v167 v168 v169 v170 v171 v172 v173 v174 v175 v176 v177 v178 v179 v180 v181 v182 v183 v184 v185 v186 v187 v188 v189 v190 v191 v192 v193 v194 v195 v196 v197 v198 v199 v200 v201 v202 v203 v204 v205 v206 v207 v208 v209 v210 v211 v212 v213 v214 v215 v216 v217 v218 v219 v220 v221 v222 v223 v224 v225 v226 v227 v228 v229 v230 v231 v232 v233 v234 v235 v236 v237 v238 v239 v240 v241 v242 v243 v244 v245 v246 v247 v248 v249 v250 v251 v252 v253)
-        DEFINE v001 AS TRUE, v002 AS TRUE, v003 AS TRUE, v004 AS TRUE, v005 AS TRUE, v006 AS TRUE, v007 AS TRUE, v008 AS TRUE, v009 AS TRUE, v010 AS TRUE, v011 AS TRUE, v012 AS TRUE, v013 AS TRUE, v014 AS TRUE, v015 AS TRUE, v016 AS TRUE, v017 AS TRUE, v018 AS TRUE, v019 AS TRUE, v020 AS TRUE, v021 AS TRUE, v022 AS TRUE, v023 AS TRUE, v024 AS TRUE, v025 AS TRUE, v026 AS TRUE, v027 AS TRUE, v028 AS TRUE, v029 AS TRUE, v030 AS TRUE, v031 AS TRUE, v032 AS TRUE, v033 AS TRUE, v034 AS TRUE, v035 AS TRUE, v036 AS TRUE, v037 AS TRUE, v038 AS TRUE, v039 AS TRUE, v040 AS TRUE, v041 AS TRUE, v042 AS TRUE, v043 AS TRUE, v044 AS TRUE, v045 AS TRUE, v046 AS TRUE, v047 AS TRUE, v048 AS TRUE, v049 AS TRUE, v050 AS TRUE, v051 AS TRUE, v052 AS TRUE, v053 AS TRUE, v054 AS TRUE, v055 AS TRUE, v056 AS TRUE, v057 AS TRUE, v058 AS TRUE, v059 AS TRUE, v060 AS TRUE, v061 AS TRUE, v062 AS TRUE, v063 AS TRUE, v064 AS TRUE, v065 AS TRUE, v066 AS TRUE, v067 AS TRUE, v068 AS TRUE, v069 AS TRUE, v070 AS TRUE, v071 AS TRUE, v072 AS TRUE, v073 AS TRUE, v074 AS TRUE, v075 AS TRUE, v076 AS TRUE, v077 AS TRUE, v078 AS TRUE, v079 AS TRUE, v080 AS TRUE, v081 AS TRUE, v082 AS TRUE, v083 AS TRUE, v084 AS TRUE, v085 AS TRUE, v086 AS TRUE, v087 AS TRUE, v088 AS TRUE, v089 AS TRUE, v090 AS TRUE, v091 AS TRUE, v092 AS TRUE, v093 AS TRUE, v094 AS TRUE, v095 AS TRUE, v096 AS TRUE, v097 AS TRUE, v098 AS TRUE, v099 AS TRUE, v100 AS TRUE, v101 AS TRUE, v102 AS TRUE, v103 AS TRUE, v104 AS TRUE, v105 AS TRUE, v106 AS TRUE, v107 AS TRUE, v108 AS TRUE, v109 AS TRUE, v110 AS TRUE, v111 AS TRUE, v112 AS TRUE, v113 AS TRUE, v114 AS TRUE, v115 AS TRUE, v116 AS TRUE, v117 AS TRUE, v118 AS TRUE, v119 AS TRUE, v120 AS TRUE, v121 AS TRUE, v122 AS TRUE, v123 AS TRUE, v124 AS TRUE, v125 AS TRUE, v126 AS TRUE, v127 AS TRUE, v128 AS TRUE, v129 AS TRUE, v130 AS TRUE, v131 AS TRUE, v132 AS TRUE, v133 AS TRUE, v134 AS TRUE, v135 AS TRUE, v136 AS TRUE, v137 AS TRUE, v138 AS TRUE, v139 AS TRUE, v140 AS TRUE, v141 AS TRUE, v142 AS TRUE, v143 AS TRUE, v144 AS TRUE, v145 AS TRUE, v146 AS TRUE, v147 AS TRUE, v148 AS TRUE, v149 AS TRUE, v150 AS TRUE, v151 AS TRUE, v152 AS TRUE, v153 AS TRUE, v154 AS TRUE, v155 AS TRUE, v156 AS TRUE, v157 AS TRUE, v158 AS TRUE, v159 AS TRUE, v160 AS TRUE, v161 AS TRUE, v162 AS TRUE, v163 AS TRUE, v164 AS TRUE, v165 AS TRUE, v166 AS TRUE, v167 AS TRUE, v168 AS TRUE, v169 AS TRUE, v170 AS TRUE, v171 AS TRUE, v172 AS TRUE, v173 AS TRUE, v174 AS TRUE, v175 AS TRUE, v176 AS TRUE, v177 AS TRUE, v178 AS TRUE, v179 AS TRUE, v180 AS TRUE, v181 AS TRUE, v182 AS TRUE, v183 AS TRUE, v184 AS TRUE, v185 AS TRUE, v186 AS TRUE, v187 AS TRUE, v188 AS TRUE, v189 AS TRUE, v190 AS TRUE, v191 AS TRUE, v192 AS TRUE, v193 AS TRUE, v194 AS TRUE, v195 AS TRUE, v196 AS TRUE, v197 AS TRUE, v198 AS TRUE, v199 AS TRUE, v200 AS TRUE, v201 AS TRUE, v202 AS TRUE, v203 AS TRUE, v204 AS TRUE, v205 AS TRUE, v206 AS TRUE, v207 AS TRUE, v208 AS TRUE, v209 AS TRUE, v210 AS TRUE, v211 AS TRUE, v212 AS TRUE, v213 AS TRUE, v214 AS TRUE, v215 AS TRUE, v216 AS TRUE, v217 AS TRUE, v218 AS TRUE, v219 AS TRUE, v220 AS TRUE, v221 AS TRUE, v222 AS TRUE, v223 AS TRUE, v224 AS TRUE, v225 AS TRUE, v226 AS TRUE, v227 AS TRUE, v228 AS TRUE, v229 AS TRUE, v230 AS TRUE, v231 AS TRUE, v232 AS TRUE, v233 AS TRUE, v234 AS TRUE, v235 AS TRUE, v236 AS TRUE, v237 AS TRUE, v238 AS TRUE, v239 AS TRUE, v240 AS TRUE, v241 AS TRUE, v242 AS TRUE, v243 AS TRUE, v244 AS TRUE, v245 AS TRUE, v246 AS TRUE, v247 AS TRUE, v248 AS TRUE, v249 AS TRUE, v250 AS TRUE, v251 AS TRUE, v252 AS TRUE, v253 AS TRUE)"
-PL/pgSQL function inline_code_block line 17 at EXECUTE
-    CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER);
-    INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100);
-    INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL);  -- NULL in middle
-    INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200);
-    INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150);
-    SELECT company, tdate, price, count(*) OVER w AS match_count
-    FROM stock_null
-    WINDOW w AS (
-      PARTITION BY company
-      ORDER BY tdate
-      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-      PATTERN (START UP DOWN)
-      DEFINE START AS TRUE, UP AS price > PREV(price), DOWN AS price <
-PREV(price)
-    );
- company |   tdate    | price | match_count 
----------+------------+-------+-------------
- c1      | 07-01-2023 |   100 |           0
- c1      | 07-02-2023 |       |           0
- c1      | 07-03-2023 |   200 |           0
- c1      | 07-04-2023 |   150 |           0
-(4 rows)
-
--- Overlapping match tests (requires multi-context for correct behavior)
--- Using array flags: 'X' = ANY(flags) for multi-TRUE support
--- Test 1: A B C D E | B C D | C D E F - three overlapping patterns
--- Different end points: B C D (4), A B C D E (5), C D E F (6)
-WITH test_overlap1 AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E']),
-        (6, ARRAY['F'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_overlap1
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C D E | B C D | C D E F)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags),
-        F AS 'F' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         5
-  2 | {B}   |             |          
-  3 | {C}   |             |          
-  4 | {D}   |             |          
-  5 | {E}   |             |          
-  6 | {F}   |             |          
-(6 rows)
-
-WITH test_overlap1 AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E']),
-        (6, ARRAY['F'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_overlap1
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A B C D E | B C D | C D E F)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags),
-        F AS 'F' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         5
-  2 | {B}   |           2 |         4
-  3 | {C}   |           3 |         6
-  4 | {D}   |             |          
-  5 | {E}   |             |          
-  6 | {F}   |             |          
-(6 rows)
-
--- PAST LAST: only one match
--- TO NEXT ROW with multi-context: three matches
---   Row 1: A B C D E (1-5)
---   Row 2: B C D (2-4) <- ends first!
---   Row 3: C D E F (3-6) <- ends last!
--- Test 1b: Longer pattern FAILS, shorter pattern should survive
--- Pattern: A+ B C D E | B+ C
--- A+ B C D E fails (no E found in sequence)
--- B+ C matches at rows 2-3
--- Result: match 2-3 (B+ C)
-WITH test_overlap1b AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_overlap1b
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A+ B C D E | B+ C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |             |          
-  2 | {B}   |           2 |         3
-  3 | {C}   |             |          
-  4 | {D}   |             |          
-  5 | {X}   |             |          
-(5 rows)
-
--- Test 2: A B+ C | B+ D - long B sequence with different endings
-WITH test_overlap2 AS (
-    SELECT * FROM (VALUES
-        (1,  ARRAY['A']),
-        (2,  ARRAY['B']),
-        (3,  ARRAY['B']),
-        (4,  ARRAY['B']),
-        (5,  ARRAY['B']),
-        (6,  ARRAY['C']),
-        (7,  ARRAY['B']),
-        (8,  ARRAY['B']),
-        (9,  ARRAY['B']),
-        (10, ARRAY['D'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_overlap2
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A B+ C | B+ D)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         6
-  2 | {B}   |             |          
-  3 | {B}   |             |          
-  4 | {B}   |             |          
-  5 | {B}   |             |          
-  6 | {C}   |             |          
-  7 | {B}   |           7 |        10
-  8 | {B}   |           8 |        10
-  9 | {B}   |           9 |        10
- 10 | {D}   |             |          
-(10 rows)
-
--- Current result (correct):
---   Row 1: A B+ C (1-6)
---   Row 7-9: B+ D (7-10, 8-10, 9-10)
--- Note: Row 2-6 cannot match B+ D because Row 6 is C, not D
--- With absorption: 8-10 and 9-10 would be absorbed by 7-10 (earlier context covers later)
--- Test 3: Greedy quantifier with late failure - A B C+ D | A B
--- Pattern expects D after C+, but E comes instead ("betrayal")
-WITH test_betrayal AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['C']),
-        (5, ARRAY['C']),
-        (6, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_betrayal
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C+ D | A B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         2
-  2 | {B}   |             |          
-  3 | {C}   |             |          
-  4 | {C}   |             |          
-  5 | {C}   |             |          
-  6 | {E}   |             |          
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+ C)
+ DEFINE
+  A AS val IS NOT NULL,
+  B AS val IS NULL AND NEXT(val) IS NULL,
+  C AS val IS NULL AND NEXT(val) IS NOT NULL
+);
+ id | val | cnt 
+----+-----+-----
+  1 | 100 |   4
+  2 |     |   0
+  3 |     |   0
+  4 |     |   0
+  5 | 200 |   0
+  6 | 300 |   0
 (6 rows)
 
--- A B C+ D fails at Row 6 (E instead of D)
--- Question: Does it fallback to A B (1-2)?
--- Test 4: Lexical Order test - A B C | A B C D E
--- SQL standard: first matching alternative wins
-WITH test_lexical AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_lexical
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C | A B C D E)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         3
-  2 | {B}   |             |          
-  3 | {C}   |             |          
-  4 | {D}   |             |          
-  5 | {E}   |             |          
-(5 rows)
-
--- SQL standard Lexical Order: A B C (1-3) wins (first alternative)
--- Test 4b: Reversed pattern order - A B C D E | A B C
-WITH test_lexical2 AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_lexical2
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C D E | A B C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         5
-  2 | {B}   |             |          
-  3 | {C}   |             |          
-  4 | {D}   |             |          
-  5 | {E}   |             |          
-(5 rows)
-
--- SQL standard Lexical Order: A B C D E (1-5) wins (first alternative)
--- Test 5: Multiple TRUE in single row (overlapping pattern variables)
--- Each row matches multiple DEFINE conditions simultaneously
-WITH test_multi_true AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A','B']),        -- A and B both TRUE
-        (2, ARRAY['B','C']),        -- B and C both TRUE
-        (3, ARRAY['C','D']),        -- C and D both TRUE
-        (4, ARRAY['D','E']),        -- D and E both TRUE
-        (5, ARRAY['E','_'])         -- E only
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_multi_true
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C D E)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A,B} |           1 |         5
-  2 | {B,C} |             |          
-  3 | {C,D} |             |          
-  4 | {D,E} |             |          
-  5 | {E,_} |             |          
-(5 rows)
-
--- Row 1: A=T, B=T -> matches A
--- Row 2: B=T, C=T -> matches B
--- Row 3: C=T, D=T -> matches C
--- Row 4: D=T, E=T -> matches D
--- Row 5: E=T      -> matches E
--- Result: match 1-5 (A B C D E)
--- Test 6: Diagonal pattern with multi-TRUE (shifted overlap)
-WITH test_diagonal AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A','_']),
-        (2, ARRAY['B','A']),
-        (3, ARRAY['C','B']),
-        (4, ARRAY['D','C']),
-        (5, ARRAY['_','D'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_diagonal
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A B C D)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A,_} |           1 |         4
-  2 | {B,A} |           2 |         5
-  3 | {C,B} |             |          
-  4 | {D,C} |             |          
-  5 | {_,D} |             |          
-(5 rows)
-
--- Possible matches:
---   Start Row 1: A(1) B(2) C(3) D(4) -> 1-4
---   Start Row 2: A(2) B(3) C(4) D(5) -> 2-5 (because Row 2 has A too!)
--- ===================================================================
--- Context Absorption Tests
--- ===================================================================
--- Test absorption 1: Basic A+ pattern - later contexts absorbed by earlier
-WITH test_absorb_basic AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['A']),
-        (5, ARRAY['B'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_absorb_basic
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A+)
-    DEFINE A AS 'A' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         4
-  2 | {A}   |           2 |         4
-  3 | {A}   |           3 |         4
-  4 | {A}   |           4 |         4
-  5 | {B}   |             |          
-(5 rows)
-
--- Pattern A+ is absorbable (unbounded first element, only one unbounded)
--- 4 matches: (1-4, 2-4, 3-4, 4-4)
--- Test absorption 2: A+ B pattern - absorption with fixed suffix
-WITH test_absorb_suffix AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['B']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_absorb_suffix
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A+ B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         4
-  2 | {A}   |           2 |         4
-  3 | {A}   |           3 |         4
-  4 | {B}   |             |          
-  5 | {X}   |             |          
-(5 rows)
-
--- Pattern A+ B is absorbable (A+ unbounded first, B bounded suffix)
--- All potential matches end at same row (row 4 with B)
--- 3 matches: (1-4, 2-4, 3-4)
--- Test absorption 3: Per-branch absorption with ALT (B+ C | B+ D)
-WITH test_absorb_alt AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['B']),
-        (2, ARRAY['B']),
-        (3, ARRAY['B']),
-        (4, ARRAY['D']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_absorb_alt
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (B+ C | B+ D)
-    DEFINE
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {B}   |           1 |         4
-  2 | {B}   |           2 |         4
-  3 | {B}   |           3 |         4
-  4 | {D}   |             |          
-  5 | {X}   |             |          
-(5 rows)
-
--- Both branches B+ C and B+ D are absorbable (B+ unbounded first)
--- B+ D branch matches: 3 matches (1-4, 2-4, 3-4)
--- Test absorption 4: Non-absorbable pattern (A B+ - unbounded not first)
-WITH test_no_absorb AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['B']),
-        (4, ARRAY['B']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_no_absorb
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A B+)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         4
-  2 | {B}   |             |          
-  3 | {B}   |             |          
-  4 | {B}   |             |          
-  5 | {X}   |             |          
-(5 rows)
-
--- Pattern A B+ is NOT absorbable (A bounded first, B+ unbounded but not first)
--- Only Row 1 can start match (only row with A), so only one match: 1-4
--- Test absorption 5: GROUP merge enables absorption
-WITH test_absorb_group AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['A']),
-        (4, ARRAY['B']),
-        (5, ARRAY['A']),
-        (6, ARRAY['B']),
-        (7, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_absorb_group
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN ((A B) (A B)+)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         6
-  2 | {B}   |             |          
-  3 | {A}   |           3 |         6
-  4 | {B}   |             |          
-  5 | {A}   |             |          
-  6 | {B}   |             |          
-  7 | {X}   |             |          
-(7 rows)
-
--- Pattern optimized: (A B) (A B)+ -> (A B){2,}
--- 2 matches: 1-6 (3 reps), 3-6 (2 reps)
--- Test absorption 6: Multiple unbounded - first element unbounded enables absorption
-WITH test_multi_unbounded AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['B']),
-        (4, ARRAY['B']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_multi_unbounded
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A+ B+)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         4
-  2 | {A}   |           2 |         4
-  3 | {B}   |             |          
-  4 | {B}   |             |          
-  5 | {X}   |             |          
-(5 rows)
-
--- 2 matches: 1-4, 2-4 (same endpoint 4)
--- ============================================
--- Jacob's RPR Patterns (from jacob branch)
--- ============================================
--- Test: A? (optional, greedy)
-WITH jacob_optional AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_optional
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A?)
-    DEFINE A AS 'A' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         1
-  2 | {X}   |             |          
-(2 rows)
-
--- Expected: 1-1 (matches A)
--- Test: A{2} (exact count)
-WITH jacob_exact AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_exact
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A{2})
-    DEFINE A AS 'A' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         2
-  2 | {A}   |             |          
-  3 | {A}   |             |          
-  4 | {X}   |             |          
-(4 rows)
-
--- Expected: 1-2
--- Test: A{1,3} (bounded range, greedy)
-WITH jacob_bounded AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['A']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_bounded
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A{1,3})
-    DEFINE A AS 'A' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         3
-  2 | {A}   |             |          
-  3 | {A}   |             |          
-  4 | {A}   |           4 |         4
-  5 | {X}   |             |          
-(5 rows)
-
--- Expected: 1-3 (greedy takes max), then 4-4
--- Test: A | B (simple alternation)
-WITH jacob_simple_alt AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_simple_alt
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A | B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         1
-  2 | {B}   |           2 |         2
-  3 | {X}   |             |          
-(3 rows)
-
--- Expected: 1-1 (A), 2-2 (B)
--- Test: A | B | C (three-way alternation)
-WITH jacob_three_alt AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['B']),
-        (2, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_three_alt
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A | B | C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {B}   |           1 |         1
-  2 | {X}   |             |          
-(2 rows)
-
--- Expected: 1-1 (B)
--- Test: A B C (concatenation)
-WITH jacob_concat AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_concat
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         3
-  2 | {B}   |             |          
-  3 | {C}   |             |          
-  4 | {X}   |             |          
-(4 rows)
-
--- Expected: 1-3
--- Test: A B? C (optional middle)
-WITH jacob_optional_mid AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['C']),
-        (3, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_optional_mid
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B? C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         2
-  2 | {C}   |             |          
-  3 | {X}   |             |          
-(3 rows)
-
--- Expected: 1-2 (A C, B skipped)
--- Test: (A B){2} (nested group with quantifier)
-WITH jacob_nested_group AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['A']),
-        (4, ARRAY['B']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_nested_group
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN ((A B){2})
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         4
-  2 | {B}   |             |          
-  3 | {A}   |             |          
-  4 | {B}   |             |          
-  5 | {X}   |             |          
-(5 rows)
-
--- Expected: 1-4 (A B A B)
--- Test: (A){3} (quantifier on grouped single element)
-WITH jacob_group_quant AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_group_quant
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN ((A){3})
-    DEFINE A AS 'A' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         3
-  2 | {A}   |             |          
-  3 | {A}   |             |          
-  4 | {X}   |             |          
-(4 rows)
-
--- Expected: 1-3
--- Test: A B C | A B C D E (lexical order - first alt wins)
-WITH jacob_lex_first AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_lex_first
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C | A B C D E)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         3
-  2 | {B}   |             |          
-  3 | {C}   |             |          
-  4 | {D}   |             |          
-  5 | {E}   |             |          
-(5 rows)
-
--- Expected: 1-3 (A B C wins by lexical order)
--- Test: A B C D E | A B C (lexical order - longer first wins)
-WITH jacob_lex_long AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_lex_long
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C D E | A B C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         5
-  2 | {B}   |             |          
-  3 | {C}   |             |          
-  4 | {D}   |             |          
-  5 | {E}   |             |          
-(5 rows)
-
--- Expected: 1-5 (A B C D E wins by lexical order)
--- ============================================
--- Alternation with quantifiers (BUG cases from Jacob's tests)
--- ============================================
--- Test: (A | B)+ C - alternation inside quantified group followed by C
-WITH jacob_alt_quant AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['A']),
-        (4, ARRAY['C'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_alt_quant
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN ((A | B)+ C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         4
-  2 | {B}   |             |          
-  3 | {A}   |             |          
-  4 | {C}   |             |          
-(4 rows)
-
--- Expected: 1-4 (A B A C)
--- Test: ((A | B) C)+ - alternation inside group with outer quantifier
-WITH jacob_alt_group AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['C']),
-        (3, ARRAY['B']),
-        (4, ARRAY['C']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_alt_group
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (((A | B) C)+)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A}   |           1 |         4
-  2 | {C}   |             |          
-  3 | {B}   |             |          
-  4 | {C}   |             |          
-  5 | {X}   |             |          
-(5 rows)
-
--- Expected: 1-4 (A C B C)
--- ============================================
--- RELUCTANT quantifiers
--- ============================================
--- Test: A+? B (reluctant) - B available at row 3, A exits early
-WITH jacob_reluctant AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A','_']),
-        (2, ARRAY['A','_']),
-        (3, ARRAY['A','B']),
-        (4, ARRAY['B','_'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_reluctant
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A+? B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A,_} |           1 |         3
-  2 | {A,_} |             |          
-  3 | {A,B} |             |          
-  4 | {B,_} |             |          
-(4 rows)
-
--- Test: A{1,3}? B (reluctant bounded) - same data, bounded quantifier
-WITH jacob_reluctant_bounded AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A','_']),
-        (2, ARRAY['A','_']),
-        (3, ARRAY['A','B']),
-        (4, ARRAY['B','_'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_reluctant_bounded
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A{1,3}? B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
- id | flags | match_start | match_end 
-----+-------+-------------+-----------
-  1 | {A,_} |           1 |         3
-  2 | {A,_} |             |          
-  3 | {A,B} |             |          
-  4 | {B,_} |             |          
-(4 rows)
-
--- ============================================
--- Nested quantifiers (pathological patterns)
--- ============================================
--- These patterns previously caused segfault or infinite loop.
--- Now they are either optimized at compile time or handled safely at runtime.
--- Test: (A*)* - nested unbounded quantifiers (optimized to A*)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A*)*)
-    DEFINE A AS TRUE
-);
- v | c 
----+---
- 1 | 5
- 2 | 0
- 3 | 0
- 4 | 0
- 5 | 0
-(5 rows)
-
--- Test: (A*)+ - inner nullable, outer requires one (optimized to A*)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A*)+)
-    DEFINE A AS TRUE
-);
- v | c 
----+---
- 1 | 5
- 2 | 0
- 3 | 0
- 4 | 0
- 5 | 0
-(5 rows)
-
--- Test: (A+)* - outer nullable (optimized to A*)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A+)*)
-    DEFINE A AS TRUE
-);
- v | c 
----+---
- 1 | 5
- 2 | 0
- 3 | 0
- 4 | 0
- 5 | 0
-(5 rows)
-
--- Test: (A+)+ - both require match (optimized to A+)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A+)+)
-    DEFINE A AS TRUE
-);
- v | c 
----+---
- 1 | 5
- 2 | 0
- 3 | 0
- 4 | 0
- 5 | 0
-(5 rows)
-
--- Test: (A* B*)* - complex nested pattern (runtime protection)
--- Not optimized but handled safely by empty-match loop prevention
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A* B*)*)
-    DEFINE A AS TRUE, B AS TRUE
-);
- v | c 
----+---
- 1 | 5
- 2 | 0
- 3 | 0
- 4 | 0
- 5 | 0
-(5 rows)
-
--- Test: (((A)*)*)*  - triple nested (optimized through recursive optimization)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 3) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((((A)*)*)*)
-    DEFINE A AS TRUE
-);
- v | c 
----+---
- 1 | 3
- 2 | 0
- 3 | 0
-(3 rows)
-
+DROP TABLE rpr_consec_null;
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index ab878443379..3383b242ef0 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1449,7 +1449,7 @@ ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A+ *)
                         ^
 -- Expected: ERROR: syntax error at or near "*"
--- ? ? (invalid combination)
+-- ? ? (parsed as ?? reluctant quantifier)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -2203,7 +2203,32 @@ SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
 (1 row)
 
 DROP VIEW rpr_serial_v8;
-DROP TABLE rpr_serial;
+-- Reluctant {1}? quantifier deparse through ruleutils
+CREATE VIEW rpr_quant_reluctant_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             INITIAL
+             PATTERN (A{1}? B)
+             DEFINE A AS val > 0, B AS val > 0);
+SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a{1}? b)                                                         +
+   DEFINE                                                                    +
+   a AS (val > 0),                                                           +
+   b AS (val > 0) );
+(1 row)
+
+DROP VIEW rpr_quant_reluctant_v;
 -- Materialized view (if supported)
 CREATE TABLE rpr_mview (id INT, val INT);
 INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
@@ -2251,6 +2276,52 @@ SELECT * FROM rpr_mview_v1 ORDER BY id;
 
 DROP MATERIALIZED VIEW rpr_mview_v1;
 DROP TABLE rpr_mview;
+-- CREATE TABLE AS SELECT with RPR
+CREATE TABLE rpr_ctas (id INT, val INT);
+INSERT INTO rpr_ctas VALUES (1, 10), (2, 20), (3, 15), (4, 25);
+CREATE TABLE rpr_ctas_result AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_ctas
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS val > PREV(val)
+);
+SELECT * FROM rpr_ctas_result ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+(4 rows)
+
+-- INSERT INTO ... SELECT with RPR
+CREATE TABLE rpr_insert_target (id INT, val INT, cnt BIGINT);
+INSERT INTO rpr_insert_target
+SELECT id, val, count(*) OVER w
+FROM rpr_ctas
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS val > PREV(val)
+);
+SELECT * FROM rpr_insert_target ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+(4 rows)
+
+DROP TABLE rpr_ctas_result;
+DROP TABLE rpr_insert_target;
+DROP TABLE rpr_ctas;
 -- Prepared statements (tests outfuncs.c / readfuncs.c)
 CREATE TABLE rpr_prep (id INT, val INT);
 INSERT INTO rpr_prep VALUES (1, 10), (2, 20), (3, 30);
@@ -2608,6 +2679,57 @@ SELECT pg_get_viewdef('rpr_multiwin_v'::regclass);
 
 DROP VIEW rpr_multiwin_v;
 DROP TABLE rpr_multiwin;
+-- {n} quantifier display in view
+CREATE VIEW rpr_quant_n_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             INITIAL
+             PATTERN (A{3})
+             DEFINE A AS val > 0);
+SELECT pg_get_viewdef('rpr_quant_n_v'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a{3})                                                            +
+   DEFINE                                                                    +
+   a AS (val > 0) );
+(1 row)
+
+DROP VIEW rpr_quant_n_v;
+-- {n,} quantifier display in view
+CREATE VIEW rpr_quant_n_plus_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             INITIAL
+             PATTERN (A{2,})
+             DEFINE A AS val > 0);
+SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a{2,})                                                           +
+   DEFINE                                                                    +
+   a AS (val > 0) );
+(1 row)
+
+DROP VIEW rpr_quant_n_plus_v;
+DROP TABLE rpr_serial;
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
@@ -2694,6 +2816,7 @@ WINDOW w AS (
 ERROR:  pattern variable qualified column reference "a.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS A.val > 0
                         ^
+-- Expected: ERROR: pattern variable qualified column reference "a.val" is not supported
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2706,6 +2829,7 @@ WINDOW w AS (
 ERROR:  pattern variable qualified column reference "b.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS B.val > 0
                         ^
+-- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2718,6 +2842,7 @@ WINDOW w AS (
 ERROR:  pattern variable qualified column reference "b.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                       ^
+-- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
 -- FROM-clause range variable qualified name: not allowed (prohibited by §6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2730,6 +2855,7 @@ WINDOW w AS (
 ERROR:  range variable qualified column reference "rpr_err.val" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS rpr_err.val > 0
                         ^
+-- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
 -- Semantic errors
 -- Undefined column in DEFINE
 SELECT COUNT(*) OVER w
@@ -2769,7 +2895,7 @@ WINDOW w AS (
 ERROR:  aggregate functions are not allowed in DEFINE
 LINE 7:     DEFINE A AS COUNT(*) > 0
                         ^
--- Expected: ERROR or works depending on implementation
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
 -- Subquery in DEFINE (NOT SUPPORTED)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3019,6 +3145,26 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
+-- Data execution: SEQ flatten produces correct results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP TO NEXT ROW
+             PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   0
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
 -- ALT flatten: (A | (B | C))+ -> (a | b | c)+
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -3049,6 +3195,26 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
+-- Data execution: ALT dedup produces correct results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP PAST LAST ROW
+             PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |  10
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   0
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
 -- Quantifier multiply: (A{2}){3} -> a{6}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -3468,6 +3634,26 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
+-- Data execution: GROUP unwrap produces correct results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP TO NEXT ROW
+             PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   1
+  2 |  20 |   1
+  3 |  30 |   1
+  4 |  40 |   1
+  5 |  50 |   1
+  6 |  60 |   1
+  7 |  70 |   1
+  8 |  80 |   1
+  9 |  90 |   1
+ 10 | 100 |   1
+(10 rows)
+
 -- Reluctant optimization bypass: VAR merge
 -- A+? A stays as a+? a (greedy A+ A merges to a{2,})
 EXPLAIN (COSTS OFF)
@@ -3612,6 +3798,134 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
+-- Duplicate GROUP removal: ((A | B)+ | (A | B)+) -> (a | b)+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A | B)+ | (A | B)+) DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b)+
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive VAR merge with zero-min: A* A+ -> a+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A* A+) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive VAR merge (4-element): A A{2} A+ A{3} -> a{7,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A A{2} A+ A{3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{7,}"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- PREFIX+SUFFIX merge (5-way): A B A B (A B)+ A B A B -> (a b){5,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B A B (A B)+ A B A B)
+             DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' b'){5,}"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Unwrap single-item ALT after dedup: (A | A)+ -> a+
+-- ALT dedup reduces to single-item, then GROUP unwrap
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A | A)+) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- GROUP{1,1} to SEQ with flatten: ((A B)(C D)) -> a b c d
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B)(C D)))
+             DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+                    C AS val > 50 AND val <= 75, D AS val > 75);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b c d
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested ALT pattern: ((A B) | C) D | A B C
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B) | C) D | A B C)
+             DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+                    C AS val > 50 AND val <= 75, D AS val > 75);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a b | c) d | a b c)
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested ALT with unbounded: ((A+ B) | C) D | A B C
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A+ B) | C) D | A B C)
+             DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+                    C AS val > 50 AND val <= 75, D AS val > 75);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a+" b | c) d | a b c)
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
 -- ============================================================
 -- Absorption Flag Display Tests
 -- ============================================================
@@ -3801,6 +4115,28 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
+-- Reluctant {1}? quantifier deparse
+-- A{1}? is a reluctant {1,1} quantifier.  The deparse code must
+-- output "{1}" explicitly to disambiguate from a bare "?" quantifier
+-- (which would mean {0,1}).
+EXPLAIN (COSTS OFF) SELECT count(*) OVER w
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY val
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1}? B)
+    DEFINE A AS val > 0, B AS val > 0
+);
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY val ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{1}? b
+   ->  Sort
+         Sort Key: val
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
 -- ============================================================
 -- Absorption Analysis Tests
 -- ============================================================
@@ -4529,7 +4865,8 @@ ORDER BY category;
 ERROR:  syntax error at or near "GROUP"
 LINE 12: GROUP BY category
          ^
--- Expected: ERROR (GROUP BY with window RPR not supported)
+-- Expected: ERROR: syntax error at or near "GROUP"
+-- (GROUP BY after WINDOW clause is not valid SQL syntax)
 -- ============================================================
 -- Subquery and CTE Tests
 -- Files: planner.c, prepjointree.c
@@ -5007,7 +5344,8 @@ CREATE TABLE rpr_sort (id INT, category VARCHAR(10), val INT);
 INSERT INTO rpr_sort VALUES
     (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
--- RPR with GROUP BY
+-- RPR with GROUP BY (aggregate in DEFINE → ERROR before GROUP BY interaction)
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
        MAX(val) as max_val,
@@ -5024,7 +5362,8 @@ ORDER BY category;
 ERROR:  aggregate functions are not allowed in DEFINE
 LINE 11:     DEFINE A AS COUNT(*) > 0
                          ^
--- RPR with HAVING
+-- RPR with HAVING (same aggregate-in-DEFINE error)
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
        COUNT(*) OVER w as window_cnt
@@ -5787,6 +6126,3 @@ FROM (SELECT id, val,
 (2 rows)
 
 DROP TABLE rpr_plan;
--- ============================================================
--- End of rpr_base.sql
--- ============================================================
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 340408cc454..817269021f4 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -6,8 +6,9 @@
 -- This test suite validates EXPLAIN output for RPR queries,
 -- including NFA statistics shown in EXPLAIN ANALYZE:
 --   - NFA States: peak, total, merged
---   - NFA Contexts: peak, total, absorbed, skipped
+--   - NFA Contexts: peak, total, pruned
 --   - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
+--   - NFA: absorbed (len min/max/avg), skipped (len min/max/avg)
 --   - Pattern deparse formatting
 --   - Multiple output formats (text, JSON, XML)
 --
@@ -434,7 +435,7 @@ WINDOW w AS (
 (9 rows)
 
 DROP VIEW rpr_v;
--- Grouped pattern with quantifier - state merging
+-- Grouped pattern with quantifier - state count with grouping
 CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
@@ -637,7 +638,7 @@ WINDOW w AS (
 (9 rows)
 
 DROP VIEW rpr_v;
--- High state merging - alternation with plus quantifier
+-- High state count - alternation with plus quantifier
 CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
@@ -758,7 +759,7 @@ WINDOW w AS (
 
 DROP VIEW rpr_v;
 -- ============================================================
--- Context Statistics Tests (peak, total, absorbed, skipped)
+-- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
 -- Context absorption with unbounded quantifier at start
 CREATE TEMP VIEW rpr_v AS
@@ -1047,7 +1048,7 @@ WINDOW w AS (
 (9 rows)
 
 DROP VIEW rpr_v;
--- Mix of short and long matches
+-- Uniform match length with mismatches from gap rows (v%20 = 11..15)
 CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
@@ -1094,8 +1095,8 @@ DROP VIEW rpr_v;
 -- ============================================================
 -- Mismatch Length Statistics Tests
 -- ============================================================
--- Pattern that causes mismatches with length > 1
--- Mismatch happens when partial match fails after processing multiple rows
+-- Pattern with complete match every cycle: 0 mismatched
+-- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched
 CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM (
@@ -1374,87 +1375,6 @@ WINDOW w AS (
  ]
 (1 row)
 
-DROP VIEW rpr_v;
--- ============================================================
--- XML Format Tests
--- ============================================================
--- XML format output
-CREATE TEMP VIEW rpr_v AS
-SELECT count(*) OVER w
-FROM generate_series(1, 30) AS s(v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B)
-    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
-       line       
-------------------
-   PATTERN (a b) 
-(1 row)
-
-SELECT rpr_explain_filter('
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
-SELECT count(*) OVER w
-FROM generate_series(1, 30) AS s(v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B)
-    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-)');
-                               rpr_explain_filter                               
---------------------------------------------------------------------------------
- <explain xmlns="http://www.postgresql.org/2009/explain">                      +
-   <Query>                                                                     +
-     <Plan>                                                                    +
-       <Node-Type>WindowAgg</Node-Type>                                        +
-       <Parallel-Aware>false</Parallel-Aware>                                  +
-       <Async-Capable>false</Async-Capable>                                    +
-       <Actual-Rows>30.00</Actual-Rows>                                        +
-       <Actual-Loops>1</Actual-Loops>                                          +
-       <Disabled>false</Disabled>                                              +
-       <Window>w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)</Window>+
-       <Pattern>a b</Pattern>                                                  +
-       <Storage>Memory</Storage>                                               +
-       <Maximum-Storage>0</Maximum-Storage>                                    +
-       <NFA-States-Peak>2</NFA-States-Peak>                                    +
-       <NFA-States-Total>31</NFA-States-Total>                                 +
-       <NFA-States-Merged>0</NFA-States-Merged>                                +
-       <NFA-Contexts-Peak>2</NFA-Contexts-Peak>                                +
-       <NFA-Contexts-Total>31</NFA-Contexts-Total>                             +
-       <NFA-Contexts-Absorbed>0</NFA-Contexts-Absorbed>                        +
-       <NFA-Contexts-Skipped>15</NFA-Contexts-Skipped>                         +
-       <NFA-Contexts-Pruned>0</NFA-Contexts-Pruned>                            +
-       <NFA-Matched>15</NFA-Matched>                                           +
-       <NFA-Mismatched>0</NFA-Mismatched>                                      +
-       <NFA-Match-Length-Min>2</NFA-Match-Length-Min>                          +
-       <NFA-Match-Length-Max>2</NFA-Match-Length-Max>                          +
-       <NFA-Match-Length-Avg>2.0</NFA-Match-Length-Avg>                        +
-       <NFA-Skipped-Length-Min>1</NFA-Skipped-Length-Min>                      +
-       <NFA-Skipped-Length-Max>1</NFA-Skipped-Length-Max>                      +
-       <NFA-Skipped-Length-Avg>1.0</NFA-Skipped-Length-Avg>                    +
-       <Plans>                                                                 +
-         <Plan>                                                                +
-           <Node-Type>Function Scan</Node-Type>                                +
-           <Parent-Relationship>Outer</Parent-Relationship>                    +
-           <Parallel-Aware>false</Parallel-Aware>                              +
-           <Async-Capable>false</Async-Capable>                                +
-           <Function-Name>generate_series</Function-Name>                      +
-           <Alias>s</Alias>                                                    +
-           <Actual-Rows>30.00</Actual-Rows>                                    +
-           <Actual-Loops>1</Actual-Loops>                                      +
-           <Disabled>false</Disabled>                                          +
-         </Plan>                                                               +
-       </Plans>                                                                +
-     </Plan>                                                                   +
-     <Triggers>                                                                +
-     </Triggers>                                                               +
-   </Query>                                                                    +
- </explain>
-(1 row)
-
 DROP VIEW rpr_v;
 -- JSON format with mismatch statistics
 -- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
@@ -1618,6 +1538,87 @@ WINDOW w AS (
  ]
 (1 row)
 
+DROP VIEW rpr_v;
+-- ============================================================
+-- XML Format Tests
+-- ============================================================
+-- XML format output
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line       
+------------------
+   PATTERN (a b) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+)');
+                               rpr_explain_filter                               
+--------------------------------------------------------------------------------
+ <explain xmlns="http://www.postgresql.org/2009/explain">                      +
+   <Query>                                                                     +
+     <Plan>                                                                    +
+       <Node-Type>WindowAgg</Node-Type>                                        +
+       <Parallel-Aware>false</Parallel-Aware>                                  +
+       <Async-Capable>false</Async-Capable>                                    +
+       <Actual-Rows>30.00</Actual-Rows>                                        +
+       <Actual-Loops>1</Actual-Loops>                                          +
+       <Disabled>false</Disabled>                                              +
+       <Window>w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)</Window>+
+       <Pattern>a b</Pattern>                                                  +
+       <Storage>Memory</Storage>                                               +
+       <Maximum-Storage>0</Maximum-Storage>                                    +
+       <NFA-States-Peak>2</NFA-States-Peak>                                    +
+       <NFA-States-Total>31</NFA-States-Total>                                 +
+       <NFA-States-Merged>0</NFA-States-Merged>                                +
+       <NFA-Contexts-Peak>2</NFA-Contexts-Peak>                                +
+       <NFA-Contexts-Total>31</NFA-Contexts-Total>                             +
+       <NFA-Contexts-Absorbed>0</NFA-Contexts-Absorbed>                        +
+       <NFA-Contexts-Skipped>15</NFA-Contexts-Skipped>                         +
+       <NFA-Contexts-Pruned>0</NFA-Contexts-Pruned>                            +
+       <NFA-Matched>15</NFA-Matched>                                           +
+       <NFA-Mismatched>0</NFA-Mismatched>                                      +
+       <NFA-Match-Length-Min>2</NFA-Match-Length-Min>                          +
+       <NFA-Match-Length-Max>2</NFA-Match-Length-Max>                          +
+       <NFA-Match-Length-Avg>2.0</NFA-Match-Length-Avg>                        +
+       <NFA-Skipped-Length-Min>1</NFA-Skipped-Length-Min>                      +
+       <NFA-Skipped-Length-Max>1</NFA-Skipped-Length-Max>                      +
+       <NFA-Skipped-Length-Avg>1.0</NFA-Skipped-Length-Avg>                    +
+       <Plans>                                                                 +
+         <Plan>                                                                +
+           <Node-Type>Function Scan</Node-Type>                                +
+           <Parent-Relationship>Outer</Parent-Relationship>                    +
+           <Parallel-Aware>false</Parallel-Aware>                              +
+           <Async-Capable>false</Async-Capable>                                +
+           <Function-Name>generate_series</Function-Name>                      +
+           <Alias>s</Alias>                                                    +
+           <Actual-Rows>30.00</Actual-Rows>                                    +
+           <Actual-Loops>1</Actual-Loops>                                      +
+           <Disabled>false</Disabled>                                          +
+         </Plan>                                                               +
+       </Plans>                                                                +
+     </Plan>                                                                   +
+     <Triggers>                                                                +
+     </Triggers>                                                               +
+   </Query>                                                                    +
+ </explain>
+(1 row)
+
 DROP VIEW rpr_v;
 -- ============================================================
 -- Multiple Partitions Tests
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 03ee174d359..c8464e8fcf4 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -322,6 +322,161 @@ WINDOW w AS (
   5 | {_}   |             |          
 (5 rows)
 
+-- Absorption with fixed suffix: A+ B
+WITH test_absorb_suffix AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_suffix
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {A}   |           2 |         4
+  3 | {A}   |           3 |         4
+  4 | {B}   |             |          
+  5 | {X}   |             |          
+(5 rows)
+
+-- Per-branch absorption with ALT: B+ C | B+ D
+WITH test_absorb_alt AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['B']),
+        (3, ARRAY['B']),
+        (4, ARRAY['D']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_alt
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (B+ C | B+ D)
+    DEFINE
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         4
+  2 | {B}   |           2 |         4
+  3 | {B}   |           3 |         4
+  4 | {D}   |             |          
+  5 | {X}   |             |          
+(5 rows)
+
+-- Non-absorbable: A B+ (unbounded not in first position)
+WITH test_no_absorb AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['B']),
+        (4, ARRAY['B']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_no_absorb
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {B}   |             |          
+  3 | {B}   |             |          
+  4 | {B}   |             |          
+  5 | {X}   |             |          
+(5 rows)
+
+-- GROUP merge enables absorption: (A B) (A B)+ optimized to (A B){2,}
+WITH test_absorb_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A B) (A B)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         6
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         6
+  4 | {B}   |             |          
+  5 | {A}   |             |          
+  6 | {B}   |             |          
+  7 | {X}   |             |          
+(7 rows)
+
+-- Multiple unbounded: A+ B+ (first element unbounded enables absorption)
+WITH test_multi_unbounded AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['B']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_multi_unbounded
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {A}   |           2 |         4
+  3 | {B}   |             |          
+  4 | {B}   |             |          
+  5 | {X}   |             |          
+(5 rows)
+
 -- ============================================================
 -- Context Lifecycle
 -- ============================================================
@@ -674,7 +829,8 @@ WINDOW w AS (
 (5 rows)
 
 -- Optional reluctant group: (A B)?? C
--- nfa_advance_begin: reluctant prefers skip (0 iterations) over enter
+-- nfa_advance_begin: reluctant tries skip first, but skip path needs C
+-- at row 1 which is A → skip fails. Enter path succeeds: A(1) B(2) C(3).
 WITH test_optional_reluctant AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -734,6 +890,44 @@ WINDOW w AS (
   4 | {B}   |             |          
 (4 rows)
 
+-- Reluctant optional group skip-to-FIN
+-- When a reluctant optional group's skip path reaches FIN, the group
+-- entry path is abandoned (nodeWindowAgg.c nfa_advance_begin).
+-- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)??
+-- prefers to skip.  Skip goes to FIN (group is last element), so
+-- the match completes with just C.
+WITH test_begin_skip_fin AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['C']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['C','A']),
+        (5, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_begin_skip_fin
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (C (A B)??)
+    DEFINE
+        C AS 'C' = ANY(flags),
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {C}   |           1 |         1
+  2 | {A}   |             |          
+  3 | {B}   |             |          
+  4 | {C,A} |           4 |         4
+  5 | {B}   |             |          
+(5 rows)
+
 -- ============================================================
 -- Match Phase
 -- ============================================================
@@ -1664,6 +1858,64 @@ WINDOW w AS (
   4 | {B}   |             |          
 (4 rows)
 
+-- A+? B (reluctant plus): exits A at first B availability
+-- (Same scenario as greedy-vs-reluctant comparison above; retained for
+-- standalone quantifier coverage alongside A{1,3}? and A{2,3}? below)
+WITH test_reluctant_plus AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','_']),
+        (2, ARRAY['A','_']),
+        (3, ARRAY['A','B']),
+        (4, ARRAY['B','_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_reluctant_plus
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A,_} |           1 |         3
+  2 | {A,_} |             |          
+  3 | {A,B} |             |          
+  4 | {B,_} |             |          
+(4 rows)
+
+-- A{1,3}? B (reluctant bounded): same data, bounded quantifier
+WITH test_reluctant_bounded AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','_']),
+        (2, ARRAY['A','_']),
+        (3, ARRAY['A','B']),
+        (4, ARRAY['B','_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_reluctant_bounded
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{1,3}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A,_} |           1 |         3
+  2 | {A,_} |             |          
+  3 | {A,B} |             |          
+  4 | {B,_} |             |          
+(4 rows)
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
@@ -2030,6 +2282,283 @@ WINDOW w AS (
   3 | {B}   |           3 |         3
 (3 rows)
 
+-- Overlapping match: A B C D E | B C D | C D E F (SKIP PAST LAST ROW)
+WITH test_overlap1 AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['E']),
+        (6, ARRAY['F'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E | B C D | C D E F)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags),
+        F AS 'F' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         5
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {E}   |             |          
+  6 | {F}   |             |          
+(6 rows)
+
+-- Same with SKIP TO NEXT ROW: three overlapping matches
+WITH test_overlap1 AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['E']),
+        (6, ARRAY['F'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B C D E | B C D | C D E F)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags),
+        F AS 'F' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         5
+  2 | {B}   |           2 |         4
+  3 | {C}   |           3 |         6
+  4 | {D}   |             |          
+  5 | {E}   |             |          
+  6 | {F}   |             |          
+(6 rows)
+
+-- Longer pattern fails, shorter survives: A+ B C D E | B+ C
+WITH test_overlap1b AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1b
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B C D E | B+ C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |             |          
+  2 | {B}   |           2 |         3
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {X}   |             |          
+(5 rows)
+
+-- Long B sequence with different endings: A B+ C | B+ D
+WITH test_overlap2 AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),
+        (2,  ARRAY['B']),
+        (3,  ARRAY['B']),
+        (4,  ARRAY['B']),
+        (5,  ARRAY['B']),
+        (6,  ARRAY['C']),
+        (7,  ARRAY['B']),
+        (8,  ARRAY['B']),
+        (9,  ARRAY['B']),
+        (10, ARRAY['D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap2
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B+ C | B+ D)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         6
+  2 | {B}   |             |          
+  3 | {B}   |             |          
+  4 | {B}   |             |          
+  5 | {B}   |             |          
+  6 | {C}   |             |          
+  7 | {B}   |           7 |        10
+  8 | {B}   |           8 |        10
+  9 | {B}   |           9 |        10
+ 10 | {D}   |             |          
+(10 rows)
+
+-- Greedy with late failure ("betrayal"): A B C+ D | A B
+WITH test_betrayal AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['C']),
+        (5, ARRAY['C']),
+        (6, ARRAY['E'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_betrayal
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C+ D | A B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         2
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {C}   |             |          
+  5 | {C}   |             |          
+  6 | {E}   |             |          
+(6 rows)
+
+-- Multiple TRUE per row: overlapping pattern variables
+WITH test_multi_true AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['B','C']),
+        (3, ARRAY['C','D']),
+        (4, ARRAY['D','E']),
+        (5, ARRAY['E','_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_multi_true
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A,B} |           1 |         5
+  2 | {B,C} |             |          
+  3 | {C,D} |             |          
+  4 | {D,E} |             |          
+  5 | {E,_} |             |          
+(5 rows)
+
+-- Diagonal pattern with shifted multi-TRUE overlap
+WITH test_diagonal AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','_']),
+        (2, ARRAY['B','A']),
+        (3, ARRAY['C','B']),
+        (4, ARRAY['D','C']),
+        (5, ARRAY['_','D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_diagonal
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B C D)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A,_} |           1 |         4
+  2 | {B,A} |           2 |         5
+  3 | {C,B} |             |          
+  4 | {D,C} |             |          
+  5 | {_,D} |             |          
+(5 rows)
+
+-- ((A | B) C)+ - alternation inside group with outer quantifier
+WITH test_alt_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['C']),
+        (3, ARRAY['B']),
+        (4, ARRAY['C']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_alt_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A | B) C)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {C}   |             |          
+  3 | {B}   |             |          
+  4 | {C}   |             |          
+  5 | {X}   |             |          
+(5 rows)
+
 -- ============================================================
 -- Deep Nested Groups
 -- ============================================================
@@ -2227,6 +2756,69 @@ WINDOW w AS (
   5 | {C}   |             |          
 (5 rows)
 
+-- (A B){2} - group with exact quantifier
+WITH test_group_exact AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_group_exact
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B){2})
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {B}   |             |          
+  3 | {A}   |             |          
+  4 | {B}   |             |          
+  5 | {X}   |             |          
+(5 rows)
+
+-- Nested END->END fast-forward
+-- When an inner group has a nullable body and count < min, the
+-- fast-forward path exits through the outer END, incrementing
+-- the outer group's count (nodeWindowAgg.c nfa_advance_end).
+-- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses
+-- because the optimizer cannot safely multiply non-exact quantifiers.
+-- Data has no A rows, forcing all-empty iterations via fast-forward.
+WITH test_nested_ff AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['B']),
+        (3, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nested_ff
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A?){2,3}){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |             |          
+  2 | {B}   |             |          
+  3 | {B}   |             |          
+(3 rows)
+
 -- ============================================================
 -- SKIP Options (Runtime)
 -- ============================================================
@@ -2390,6 +2982,8 @@ ORDER BY mode, id;
 
 -- ============================================================
 -- INITIAL Mode (Runtime)
+-- Placeholder: INITIAL is not yet implemented (syntax error).
+-- Kept here so tests convert to runtime tests when implemented.
 -- ============================================================
 -- INITIAL mode (not yet supported - produces syntax error)
 WITH test_initial_mode AS (
@@ -2597,6 +3191,80 @@ WINDOW w AS (
   5 | {B}   |             |          
 (5 rows)
 
+-- N FOLLOWING + SKIP TO NEXT ROW: overlapping matches bounded by frame
+-- Row 1: frame [1,4], A(1-3) B(4) -> match
+-- Row 2: frame [2,5], A(2-3) B(4) -> match
+-- Row 3: frame [3,6], A(3) B(4) -> match
+-- Row 5: frame [5,6], A(5) B(6) -> match
+WITH test_n_skip_next AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_n_skip_next
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {A}   |           2 |         4
+  3 | {A}   |           3 |         4
+  4 | {B}   |             |          
+  5 | {A}   |           5 |         6
+  6 | {B}   |             |          
+(6 rows)
+
+-- Frame exactly 1 row short of potential match
+-- From row 1: A A A B needs 4 rows but frame holds 3 -> no match
+-- From row 2: A A B fits in 3-row frame -> match
+WITH test_frame_one_short AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_frame_one_short
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |             |          
+  2 | {A}   |           2 |         4
+  3 | {A}   |           3 |         4
+  4 | {B}   |             |          
+  5 | {A}   |           5 |         6
+  6 | {B}   |             |          
+(6 rows)
+
 -- ============================================================
 -- Special Partition Cases
 -- ============================================================
@@ -3407,7 +4075,8 @@ WINDOW w AS (
 
 -- (A?){0,3}: min=0, nullable inner.
 -- A never matches. A? matches empty, min=0 satisfied immediately.
--- Expected: empty match (match_start = match_end) for every row
+-- Per standard: empty match expected for every row.
+-- XXX: visited bitmap blocks empty iteration → no match (same as {2,3})
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3436,7 +4105,8 @@ WINDOW w AS (
 
 -- (A?){1,3}: min=1, nullable inner.
 -- A never matches. Need 1 empty iteration to satisfy min=1.
--- Expected: empty match for every row
+-- Per standard: empty match expected for every row.
+-- XXX: visited bitmap blocks empty iteration → no match (same as {2,3})
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3497,7 +4167,8 @@ WINDOW w AS (
 -- (A?){2,3} mixed: some rows match A, some don't
 -- Rows 1-2: A matches, greedy takes 2 → min satisfied
 -- Row 3: A doesn't match, needs 2 empty iterations for min=2
--- Expected: all rows produce matches
+-- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
+-- Row 4: A matches 1 real iter + 1 ff empty exit → match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index a263074e3e9..8dbd3f9036a 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1,5 +1,10 @@
 --
--- Test for row pattern definition clause
+-- Test for row pattern recognition: WINDOW clause integration and
+-- real-world scenario tests using stock price data.
+--
+-- Parser/planner tests: rpr_base.sql
+-- NFA engine tests: rpr_nfa.sql
+-- EXPLAIN statistics tests: rpr_explain.sql
 --
 
 CREATE TEMP TABLE stock (
@@ -30,6 +35,10 @@ INSERT INTO stock VALUES ('company2', '2023-07-10', 1300);
 
 SELECT * FROM stock;
 
+--
+-- Basic pattern matching with PREV/NEXT
+--
+
 -- basic test using PREV
 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
  nth_value(tdate, 2) OVER w AS nth_second
@@ -175,7 +184,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
   HIGH AS price > 150
 );
 
--- basic test with none-greedy pattern
+-- basic test with fixed-length pattern (A A A = exactly 3)
 SELECT company, tdate, price, count(*) OVER w
  FROM stock
  WINDOW w AS (
@@ -294,7 +303,9 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
   UPDOWN AS price > PREV(price) AND price > NEXT(price)
 );
 
--- using AFTER MATCH SKIP TO NEXT ROW
+-- using AFTER MATCH SKIP TO NEXT ROW (same pattern as above;
+-- match length is always 2, so result is identical to SKIP PAST LAST ROW.
+-- SKIP TO NEXT ROW's distinct effect is tested in backtracking section.)
 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
  FROM stock
  WINDOW w AS (
@@ -308,8 +319,92 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
   UPDOWN AS price > PREV(price) AND price > NEXT(price)
 );
 
--- match everything
+-- PREV returns NULL at partition's first row (null_slot path)
+SELECT company, tdate, price, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (BOUNDARY REST+)
+ DEFINE
+  BOUNDARY AS PREV(price) IS NULL,
+  REST AS PREV(price) IS NOT NULL
+);
+
+-- NEXT returns NULL at partition's last row (null_slot path)
+SELECT company, tdate, price, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ BOUNDARY)
+ DEFINE
+  A AS NEXT(price) IS NOT NULL,
+  BOUNDARY AS NEXT(price) IS NULL
+);
+
+-- DESC order: PREV refers to the row with later date
+SELECT company, tdate, price, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate DESC
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START DOWN+ UP+)
+ DEFINE
+  START AS TRUE,
+  DOWN AS price < PREV(price),
+  UP AS price > PREV(price)
+);
+
+-- Multiple partitions with unequal sizes
+WITH multi_part AS (
+ SELECT * FROM (VALUES
+  ('a', 1, 10), ('a', 2, 20), ('a', 3, 15),
+  ('b', 1, 5),
+  ('c', 1, 100), ('c', 2, 200), ('c', 3, 150), ('c', 4, 140), ('c', 5, 300)
+ ) AS t(grp, id, val)
+)
+SELECT grp, id, val, count(*) OVER w
+FROM multi_part
+WINDOW w AS (
+ PARTITION BY grp
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+  A AS val <= NEXT(val),
+  B AS val > PREV(val) OR val < PREV(val)
+);
+
+-- FLOAT/NUMERIC DEFINE conditions
+WITH float_data AS (
+ SELECT * FROM (VALUES
+  (1, 1.0::float8), (2, 1.5), (3, 1.4999), (4, 1.50001), (5, 0.1)
+ ) AS t(id, val)
+)
+SELECT id, val, count(*) OVER w
+FROM float_data
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+  A AS TRUE,
+  B AS val > PREV(val) * 0.99
+);
+
+--
+-- SKIP TO / Backtracking / Frame boundary
+--
 
+-- match everything
 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
  FROM stock
  WINDOW w AS (
@@ -474,6 +569,25 @@ UP AS price > PREV(price),
 DOWN AS price < PREV(price)
 );
 
+-- row_number() within RPR reduced frame
+SELECT company, tdate, price, row_number() OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+
+--
+-- SQL Integration: JOIN, CTE, LATERAL
+--
+
 -- JOIN case
 CREATE TEMP TABLE t1 (i int, v1 int);
 CREATE TEMP TABLE t2 (j int, v2 int);
@@ -552,6 +666,10 @@ SELECT id, i, j, count(*) OVER w
   COND AS PREV(i + j + 1) < 10
 );
 
+--
+-- Large-scale / scalability tests
+--
+
 -- Smoke test for larger partitions.
 WITH s AS (
  SELECT v, count(*) OVER w AS c
@@ -607,8 +725,9 @@ result AS (
 SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
 
 --
--- Using IGNORE NULLS
+-- IGNORE NULLS
 --
+
 -- no NULL rows case. The result should be identical with "basic test using PREV"
 SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w,
  last_value(price) IGNORE NULLS OVER w,
@@ -626,7 +745,7 @@ SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w,
 );
 
 -- nth_value with IGNORE NULLS option wants to find the second row but
--- due a NULL in the midlle, it returns the third row.
+-- due to a NULL in the middle, it returns the third row.
 WITH data AS (
  SELECT * FROM (VALUES
   (10, 1), (11, NULL), (12, 3), (13, 4)
@@ -643,8 +762,8 @@ WITH data AS (
   );
 
 -- nth_value with IGNORE NULLS option wants to find the third row but
--- due a NULL in the midlle, it reaches the end of reduced frame and
--- return NULL
+-- due to a NULL in the middle, it reaches the end of reduced frame and
+-- returns NULL
 WITH data AS (
  SELECT * FROM (VALUES
   (10, 1), (11, NULL), (12, 3), (13, 4)
@@ -677,1503 +796,115 @@ WINDOW w AS (
   DOWN AS price < PREV(price)
 );
 
--- View and pg_get_viewdef tests.
-CREATE TEMP VIEW v_window AS
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
- nth_value(tdate, 2) OVER w AS nth_second
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price)
-);
-
-SELECT * FROM v_window;
-SELECT pg_get_viewdef('v_window');
-
---
--- Pattern optimization tests
--- VIEW shows original pattern, EXPLAIN shows optimized pattern
---
-
--- Test: duplicate alternatives removal (A | B | A)+ -> (A | B)+
-CREATE TEMP VIEW v_opt_dup AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A | B | A)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_dup');  -- original: ((a | b | a)+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup;  -- optimized: ((a | b)+)
-
--- Test: duplicate group removal ((A | B)+ | (A | B)+) -> (A | B)+
-CREATE TEMP VIEW v_opt_dup_group AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A | B)+ | (A | B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_dup_group');  -- original: ((a | b)+ | (a | b)+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup_group;  -- optimized: ((a | b)+)
-
--- Test: consecutive vars merge (A A A) -> A{3}
-CREATE TEMP VIEW v_opt_merge AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A A A)
- DEFINE
-  A AS price >= 140 AND price <= 150
-);
-SELECT pg_get_viewdef('v_opt_merge');  -- original: (a a a)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge;  -- optimized: a{3}
-
--- Test: quantified vars merge (A A+ A) -> A{3,}
-CREATE TEMP VIEW v_opt_merge_quant AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A A+ A)
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_merge_quant');  -- original: (a a+ a)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_quant;  -- optimized: a{3,}
-
--- Test: merge two unbounded (A+ A+) -> A{2,}
-CREATE TEMP VIEW v_opt_merge_unbounded AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
+-- IGNORE NULLS + first_value where first value in reduced frame is NULL
+WITH data AS (
+ SELECT * FROM (VALUES
+  (1, NULL), (2, NULL), (3, 30), (4, 40)
+ ) AS t(id, val))
+SELECT id, val,
+ first_value(val) IGNORE NULLS OVER w AS fv_ignull,
+ count(*) OVER w
+FROM data
+WINDOW w AS (
+ ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A+ A+)
- DEFINE
-  A AS price > 100
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
 );
-SELECT pg_get_viewdef('v_opt_merge_unbounded');  -- original: (a+ a+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_unbounded;  -- optimized: a{2,}
 
--- Test: merge with zero-min (A* A+) -> A+
-CREATE TEMP VIEW v_opt_merge_star AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
+-- IGNORE NULLS + all values NULL in reduced frame
+WITH data AS (
+ SELECT * FROM (VALUES
+  (1, NULL), (2, NULL), (3, NULL)
+ ) AS t(id, val))
+SELECT id, val,
+ first_value(val) IGNORE NULLS OVER w AS fv_ignull,
+ last_value(val) IGNORE NULLS OVER w AS lv_ignull,
+ count(*) OVER w
+FROM data
+WINDOW w AS (
+ ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A* A+)
- DEFINE
-  A AS price > 100
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
 );
-SELECT pg_get_viewdef('v_opt_merge_star');  -- original: (a* a+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_star;  -- optimized: a+
 
--- Test: complex merge (A A{2} A+ A{3}) -> A{7,}
-CREATE TEMP VIEW v_opt_merge_complex AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A A{2} A+ A{3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_merge_complex');  -- original: (a a{2} a+ a{3})
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_complex;  -- optimized: a{7,}
+--
+-- last_value IGNORE NULLS with reduced frame containing all NULLs
+-- Exercises ignorenulls_getfuncarginframe SEEK_TAIL out-of-frame path
+-- when notnull_relpos >= num_reduced_frame.
+--
+CREATE TEMP TABLE rpr_nullval (id INT, val INT);
+INSERT INTO rpr_nullval VALUES (1, 10), (2, NULL), (3, NULL), (4, 20);
 
--- Test: group merge ((A B) (A B)+) -> (A B){2,}
-CREATE TEMP VIEW v_opt_merge_group AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A B) (A B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
+SELECT id, val,
+       last_value(val) IGNORE NULLS OVER w AS lv_ignull,
+       count(*) OVER w AS cnt
+FROM rpr_nullval
+WINDOW w AS (
+  ORDER BY id
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  PATTERN (A B+)
+  DEFINE
+    A AS val IS NOT NULL,
+    B AS val IS NULL
 );
-SELECT pg_get_viewdef('v_opt_merge_group');  -- original: ((a b) (a b)+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group;  -- expected: (a b){2,}
 
--- Test: group merge A B (A B)+ -> (A B){2,}
-CREATE TEMP VIEW v_opt_merge_group2 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A B (A B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_merge_group2');  -- original: (a b (a b)+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group2;  -- expected: (a b){2,}
+--
+-- NULL handling
+--
 
--- Test: group merge (A B) (A B)+ (A B) -> (A B){3,}
-CREATE TEMP VIEW v_opt_merge_group3 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A B) (A B)+ (A B))
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_merge_group3');  -- original: ((a b) (a b)+ (a b))
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group3;  -- expected: (a b){3,}
+CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER);
+INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100);
+INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL);  -- NULL in middle
+INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200);
+INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150);
 
--- Test: group merge A B A B (A B)+ A B A B -> (A B){5,}
-CREATE TEMP VIEW v_opt_merge_group4 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A B A B (A B)+ A B A B)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
+SELECT company, tdate, price, count(*) OVER w AS match_count
+FROM stock_null
+WINDOW w AS (
+  PARTITION BY company
+  ORDER BY tdate
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  PATTERN (START UP DOWN)
+  DEFINE START AS TRUE, UP AS price > PREV(price), DOWN AS price <
+PREV(price)
 );
-SELECT pg_get_viewdef('v_opt_merge_group4');  -- original: (a b a b (a b)+ a b a b)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group4;  -- expected: (a b){5,}
 
--- Test: group merge C A B (A B)+ A B C -> C (A B){3,} C
-CREATE TEMP VIEW v_opt_merge_group5 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (C A B (A B)+ A B C)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100,
-  C AS price > 200
-);
-SELECT pg_get_viewdef('v_opt_merge_group5');  -- original: (c a b (a b)+ a b c)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group5;  -- expected: c (a b){3,} c
+-- Consecutive NULLs: PREV navigates through NULL values
+CREATE TEMP TABLE rpr_consec_null (id INT, val INT);
+INSERT INTO rpr_consec_null VALUES
+ (1, 100), (2, NULL), (3, NULL), (4, NULL), (5, 200), (6, 300);
 
--- Test: consecutive GROUP merge (A B)+ (A B)+ -> (A B){2,}
-CREATE TEMP VIEW v_opt_merge_consec_group AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
+-- PREV(val) IS NULL succeeds for both null_slot (first row) and actual NULL
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_consec_null
+WINDOW w AS (
+ ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A B)+ (A B)+)
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+ C)
  DEFINE
-  A AS price > 100,
-  B AS price <= 100
+  A AS val IS NULL,
+  B AS val IS NULL AND PREV(val) IS NULL,
+  C AS val IS NOT NULL
 );
-SELECT pg_get_viewdef('v_opt_merge_consec_group');  -- original: ((a b)+ (a b)+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_consec_group;  -- expected: (a b){2,}
 
--- Test: consecutive GROUP merge with different quantifiers (A B){2} (A B){3} -> (A B){5}
-CREATE TEMP VIEW v_opt_merge_consec_group2 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
+-- NEXT(val) through consecutive NULLs
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_consec_null
+WINDOW w AS (
+ ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A B){2} (A B){3})
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+ C)
  DEFINE
-  A AS price > 100,
-  B AS price <= 100
+  A AS val IS NOT NULL,
+  B AS val IS NULL AND NEXT(val) IS NULL,
+  C AS val IS NULL AND NEXT(val) IS NOT NULL
 );
-SELECT pg_get_viewdef('v_opt_merge_consec_group2');  -- original: ((a b){2} (a b){3})
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_consec_group2;  -- expected: (a b){5}
 
--- Test {n} quantifier display
-CREATE TEMP VIEW v_quantifier_n AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A{3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_quantifier_n');
-
--- Test {n,} quantifier display
-CREATE TEMP VIEW v_quantifier_n_plus AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A{2,})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_quantifier_n_plus');
-
--- Test: flatten nested SEQ (A (B C)) -> A B C
-CREATE TEMP VIEW v_opt_flatten_seq AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A (B C))
- DEFINE
-  A AS price > 100,
-  B AS price > 150,
-  C AS price < 150
-);
-SELECT pg_get_viewdef('v_opt_flatten_seq');  -- original: (a (b c))
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_seq;  -- optimized: a b c
-
--- Test: flatten nested ALT (A | (B | C)) -> (A | B | C)
-CREATE TEMP VIEW v_opt_flatten_alt AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A | (B | C))+)
- DEFINE
-  A AS price > 200,
-  B AS price > 100,
-  C AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_flatten_alt');  -- original: ((a | (b | c))+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_alt;  -- optimized: ((a | b | c))+
-
--- Test: unwrap GROUP{1,1} ((A)) -> A
-CREATE TEMP VIEW v_opt_unwrap_group AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (((A)))
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_unwrap_group');  -- original: (((a)))
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_group;  -- optimized: a
-
--- Test: quantifier multiplication (A{2}){3} -> A{6}
-CREATE TEMP VIEW v_opt_quant_mult AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A{2}){3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_quant_mult');  -- original: ((a{2}){3})
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult;  -- optimized: a{6}
-
--- Test: quantifier multiplication (A{2,4}){3} -> A{6,12}
-CREATE TEMP VIEW v_opt_quant_mult_range AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A{2,4}){3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_quant_mult_range');  -- original: ((a{2,4}){3})
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range;  -- optimized: a{6,12}
-
--- Test: quantifier multiplication blocked (A{2}){3,5} -> no change
--- outer range with child exact > 1 causes gaps (6,8,10 not 6,7,8,9,10)
-CREATE TEMP VIEW v_opt_quant_mult_range2 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A{2}){3,5})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_quant_mult_range2');  -- original: ((a{2}){3,5})
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range2;  -- NOT optimized: (a{2}){3,5}
-
--- Test: quantifier multiplication blocked by INF (A+){3} -> no change
-CREATE TEMP VIEW v_opt_quant_mult_inf AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A+){3})
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_quant_mult_inf');  -- original: ((a+){3})
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_inf;  -- no multiply: (a+){3}
-
--- Test: unwrap single-item ALT after duplicate removal (A | A) -> A
-CREATE TEMP VIEW v_opt_unwrap_alt AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN ((A | A)+)
- DEFINE
-  A AS price > 100
-);
-SELECT pg_get_viewdef('v_opt_unwrap_alt');  -- original: ((a | a)+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_alt;  -- optimized: a+
-
--- Test: GROUP{1,1} to SEQ with flatten ((A B)(C D)) -> A B C D
-CREATE TEMP VIEW v_opt_group_to_seq AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (((A B)(C D)))
- DEFINE
-  A AS price > 200,
-  B AS price > 150,
-  C AS price > 100,
-  D AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_group_to_seq');  -- original: (((a b)(c d)))
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_group_to_seq;  -- optimized: a b c d
-
--- Test: combined consecutive GROUP + prefix merge A B (A B)+ (A B)+ -> (A B){3,}
-CREATE TEMP VIEW v_opt_combined_merge AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (A B (A B)+ (A B)+)
- DEFINE
-  A AS price > 100,
-  B AS price <= 100
-);
-SELECT pg_get_viewdef('v_opt_combined_merge');  -- original: (a b (a b)+ (a b)+)
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_combined_merge;  -- expected: (a b){3,}
-
--- Test: nested ALT pattern - bug reproduction
-CREATE TEMP VIEW v_opt_nested_alt AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (((A B) | C) D | A B C)
- DEFINE
-  A AS price <= 100,
-  B AS price <= 150,
-  C AS price <= 200,
-  D AS price > 200
-);
-SELECT pg_get_viewdef('v_opt_nested_alt');
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_nested_alt;
-
--- Test: nested ALT with unbounded - A+ inside
-CREATE TEMP VIEW v_opt_nested_alt2 AS
-SELECT company, tdate, price, count(*) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (((A+ B) | C) D | A B C)
- DEFINE
-  A AS price <= 100,
-  B AS price <= 150,
-  C AS price <= 200,
-  D AS price > 200
-);
-SELECT pg_get_viewdef('v_opt_nested_alt2');
-EXPLAIN (COSTS OFF) SELECT * FROM v_opt_nested_alt2;
-
---
--- Error cases
---
-
--- row pattern definition variable name must not appear more than once
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price),
-  UP AS price > PREV(price)
-);
-
--- subqueries in DEFINE clause are not supported
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START LOWPRICE)
- DEFINE
-  START AS TRUE,
-  LOWPRICE AS price < (SELECT 100)
-);
-
--- aggregates in DEFINE clause are not supported
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START LOWPRICE)
- DEFINE
-  START AS TRUE,
-  LOWPRICE AS price < count(*)
-);
-
--- FRAME must start at current row when row pattern recognition is used
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price)
-);
-
--- EXCLUDE is not permitted
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- EXCLUDE CURRENT ROW
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price)
-);
-
--- SEEK is not supported
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- AFTER MATCH SKIP TO NEXT ROW
- SEEK
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(price),
-  DOWN AS price < PREV(price)
-);
-
--- PREV's argument must have at least 1 column reference
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- AFTER MATCH SKIP TO NEXT ROW
- INITIAL
- PATTERN (START UP+ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(1),
-  DOWN AS price < PREV(1)
-);
-
--- Unsupported quantifier
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- AFTER MATCH SKIP TO NEXT ROW
- INITIAL
- PATTERN (START UP~ DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(1),
-  DOWN AS price < PREV(1)
-);
-
--- PREV(1) missing column reference (error)
-SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
- FROM stock
- WINDOW w AS (
- PARTITION BY company
- ORDER BY tdate
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- AFTER MATCH SKIP TO NEXT ROW
- INITIAL
- PATTERN (START UP+? DOWN+)
- DEFINE
-  START AS TRUE,
-  UP AS price > PREV(1),
-  DOWN AS price < PREV(1)
-);
-
--- Maximum pattern variables is 251 (RPR_VARID_MAX)
-
--- Error: 252 variables exceeds limit of 251
-DO $$
-DECLARE
-    pattern_vars text;
-    define_vars text;
-    query text;
-BEGIN
-    SELECT string_agg('v' || lpad(i::text, 3, '0'), ' '),
-           string_agg('v' || lpad(i::text, 3, '0') || ' AS TRUE', ', ')
-    INTO pattern_vars, define_vars
-    FROM generate_series(1, 252) i;
-
-    query := format('SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (%s)
-        DEFINE %s)', pattern_vars, define_vars);
-
-    EXECUTE query;
-END;
-$$;
-
--- Error: 253 variables exceeds limit of 251
-DO $$
-DECLARE
-    pattern_vars text;
-    define_vars text;
-    query text;
-BEGIN
-    SELECT string_agg('v' || lpad(i::text, 3, '0'), ' '),
-           string_agg('v' || lpad(i::text, 3, '0') || ' AS TRUE', ', ')
-    INTO pattern_vars, define_vars
-    FROM generate_series(1, 253) i;
-
-    query := format('SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (%s)
-        DEFINE %s)', pattern_vars, define_vars);
-
-    EXECUTE query;
-END;
-$$;
-
-    CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER);
-    INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100);
-    INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL);  -- NULL in middle
-    INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200);
-    INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150);
-
-    SELECT company, tdate, price, count(*) OVER w AS match_count
-    FROM stock_null
-    WINDOW w AS (
-      PARTITION BY company
-      ORDER BY tdate
-      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-      PATTERN (START UP DOWN)
-      DEFINE START AS TRUE, UP AS price > PREV(price), DOWN AS price <
-PREV(price)
-    );
-
-
--- Overlapping match tests (requires multi-context for correct behavior)
--- Using array flags: 'X' = ANY(flags) for multi-TRUE support
-
--- Test 1: A B C D E | B C D | C D E F - three overlapping patterns
--- Different end points: B C D (4), A B C D E (5), C D E F (6)
-WITH test_overlap1 AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E']),
-        (6, ARRAY['F'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_overlap1
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C D E | B C D | C D E F)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags),
-        F AS 'F' = ANY(flags)
-);
-
-WITH test_overlap1 AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E']),
-        (6, ARRAY['F'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_overlap1
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A B C D E | B C D | C D E F)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags),
-        F AS 'F' = ANY(flags)
-);
--- PAST LAST: only one match
--- TO NEXT ROW with multi-context: three matches
---   Row 1: A B C D E (1-5)
---   Row 2: B C D (2-4) <- ends first!
---   Row 3: C D E F (3-6) <- ends last!
-
--- Test 1b: Longer pattern FAILS, shorter pattern should survive
--- Pattern: A+ B C D E | B+ C
--- A+ B C D E fails (no E found in sequence)
--- B+ C matches at rows 2-3
--- Result: match 2-3 (B+ C)
-WITH test_overlap1b AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_overlap1b
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A+ B C D E | B+ C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
-
--- Test 2: A B+ C | B+ D - long B sequence with different endings
-WITH test_overlap2 AS (
-    SELECT * FROM (VALUES
-        (1,  ARRAY['A']),
-        (2,  ARRAY['B']),
-        (3,  ARRAY['B']),
-        (4,  ARRAY['B']),
-        (5,  ARRAY['B']),
-        (6,  ARRAY['C']),
-        (7,  ARRAY['B']),
-        (8,  ARRAY['B']),
-        (9,  ARRAY['B']),
-        (10, ARRAY['D'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_overlap2
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A B+ C | B+ D)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags)
-);
--- Current result (correct):
---   Row 1: A B+ C (1-6)
---   Row 7-9: B+ D (7-10, 8-10, 9-10)
--- Note: Row 2-6 cannot match B+ D because Row 6 is C, not D
--- With absorption: 8-10 and 9-10 would be absorbed by 7-10 (earlier context covers later)
-
--- Test 3: Greedy quantifier with late failure - A B C+ D | A B
--- Pattern expects D after C+, but E comes instead ("betrayal")
-WITH test_betrayal AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['C']),
-        (5, ARRAY['C']),
-        (6, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_betrayal
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C+ D | A B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags)
-);
--- A B C+ D fails at Row 6 (E instead of D)
--- Question: Does it fallback to A B (1-2)?
-
--- Test 4: Lexical Order test - A B C | A B C D E
--- SQL standard: first matching alternative wins
-WITH test_lexical AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_lexical
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C | A B C D E)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
--- SQL standard Lexical Order: A B C (1-3) wins (first alternative)
-
--- Test 4b: Reversed pattern order - A B C D E | A B C
-WITH test_lexical2 AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_lexical2
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C D E | A B C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
--- SQL standard Lexical Order: A B C D E (1-5) wins (first alternative)
-
--- Test 5: Multiple TRUE in single row (overlapping pattern variables)
--- Each row matches multiple DEFINE conditions simultaneously
-WITH test_multi_true AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A','B']),        -- A and B both TRUE
-        (2, ARRAY['B','C']),        -- B and C both TRUE
-        (3, ARRAY['C','D']),        -- C and D both TRUE
-        (4, ARRAY['D','E']),        -- D and E both TRUE
-        (5, ARRAY['E','_'])         -- E only
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_multi_true
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C D E)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
--- Row 1: A=T, B=T -> matches A
--- Row 2: B=T, C=T -> matches B
--- Row 3: C=T, D=T -> matches C
--- Row 4: D=T, E=T -> matches D
--- Row 5: E=T      -> matches E
--- Result: match 1-5 (A B C D E)
-
--- Test 6: Diagonal pattern with multi-TRUE (shifted overlap)
-WITH test_diagonal AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A','_']),
-        (2, ARRAY['B','A']),
-        (3, ARRAY['C','B']),
-        (4, ARRAY['D','C']),
-        (5, ARRAY['_','D'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_diagonal
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A B C D)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags)
-);
--- Possible matches:
---   Start Row 1: A(1) B(2) C(3) D(4) -> 1-4
---   Start Row 2: A(2) B(3) C(4) D(5) -> 2-5 (because Row 2 has A too!)
-
--- ===================================================================
--- Context Absorption Tests
--- ===================================================================
-
--- Test absorption 1: Basic A+ pattern - later contexts absorbed by earlier
-WITH test_absorb_basic AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['A']),
-        (5, ARRAY['B'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_absorb_basic
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A+)
-    DEFINE A AS 'A' = ANY(flags)
-);
--- Pattern A+ is absorbable (unbounded first element, only one unbounded)
--- 4 matches: (1-4, 2-4, 3-4, 4-4)
-
--- Test absorption 2: A+ B pattern - absorption with fixed suffix
-WITH test_absorb_suffix AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['B']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_absorb_suffix
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A+ B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
--- Pattern A+ B is absorbable (A+ unbounded first, B bounded suffix)
--- All potential matches end at same row (row 4 with B)
--- 3 matches: (1-4, 2-4, 3-4)
-
--- Test absorption 3: Per-branch absorption with ALT (B+ C | B+ D)
-WITH test_absorb_alt AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['B']),
-        (2, ARRAY['B']),
-        (3, ARRAY['B']),
-        (4, ARRAY['D']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_absorb_alt
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (B+ C | B+ D)
-    DEFINE
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags)
-);
--- Both branches B+ C and B+ D are absorbable (B+ unbounded first)
--- B+ D branch matches: 3 matches (1-4, 2-4, 3-4)
-
--- Test absorption 4: Non-absorbable pattern (A B+ - unbounded not first)
-WITH test_no_absorb AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['B']),
-        (4, ARRAY['B']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_no_absorb
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A B+)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
--- Pattern A B+ is NOT absorbable (A bounded first, B+ unbounded but not first)
--- Only Row 1 can start match (only row with A), so only one match: 1-4
-
--- Test absorption 5: GROUP merge enables absorption
-WITH test_absorb_group AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['A']),
-        (4, ARRAY['B']),
-        (5, ARRAY['A']),
-        (6, ARRAY['B']),
-        (7, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_absorb_group
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN ((A B) (A B)+)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
--- Pattern optimized: (A B) (A B)+ -> (A B){2,}
--- 2 matches: 1-6 (3 reps), 3-6 (2 reps)
-
--- Test absorption 6: Multiple unbounded - first element unbounded enables absorption
-WITH test_multi_unbounded AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['B']),
-        (4, ARRAY['B']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM test_multi_unbounded
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP TO NEXT ROW
-    PATTERN (A+ B+)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
--- 2 matches: 1-4, 2-4 (same endpoint 4)
-
--- ============================================
--- Jacob's RPR Patterns (from jacob branch)
--- ============================================
-
--- Test: A? (optional, greedy)
-WITH jacob_optional AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_optional
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A?)
-    DEFINE A AS 'A' = ANY(flags)
-);
--- Expected: 1-1 (matches A)
-
--- Test: A{2} (exact count)
-WITH jacob_exact AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_exact
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A{2})
-    DEFINE A AS 'A' = ANY(flags)
-);
--- Expected: 1-2
-
--- Test: A{1,3} (bounded range, greedy)
-WITH jacob_bounded AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['A']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_bounded
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A{1,3})
-    DEFINE A AS 'A' = ANY(flags)
-);
--- Expected: 1-3 (greedy takes max), then 4-4
-
--- Test: A | B (simple alternation)
-WITH jacob_simple_alt AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_simple_alt
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A | B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
--- Expected: 1-1 (A), 2-2 (B)
-
--- Test: A | B | C (three-way alternation)
-WITH jacob_three_alt AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['B']),
-        (2, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_three_alt
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A | B | C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
--- Expected: 1-1 (B)
-
--- Test: A B C (concatenation)
-WITH jacob_concat AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_concat
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
--- Expected: 1-3
-
--- Test: A B? C (optional middle)
-WITH jacob_optional_mid AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['C']),
-        (3, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_optional_mid
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B? C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
--- Expected: 1-2 (A C, B skipped)
-
--- Test: (A B){2} (nested group with quantifier)
-WITH jacob_nested_group AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['A']),
-        (4, ARRAY['B']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_nested_group
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN ((A B){2})
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
--- Expected: 1-4 (A B A B)
-
--- Test: (A){3} (quantifier on grouped single element)
-WITH jacob_group_quant AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['A']),
-        (3, ARRAY['A']),
-        (4, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_group_quant
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN ((A){3})
-    DEFINE A AS 'A' = ANY(flags)
-);
--- Expected: 1-3
-
--- Test: A B C | A B C D E (lexical order - first alt wins)
-WITH jacob_lex_first AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_lex_first
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C | A B C D E)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
--- Expected: 1-3 (A B C wins by lexical order)
-
--- Test: A B C D E | A B C (lexical order - longer first wins)
-WITH jacob_lex_long AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['C']),
-        (4, ARRAY['D']),
-        (5, ARRAY['E'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_lex_long
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B C D E | A B C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags),
-        D AS 'D' = ANY(flags),
-        E AS 'E' = ANY(flags)
-);
--- Expected: 1-5 (A B C D E wins by lexical order)
-
--- ============================================
--- Alternation with quantifiers (BUG cases from Jacob's tests)
--- ============================================
-
--- Test: (A | B)+ C - alternation inside quantified group followed by C
-WITH jacob_alt_quant AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['B']),
-        (3, ARRAY['A']),
-        (4, ARRAY['C'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_alt_quant
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN ((A | B)+ C)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
--- Expected: 1-4 (A B A C)
-
--- Test: ((A | B) C)+ - alternation inside group with outer quantifier
-WITH jacob_alt_group AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A']),
-        (2, ARRAY['C']),
-        (3, ARRAY['B']),
-        (4, ARRAY['C']),
-        (5, ARRAY['X'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_alt_group
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (((A | B) C)+)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags),
-        C AS 'C' = ANY(flags)
-);
--- Expected: 1-4 (A C B C)
-
--- ============================================
--- RELUCTANT quantifiers
--- ============================================
-
--- Test: A+? B (reluctant) - B available at row 3, A exits early
-WITH jacob_reluctant AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A','_']),
-        (2, ARRAY['A','_']),
-        (3, ARRAY['A','B']),
-        (4, ARRAY['B','_'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_reluctant
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A+? B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
-
--- Test: A{1,3}? B (reluctant bounded) - same data, bounded quantifier
-WITH jacob_reluctant_bounded AS (
-    SELECT * FROM (VALUES
-        (1, ARRAY['A','_']),
-        (2, ARRAY['A','_']),
-        (3, ARRAY['A','B']),
-        (4, ARRAY['B','_'])
-    ) AS t(id, flags)
-)
-SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
-FROM jacob_reluctant_bounded
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A{1,3}? B)
-    DEFINE
-        A AS 'A' = ANY(flags),
-        B AS 'B' = ANY(flags)
-);
-
--- ============================================
--- Nested quantifiers (pathological patterns)
--- ============================================
--- These patterns previously caused segfault or infinite loop.
--- Now they are either optimized at compile time or handled safely at runtime.
-
--- Test: (A*)* - nested unbounded quantifiers (optimized to A*)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A*)*)
-    DEFINE A AS TRUE
-);
-
--- Test: (A*)+ - inner nullable, outer requires one (optimized to A*)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A*)+)
-    DEFINE A AS TRUE
-);
-
--- Test: (A+)* - outer nullable (optimized to A*)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A+)*)
-    DEFINE A AS TRUE
-);
-
--- Test: (A+)+ - both require match (optimized to A+)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A+)+)
-    DEFINE A AS TRUE
-);
-
--- Test: (A* B*)* - complex nested pattern (runtime protection)
--- Not optimized but handled safely by empty-match loop prevention
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 5) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((A* B*)*)
-    DEFINE A AS TRUE, B AS TRUE
-);
-
--- Test: (((A)*)*)*  - triple nested (optimized through recursive optimization)
-SELECT v, count(*) OVER w AS c
-FROM (SELECT generate_series(1, 3) v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    INITIAL
-    PATTERN ((((A)*)*)*)
-    DEFINE A AS TRUE
-);
+DROP TABLE rpr_consec_null;
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 97b62a73a0e..da28de1a8f9 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1050,7 +1050,7 @@ WINDOW w AS (
 );
 -- Expected: ERROR: syntax error at or near "*"
 
--- ? ? (invalid combination)
+-- ? ? (parsed as ?? reluctant quantifier)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1533,7 +1533,17 @@ SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
 
 DROP VIEW rpr_serial_v8;
 
-DROP TABLE rpr_serial;
+-- Reluctant {1}? quantifier deparse through ruleutils
+CREATE VIEW rpr_quant_reluctant_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             INITIAL
+             PATTERN (A{1}? B)
+             DEFINE A AS val > 0, B AS val > 0);
+SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
+DROP VIEW rpr_quant_reluctant_v;
 
 -- Materialized view (if supported)
 
@@ -1560,6 +1570,40 @@ SELECT * FROM rpr_mview_v1 ORDER BY id;
 DROP MATERIALIZED VIEW rpr_mview_v1;
 DROP TABLE rpr_mview;
 
+-- CREATE TABLE AS SELECT with RPR
+CREATE TABLE rpr_ctas (id INT, val INT);
+INSERT INTO rpr_ctas VALUES (1, 10), (2, 20), (3, 15), (4, 25);
+
+CREATE TABLE rpr_ctas_result AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_ctas
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS val > PREV(val)
+);
+SELECT * FROM rpr_ctas_result ORDER BY id;
+
+-- INSERT INTO ... SELECT with RPR
+CREATE TABLE rpr_insert_target (id INT, val INT, cnt BIGINT);
+INSERT INTO rpr_insert_target
+SELECT id, val, count(*) OVER w
+FROM rpr_ctas
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS val > PREV(val)
+);
+SELECT * FROM rpr_insert_target ORDER BY id;
+
+DROP TABLE rpr_ctas_result;
+DROP TABLE rpr_insert_target;
+DROP TABLE rpr_ctas;
+
 -- Prepared statements (tests outfuncs.c / readfuncs.c)
 
 CREATE TABLE rpr_prep (id INT, val INT);
@@ -1822,6 +1866,32 @@ SELECT pg_get_viewdef('rpr_multiwin_v'::regclass);
 DROP VIEW rpr_multiwin_v;
 DROP TABLE rpr_multiwin;
 
+-- {n} quantifier display in view
+CREATE VIEW rpr_quant_n_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             INITIAL
+             PATTERN (A{3})
+             DEFINE A AS val > 0);
+SELECT pg_get_viewdef('rpr_quant_n_v'::regclass);
+DROP VIEW rpr_quant_n_v;
+
+-- {n,} quantifier display in view
+CREATE VIEW rpr_quant_n_plus_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             INITIAL
+             PATTERN (A{2,})
+             DEFINE A AS val > 0);
+SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
+DROP VIEW rpr_quant_n_plus_v;
+
+DROP TABLE rpr_serial;
+
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
@@ -1901,6 +1971,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
+-- Expected: ERROR: pattern variable qualified column reference "a.val" is not supported
 
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
@@ -1911,6 +1982,7 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
+-- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
 
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
@@ -1921,6 +1993,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0, B AS B.val > 0
 );
+-- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
 
 -- FROM-clause range variable qualified name: not allowed (prohibited by §6.5)
 SELECT COUNT(*) OVER w
@@ -1931,6 +2004,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
+-- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
 
 -- Semantic errors
 
@@ -1965,7 +2039,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS COUNT(*) > 0
 );
--- Expected: ERROR or works depending on implementation
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 -- Subquery in DEFINE (NOT SUPPORTED)
 SELECT COUNT(*) OVER w
@@ -2108,6 +2182,13 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
 
+-- Data execution: SEQ flatten produces correct results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP TO NEXT ROW
+             PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+
 -- ALT flatten: (A | (B | C))+ -> (a | b | c)+
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2120,6 +2201,13 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50);
 
+-- Data execution: ALT dedup produces correct results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP PAST LAST ROW
+             PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50);
+
 -- Quantifier multiply: (A{2}){3} -> a{6}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2305,6 +2393,13 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
 
+-- Data execution: GROUP unwrap produces correct results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP TO NEXT ROW
+             PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+
 -- Reluctant optimization bypass: VAR merge
 -- A+? A stays as a+? a (greedy A+ A merges to a{2,})
 EXPLAIN (COSTS OFF)
@@ -2368,6 +2463,62 @@ 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);
 
+-- Duplicate GROUP removal: ((A | B)+ | (A | B)+) -> (a | b)+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A | B)+ | (A | B)+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Consecutive VAR merge with zero-min: A* A+ -> a+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A* A+) DEFINE A AS val > 0);
+
+-- Consecutive VAR merge (4-element): A A{2} A+ A{3} -> a{7,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A A{2} A+ A{3}) DEFINE A AS val > 0);
+
+-- PREFIX+SUFFIX merge (5-way): A B A B (A B)+ A B A B -> (a b){5,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B A B (A B)+ A B A B)
+             DEFINE A AS val <= 50, B AS val > 50);
+
+-- Unwrap single-item ALT after dedup: (A | A)+ -> a+
+-- ALT dedup reduces to single-item, then GROUP unwrap
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A | A)+) DEFINE A AS val > 0);
+
+-- GROUP{1,1} to SEQ with flatten: ((A B)(C D)) -> a b c d
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B)(C D)))
+             DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+                    C AS val > 50 AND val <= 75, D AS val > 75);
+
+-- Nested ALT pattern: ((A B) | C) D | A B C
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B) | C) D | A B C)
+             DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+                    C AS val > 50 AND val <= 75, D AS val > 75);
+
+-- Nested ALT with unbounded: ((A+ B) | C) D | A B C
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A+ B) | C) D | A B C)
+             DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+                    C AS val > 50 AND val <= 75, D AS val > 75);
+
 -- ============================================================
 -- Absorption Flag Display Tests
 -- ============================================================
@@ -2450,6 +2601,19 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
              AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
 
+-- Reluctant {1}? quantifier deparse
+-- A{1}? is a reluctant {1,1} quantifier.  The deparse code must
+-- output "{1}" explicitly to disambiguate from a bare "?" quantifier
+-- (which would mean {0,1}).
+EXPLAIN (COSTS OFF) SELECT count(*) OVER w
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY val
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1}? B)
+    DEFINE A AS val > 0, B AS val > 0
+);
+
 -- ============================================================
 -- Absorption Analysis Tests
 -- ============================================================
@@ -2871,7 +3035,8 @@ WINDOW w AS (
 )
 GROUP BY category
 ORDER BY category;
--- Expected: ERROR (GROUP BY with window RPR not supported)
+-- Expected: ERROR: syntax error at or near "GROUP"
+-- (GROUP BY after WINDOW clause is not valid SQL syntax)
 
 -- ============================================================
 -- Subquery and CTE Tests
@@ -3243,7 +3408,8 @@ INSERT INTO rpr_sort VALUES
     (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
 
--- RPR with GROUP BY
+-- RPR with GROUP BY (aggregate in DEFINE → ERROR before GROUP BY interaction)
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
        COUNT(*) as group_cnt,
@@ -3259,7 +3425,8 @@ WINDOW w AS (
 )
 ORDER BY category;
 
--- RPR with HAVING
+-- RPR with HAVING (same aggregate-in-DEFINE error)
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
        COUNT(*) as group_cnt,
@@ -3771,7 +3938,3 @@ FROM (SELECT id, val,
 ) s;
 
 DROP TABLE rpr_plan;
-
--- ============================================================
--- End of rpr_base.sql
--- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index d017a2292d2..4bb49650bb7 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -6,8 +6,9 @@
 -- This test suite validates EXPLAIN output for RPR queries,
 -- including NFA statistics shown in EXPLAIN ANALYZE:
 --   - NFA States: peak, total, merged
---   - NFA Contexts: peak, total, absorbed, skipped
+--   - NFA Contexts: peak, total, pruned
 --   - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
+--   - NFA: absorbed (len min/max/avg), skipped (len min/max/avg)
 --   - Pattern deparse formatting
 --   - Multiple output formats (text, JSON, XML)
 --
@@ -310,7 +311,7 @@ WINDOW w AS (
 );');
 DROP VIEW rpr_v;
 
--- Grouped pattern with quantifier - state merging
+-- Grouped pattern with quantifier - state count with grouping
 CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
@@ -428,7 +429,7 @@ WINDOW w AS (
 );');
 DROP VIEW rpr_v;
 
--- High state merging - alternation with plus quantifier
+-- High state count - alternation with plus quantifier
 CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
@@ -499,7 +500,7 @@ WINDOW w AS (
 DROP VIEW rpr_v;
 
 -- ============================================================
--- Context Statistics Tests (peak, total, absorbed, skipped)
+-- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
 
 -- Context absorption with unbounded quantifier at start
@@ -671,7 +672,7 @@ WINDOW w AS (
 );');
 DROP VIEW rpr_v;
 
--- Mix of short and long matches
+-- Uniform match length with mismatches from gap rows (v%20 = 11..15)
 CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
@@ -702,8 +703,8 @@ DROP VIEW rpr_v;
 -- Mismatch Length Statistics Tests
 -- ============================================================
 
--- Pattern that causes mismatches with length > 1
--- Mismatch happens when partial match fails after processing multiple rows
+-- Pattern with complete match every cycle: 0 mismatched
+-- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched
 CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM (
@@ -837,33 +838,6 @@ WINDOW w AS (
 )');
 DROP VIEW rpr_v;
 
--- ============================================================
--- XML Format Tests
--- ============================================================
-
--- XML format output
-CREATE TEMP VIEW rpr_v AS
-SELECT count(*) OVER w
-FROM generate_series(1, 30) AS s(v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B)
-    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
-SELECT rpr_explain_filter('
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
-SELECT count(*) OVER w
-FROM generate_series(1, 30) AS s(v)
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (A B)
-    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-)');
-DROP VIEW rpr_v;
-
 -- JSON format with mismatch statistics
 -- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
 CREATE TEMP VIEW rpr_v AS
@@ -912,6 +886,33 @@ WINDOW w AS (
 )');
 DROP VIEW rpr_v;
 
+-- ============================================================
+-- XML Format Tests
+-- ============================================================
+
+-- XML format output
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+)');
+DROP VIEW rpr_v;
+
 -- ============================================================
 -- Multiple Partitions Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index a3f01b60bc4..c0f4dfa1248 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -255,6 +255,119 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
+-- Absorption with fixed suffix: A+ B
+WITH test_absorb_suffix AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_suffix
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Per-branch absorption with ALT: B+ C | B+ D
+WITH test_absorb_alt AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['B']),
+        (3, ARRAY['B']),
+        (4, ARRAY['D']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_alt
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (B+ C | B+ D)
+    DEFINE
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+
+-- Non-absorbable: A B+ (unbounded not in first position)
+WITH test_no_absorb AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['B']),
+        (4, ARRAY['B']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_no_absorb
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- GROUP merge enables absorption: (A B) (A B)+ optimized to (A B){2,}
+WITH test_absorb_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A B) (A B)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Multiple unbounded: A+ B+ (first element unbounded enables absorption)
+WITH test_multi_unbounded AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['B']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_multi_unbounded
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Context Lifecycle
 -- ============================================================
@@ -524,7 +637,8 @@ WINDOW w AS (
 );
 
 -- Optional reluctant group: (A B)?? C
--- nfa_advance_begin: reluctant prefers skip (0 iterations) over enter
+-- nfa_advance_begin: reluctant tries skip first, but skip path needs C
+-- at row 1 which is A → skip fails. Enter path succeeds: A(1) B(2) C(3).
 WITH test_optional_reluctant AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -571,6 +685,36 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Reluctant optional group skip-to-FIN
+-- When a reluctant optional group's skip path reaches FIN, the group
+-- entry path is abandoned (nodeWindowAgg.c nfa_advance_begin).
+-- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)??
+-- prefers to skip.  Skip goes to FIN (group is last element), so
+-- the match completes with just C.
+WITH test_begin_skip_fin AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['C']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['C','A']),
+        (5, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_begin_skip_fin
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (C (A B)??)
+    DEFINE
+        C AS 'C' = ANY(flags),
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Match Phase
 -- ============================================================
@@ -1198,6 +1342,50 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- A+? B (reluctant plus): exits A at first B availability
+-- (Same scenario as greedy-vs-reluctant comparison above; retained for
+-- standalone quantifier coverage alongside A{1,3}? and A{2,3}? below)
+WITH test_reluctant_plus AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','_']),
+        (2, ARRAY['A','_']),
+        (3, ARRAY['A','B']),
+        (4, ARRAY['B','_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_reluctant_plus
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- A{1,3}? B (reluctant bounded): same data, bounded quantifier
+WITH test_reluctant_bounded AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','_']),
+        (2, ARRAY['A','_']),
+        (3, ARRAY['A','B']),
+        (4, ARRAY['B','_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_reluctant_bounded
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{1,3}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
@@ -1481,6 +1669,211 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Overlapping match: A B C D E | B C D | C D E F (SKIP PAST LAST ROW)
+WITH test_overlap1 AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['E']),
+        (6, ARRAY['F'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E | B C D | C D E F)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags),
+        F AS 'F' = ANY(flags)
+);
+
+-- Same with SKIP TO NEXT ROW: three overlapping matches
+WITH test_overlap1 AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['E']),
+        (6, ARRAY['F'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B C D E | B C D | C D E F)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags),
+        F AS 'F' = ANY(flags)
+);
+
+-- Longer pattern fails, shorter survives: A+ B C D E | B+ C
+WITH test_overlap1b AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1b
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B C D E | B+ C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags)
+);
+
+-- Long B sequence with different endings: A B+ C | B+ D
+WITH test_overlap2 AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),
+        (2,  ARRAY['B']),
+        (3,  ARRAY['B']),
+        (4,  ARRAY['B']),
+        (5,  ARRAY['B']),
+        (6,  ARRAY['C']),
+        (7,  ARRAY['B']),
+        (8,  ARRAY['B']),
+        (9,  ARRAY['B']),
+        (10, ARRAY['D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap2
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B+ C | B+ D)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+
+-- Greedy with late failure ("betrayal"): A B C+ D | A B
+WITH test_betrayal AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['C']),
+        (5, ARRAY['C']),
+        (6, ARRAY['E'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_betrayal
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C+ D | A B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+
+-- Multiple TRUE per row: overlapping pattern variables
+WITH test_multi_true AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['B','C']),
+        (3, ARRAY['C','D']),
+        (4, ARRAY['D','E']),
+        (5, ARRAY['E','_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_multi_true
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags)
+);
+
+-- Diagonal pattern with shifted multi-TRUE overlap
+WITH test_diagonal AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','_']),
+        (2, ARRAY['B','A']),
+        (3, ARRAY['C','B']),
+        (4, ARRAY['D','C']),
+        (5, ARRAY['_','D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_diagonal
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B C D)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+
+-- ((A | B) C)+ - alternation inside group with outer quantifier
+WITH test_alt_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['C']),
+        (3, ARRAY['B']),
+        (4, ARRAY['C']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_alt_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A | B) C)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
 -- ============================================================
 -- Deep Nested Groups
 -- ============================================================
@@ -1625,6 +2018,55 @@ WINDOW w AS (
         C AS 'C' = ANY(flags)
 );
 
+-- (A B){2} - group with exact quantifier
+WITH test_group_exact AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_group_exact
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B){2})
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Nested END->END fast-forward
+-- When an inner group has a nullable body and count < min, the
+-- fast-forward path exits through the outer END, incrementing
+-- the outer group's count (nodeWindowAgg.c nfa_advance_end).
+-- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses
+-- because the optimizer cannot safely multiply non-exact quantifiers.
+-- Data has no A rows, forcing all-empty iterations via fast-forward.
+WITH test_nested_ff AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['B']),
+        (3, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nested_ff
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A?){2,3}){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
 -- ============================================================
 -- SKIP Options (Runtime)
 -- ============================================================
@@ -1751,6 +2193,8 @@ ORDER BY mode, id;
 
 -- ============================================================
 -- INITIAL Mode (Runtime)
+-- Placeholder: INITIAL is not yet implemented (syntax error).
+-- Kept here so tests convert to runtime tests when implemented.
 -- ============================================================
 
 -- INITIAL mode (not yet supported - produces syntax error)
@@ -1917,6 +2361,62 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- N FOLLOWING + SKIP TO NEXT ROW: overlapping matches bounded by frame
+-- Row 1: frame [1,4], A(1-3) B(4) -> match
+-- Row 2: frame [2,5], A(2-3) B(4) -> match
+-- Row 3: frame [3,6], A(3) B(4) -> match
+-- Row 5: frame [5,6], A(5) B(6) -> match
+WITH test_n_skip_next AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_n_skip_next
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Frame exactly 1 row short of potential match
+-- From row 1: A A A B needs 4 rows but frame holds 3 -> no match
+-- From row 2: A A B fits in 3-row frame -> match
+WITH test_frame_one_short AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_frame_one_short
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Special Partition Cases
 -- ============================================================
@@ -2567,7 +3067,8 @@ WINDOW w AS (
 
 -- (A?){0,3}: min=0, nullable inner.
 -- A never matches. A? matches empty, min=0 satisfied immediately.
--- Expected: empty match (match_start = match_end) for every row
+-- Per standard: empty match expected for every row.
+-- XXX: visited bitmap blocks empty iteration → no match (same as {2,3})
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -2590,7 +3091,8 @@ WINDOW w AS (
 
 -- (A?){1,3}: min=1, nullable inner.
 -- A never matches. Need 1 empty iteration to satisfy min=1.
--- Expected: empty match for every row
+-- Per standard: empty match expected for every row.
+-- XXX: visited bitmap blocks empty iteration → no match (same as {2,3})
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -2639,7 +3141,8 @@ WINDOW w AS (
 -- (A?){2,3} mixed: some rows match A, some don't
 -- Rows 1-2: A matches, greedy takes 2 → min satisfied
 -- Row 3: A doesn't match, needs 2 empty iterations for min=2
--- Expected: all rows produce matches
+-- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
+-- Row 4: A matches 1 real iter + 1 ff empty exit → match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2773,4 +3276,3 @@ WINDOW w AS (
         A AS price > 100,
         B AS TRUE
 );
-
-- 
2.50.1 (Apple Git-155)

From f9c80e37bbc493737ed04d237abdd4115d5a412e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 19:32:24 +0900
Subject: [PATCH 4/8] Keep RPR test objects for pg_upgrade/pg_dump testing

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 3383b242ef0..ae6d9c9b937 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -12,7 +12,7 @@
 --   Quantifiers Tests
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
---   Serialization/Deserialization Tests
+--   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
 --   Error Cases Tests
 --
 -- Planner Layer:
@@ -1930,7 +1930,6 @@ SELECT pg_get_viewdef('rpr_serial_v1'::regclass);
    a AS (val > 0) );
 (1 row)
 
-DROP VIEW rpr_serial_v1;
 -- Complex pattern with alternation
 CREATE VIEW rpr_serial_v2 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1967,7 +1966,6 @@ SELECT pg_get_viewdef('rpr_serial_v2'::regclass);
    b AS (val <= 20) );
 (1 row)
 
-DROP VIEW rpr_serial_v2;
 -- Pattern with grouping and quantifiers
 CREATE VIEW rpr_serial_v3 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -2008,7 +2006,6 @@ SELECT pg_get_viewdef('rpr_serial_v3'::regclass);
    c AS (val <= 10) );
 (1 row)
 
-DROP VIEW rpr_serial_v3;
 -- All features combined
 CREATE VIEW rpr_serial_v4 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -2053,7 +2050,6 @@ SELECT pg_get_viewdef('rpr_serial_v4'::regclass);
    finish AS (val > 15) );
 (1 row)
 
-DROP VIEW rpr_serial_v4;
 -- Additional quantifiers for deparsing coverage
 -- ? quantifier (zero or one)
 CREATE VIEW rpr_serial_v5 AS
@@ -2091,7 +2087,6 @@ SELECT pg_get_viewdef('rpr_serial_v5'::regclass);
    b AS (val > 20) );
 (1 row)
 
-DROP VIEW rpr_serial_v5;
 -- {n,} quantifier (n or more)
 CREATE VIEW rpr_serial_v6 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -2127,7 +2122,6 @@ SELECT pg_get_viewdef('rpr_serial_v6'::regclass);
    a AS (val > 15) );
 (1 row)
 
-DROP VIEW rpr_serial_v6;
 -- {n} quantifier (exactly n)
 CREATE VIEW rpr_serial_v7 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -2163,7 +2157,6 @@ SELECT pg_get_viewdef('rpr_serial_v7'::regclass);
    a AS (val > 0) );
 (1 row)
 
-DROP VIEW rpr_serial_v7;
 -- Nested ALT pattern (tests deparse of complex nested structure)
 CREATE VIEW rpr_serial_v8 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -2202,7 +2195,6 @@ SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
    d AS (val > 30) );
 (1 row)
 
-DROP VIEW rpr_serial_v8;
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
@@ -2228,7 +2220,6 @@ SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
    b AS (val > 0) );
 (1 row)
 
-DROP VIEW rpr_quant_reluctant_v;
 -- Materialized view (if supported)
 CREATE TABLE rpr_mview (id INT, val INT);
 INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
@@ -2274,8 +2265,6 @@ SELECT * FROM rpr_mview_v1 ORDER BY id;
   3 |  30 |   0
 (3 rows)
 
-DROP MATERIALIZED VIEW rpr_mview_v1;
-DROP TABLE rpr_mview;
 -- CREATE TABLE AS SELECT with RPR
 CREATE TABLE rpr_ctas (id INT, val INT);
 INSERT INTO rpr_ctas VALUES (1, 10), (2, 20), (3, 15), (4, 25);
@@ -2677,8 +2666,6 @@ SELECT pg_get_viewdef('rpr_multiwin_v'::regclass);
    b AS (val <= 15) );
 (1 row)
 
-DROP VIEW rpr_multiwin_v;
-DROP TABLE rpr_multiwin;
 -- {n} quantifier display in view
 CREATE VIEW rpr_quant_n_v AS
 SELECT id, val, count(*) OVER w
@@ -2703,7 +2690,6 @@ SELECT pg_get_viewdef('rpr_quant_n_v'::regclass);
    a AS (val > 0) );
 (1 row)
 
-DROP VIEW rpr_quant_n_v;
 -- {n,} quantifier display in view
 CREATE VIEW rpr_quant_n_plus_v AS
 SELECT id, val, count(*) OVER w
@@ -2728,8 +2714,6 @@ SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
    a AS (val > 0) );
 (1 row)
 
-DROP VIEW rpr_quant_n_plus_v;
-DROP TABLE rpr_serial;
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 817269021f4..3c70a12874a 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3,6 +3,9 @@
 -- Tests for Row Pattern Recognition EXPLAIN output
 -- ============================================================
 --
+-- Views and tables in this file are intentionally not dropped,
+-- so that pg_upgrade/pg_dump can test RPR syntax serialization.
+--
 -- This test suite validates EXPLAIN output for RPR queries,
 -- including NFA statistics shown in EXPLAIN ANALYZE:
 --   - NFA States: peak, total, merged
@@ -74,13 +77,13 @@ begin
 end;
 $$;
 -- Setup: Create test tables
-CREATE TEMP TABLE nfa_test (
+CREATE TABLE rpr_nfa_test (
     id serial,
     v int,
     cat char(1)
 );
 -- Insert test data: 100 rows with predictable pattern
-INSERT INTO nfa_test (v, cat)
+INSERT INTO rpr_nfa_test (v, cat)
 SELECT i,
        CASE
            WHEN i % 5 = 1 THEN 'A'
@@ -91,12 +94,12 @@ SELECT i,
        END
 FROM generate_series(1, 100) i;
 -- Additional test table with more complex patterns
-CREATE TEMP TABLE nfa_complex (
+CREATE TABLE rpr_nfa_complex (
     id serial,
     price int,
     trend char(1)  -- U=up, D=down, S=stable
 );
-INSERT INTO nfa_complex (price, trend)
+INSERT INTO rpr_nfa_complex (price, trend)
 VALUES
     (100, 'S'), (105, 'U'), (110, 'U'), (108, 'D'), (112, 'U'),
     (115, 'U'), (113, 'D'), (111, 'D'), (109, 'D'), (110, 'U'),
@@ -108,16 +111,16 @@ VALUES
 -- Basic NFA Statistics Tests
 -- ============================================================
 -- Simple pattern - should show basic statistics
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev01 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS cat = 'A', B AS cat = 'B'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev01'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -126,7 +129,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -143,21 +146,20 @@ WINDOW w AS (
    NFA Contexts: 2 peak, 101 total, 60 pruned
    NFA: 20 matched (len 2/2/2.0), 0 mismatched
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+   ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Pattern with no matches - 0 matched
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev02 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (X Y Z)
     DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev02'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (x y z) 
@@ -166,7 +168,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -182,21 +184,20 @@ WINDOW w AS (
    NFA States: 1 peak, 101 total, 0 merged
    NFA Contexts: 2 peak, 101 total, 100 pruned
    NFA: 0 matched, 0 mismatched
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+   ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Pattern matching every row - high match count
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev03 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (R)
     DEFINE R AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev03'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line      
 ----------------
    PATTERN (r) 
@@ -205,7 +206,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -221,13 +222,12 @@ WINDOW w AS (
    NFA States: 2 peak, 101 total, 0 merged
    NFA Contexts: 2 peak, 101 total, 0 pruned
    NFA: 100 matched (len 1/1/1.0), 0 mismatched
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+   ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Regression test: Space before parenthesis in pattern deparse
 -- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev04 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -235,7 +235,7 @@ WINDOW w AS (
     PATTERN (A (B | C))
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev04'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN (a (b | c)) 
@@ -263,11 +263,10 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Regression test: Sequential alternations at same depth
 -- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
 -- Previously failed due to missing parentheses on ALT depth decrease
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev05 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -275,7 +274,7 @@ WINDOW w AS (
     PATTERN (A ((B | C) (D | E))*)
     DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev05'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line                
 -----------------------------------
    PATTERN (a ((b | c) (d | e))*) 
@@ -302,12 +301,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- State Statistics Tests (peak, total, merged)
 -- ============================================================
 -- Simple quantifier pattern - A+ with short matches (no merging)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev06 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -316,7 +314,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev06'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line       
 -----------------
    PATTERN (a+) 
@@ -344,11 +342,10 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Alternation pattern - multiple state branches
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev07 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -357,7 +354,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev07'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line               
 ----------------------------------
    PATTERN ((a | b | c) (d | e)) 
@@ -366,7 +363,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -385,12 +382,11 @@ WINDOW w AS (
    NFA Contexts: 3 peak, 101 total, 20 pruned
    NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+   ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Complex pattern with high state count
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev08 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -402,7 +398,7 @@ WINDOW w AS (
         B AS v % 3 = 2,
         C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev08'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (a+ b* c+) 
@@ -434,9 +430,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Grouped pattern with quantifier - state count with grouping
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev09 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -445,7 +440,7 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev09'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN ((a b)+) 
@@ -474,10 +469,9 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev10 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -486,7 +480,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev10'), E'\n')) AS line WHERE line ~ 'PATTERN';
                                      line                                     
 ------------------------------------------------------------------------------
    PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b)) 
@@ -515,10 +509,9 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Consecutive ALT merge followed by different ALT
 -- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev11 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -527,7 +520,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev11'), E'\n')) AS line WHERE line ~ 'PATTERN';
                  line                 
 --------------------------------------
    PATTERN ((a | b) (a | b) (c | d)) 
@@ -556,10 +549,9 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Consecutive ALT merge followed by non-ALT element
 -- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev12 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -568,7 +560,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev12'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN ((a | b) (a | b) c) 
@@ -597,9 +589,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev13 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -608,7 +599,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B)+ (A | B))
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev13'), E'\n')) AS line WHERE line ~ 'PATTERN';
                  line                  
 ---------------------------------------
    PATTERN ((a | b) (a | b)+ (a | b)) 
@@ -637,9 +628,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- High state count - alternation with plus quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev14 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -648,7 +638,7 @@ WINDOW w AS (
     PATTERN ((A | B | C)+ D)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev14'), E'\n')) AS line WHERE line ~ 'PATTERN';
             line             
 -----------------------------
    PATTERN ((a | b | c)+ d) 
@@ -677,10 +667,9 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Early termination: first ALT branch (A) reaches FIN immediately,
 -- pruning second branch (A B+) before it can accumulate B repetitions.
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev15 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -689,7 +678,7 @@ WINDOW w AS (
     PATTERN ((A | A B)+)
     DEFINE A AS v = 1, B AS v > 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev15'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a | a b)+) 
@@ -717,9 +706,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Nested quantifiers causing state growth
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev16 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -728,7 +716,7 @@ WINDOW w AS (
     PATTERN (((A | B)+)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev16'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN (((a | b)+)+) 
@@ -757,12 +745,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
 -- Context absorption with unbounded quantifier at start
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev17 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -771,7 +758,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev17'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -800,9 +787,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- No absorption - bounded quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev18 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -811,7 +797,7 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev18'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (a{2,4} b) 
@@ -840,9 +826,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Contexts skipped by SKIP PAST LAST ROW
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev19 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -851,7 +836,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev19'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a b c) 
@@ -880,9 +865,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- High context absorption - unbounded group
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev20 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -891,7 +875,7 @@ WINDOW w AS (
     PATTERN ((A B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN ((a b)+ c) 
@@ -920,14 +904,13 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Match Length Statistics Tests
 -- ============================================================
 -- Fixed length matches - all same length
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev21 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -936,7 +919,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev21'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN (a b c d e) 
@@ -945,7 +928,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -964,12 +947,11 @@ WINDOW w AS (
    NFA Contexts: 3 peak, 101 total, 60 pruned
    NFA: 20 matched (len 5/5/5.0), 0 mismatched
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+   ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Variable length matches - min/max/avg differ
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev22 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -978,7 +960,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev22'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1007,9 +989,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Very long matches
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev23 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 200) AS s(v)
 WINDOW w AS (
@@ -1018,7 +999,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v <= 195, B AS v > 195
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev23'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1047,9 +1028,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=200.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Uniform match length with mismatches from gap rows (v%20 = 11..15)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev24 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1060,7 +1040,7 @@ WINDOW w AS (
         A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
         B AS v % 20 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev24'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1091,13 +1071,12 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Mismatch Length Statistics Tests
 -- ============================================================
 -- Pattern with complete match every cycle: 0 mismatched
 -- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev25 AS
 SELECT count(*) OVER w
 FROM (
     SELECT v,
@@ -1113,7 +1092,7 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev25'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line         
 ----------------------
    PATTERN (a+ b+ c) 
@@ -1149,9 +1128,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Long partial matches that fail
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev26 AS
 SELECT count(*) OVER w
 FROM (
     SELECT i AS v,
@@ -1172,7 +1150,7 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev26'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line         
 ----------------------
    PATTERN (a+ b+ c) 
@@ -1213,12 +1191,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series i (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- JSON Format Tests
 -- ============================================================
 -- JSON format output with all statistics
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev27 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1227,7 +1204,7 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev27'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a+ b+) 
@@ -1294,9 +1271,8 @@ WINDOW w AS (
  ]
 (1 row)
 
-DROP VIEW rpr_v;
 -- JSON format with match length statistics
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev28 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1305,7 +1281,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev28'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1375,10 +1351,9 @@ WINDOW w AS (
  ]
 (1 row)
 
-DROP VIEW rpr_v;
 -- JSON format with mismatch statistics
 -- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev29 AS
 SELECT count(*) OVER w
 FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
 WINDOW w AS (
@@ -1387,7 +1362,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v = 1, B AS v = 2, C AS v = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev29'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a b c) 
@@ -1456,10 +1431,9 @@ WINDOW w AS (
  ]
 (1 row)
 
-DROP VIEW rpr_v;
 -- JSON format with skipped context statistics
 -- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev30 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1468,7 +1442,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev30'), E'\n')) AS line WHERE line ~ 'PATTERN';
                                      line                                     
 ------------------------------------------------------------------------------
    PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b)) 
@@ -1538,12 +1512,11 @@ WINDOW w AS (
  ]
 (1 row)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- XML Format Tests
 -- ============================================================
 -- XML format output
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev31 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -1552,7 +1525,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev31'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -1619,12 +1592,11 @@ WINDOW w AS (
  </explain>
 (1 row)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Multiple Partitions Tests
 -- ============================================================
 -- Statistics across multiple partitions
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev32 AS
 SELECT count(*) OVER w
 FROM (
     SELECT p, v
@@ -1638,7 +1610,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev32'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1677,9 +1649,8 @@ WINDOW w AS (
                ->  Function Scan on generate_series v (actual rows=30.00 loops=3)
 (14 rows)
 
-DROP VIEW rpr_v;
 -- Different pattern behavior per partition
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev33 AS
 SELECT count(*) OVER w
 FROM (
     SELECT
@@ -1694,7 +1665,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS val < 5, B AS val >= 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev33'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1732,12 +1703,11 @@ WINDOW w AS (
          ->  Function Scan on generate_series v (actual rows=50.00 loops=1)
 (12 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Edge Cases
 -- ============================================================
 -- Empty result set
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev34 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 0) AS s(v)
 WINDOW w AS (
@@ -1746,7 +1716,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v = 1, B AS v = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev34'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -1770,9 +1740,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=0.00 loops=1)
 (4 rows)
 
-DROP VIEW rpr_v;
 -- Single row
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev35 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1) AS s(v)
 WINDOW w AS (
@@ -1781,7 +1750,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev35'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line      
 ----------------
    PATTERN (a) 
@@ -1809,9 +1778,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Pattern longer than data
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev36 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 5) AS s(v)
 WINDOW w AS (
@@ -1822,7 +1790,7 @@ WINDOW w AS (
         A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
         F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev36'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line               
 ----------------------------------
    PATTERN (a b c d e f g h i j) 
@@ -1852,9 +1820,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=5.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- All rows match as single match
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev37 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1863,7 +1830,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev37'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line       
 -----------------
    PATTERN (a+) 
@@ -1892,12 +1859,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Complex Pattern Tests
 -- ============================================================
 -- Nested groups
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev38 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1906,7 +1872,7 @@ WINDOW w AS (
     PATTERN (((A B) C)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev38'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN (((a b) c)+) 
@@ -1935,11 +1901,10 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Multiple alternations
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev39 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -1948,7 +1913,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev39'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line               
 ----------------------------------
    PATTERN ((a | b) (c | d | e)) 
@@ -1957,7 +1922,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -1976,12 +1941,11 @@ WINDOW w AS (
    NFA Contexts: 3 peak, 101 total, 40 pruned
    NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+   ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Optional elements
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev40 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1990,7 +1954,7 @@ WINDOW w AS (
     PATTERN (A B? C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev40'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a b? c) 
@@ -2019,9 +1983,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Bounded quantifiers
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev41 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -2030,7 +1993,7 @@ WINDOW w AS (
     PATTERN (A{2,5} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev41'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (a{2,5} b) 
@@ -2059,9 +2022,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Star quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev42 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2070,7 +2032,7 @@ WINDOW w AS (
     PATTERN (A B* C)
     DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev42'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a b* c) 
@@ -2099,21 +2061,20 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Real-world Pattern Examples
 -- ============================================================
 -- Stock price pattern - V-shape (down then up)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev43 AS
 SELECT count(*) OVER w
-FROM nfa_complex
+FROM rpr_nfa_complex
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (D+ U+)
     DEFINE D AS trend = 'D', U AS trend = 'U'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev43'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (d+ u+) 
@@ -2122,7 +2083,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_complex
+FROM rpr_nfa_complex
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -2139,21 +2100,20 @@ WINDOW w AS (
    NFA Contexts: 3 peak, 31 total, 3 pruned
    NFA: 3 matched (len 3/14/8.0), 1 mismatched (len 3/3/3.0)
    NFA: 9 absorbed (len 1/1/1.0), 14 skipped (len 1/1/1.0)
-   ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
+   ->  Seq Scan on rpr_nfa_complex (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Stock price pattern - peak (up, stable, down)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev44 AS
 SELECT count(*) OVER w
-FROM nfa_complex
+FROM rpr_nfa_complex
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (U+ S* D+)
     DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev44'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (u+ s* d+) 
@@ -2162,7 +2122,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_complex
+FROM rpr_nfa_complex
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -2179,12 +2139,11 @@ WINDOW w AS (
    NFA Contexts: 3 peak, 31 total, 1 pruned
    NFA: 4 matched (len 3/11/7.2), 0 mismatched
    NFA: 12 absorbed (len 1/1/1.0), 13 skipped (len 1/1/1.0)
-   ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
+   ->  Seq Scan on rpr_nfa_complex (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Consecutive increasing values (using PREV)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev45 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2193,7 +2152,7 @@ WINDOW w AS (
     PATTERN (A{3,})
     DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev45'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a{3,}) 
@@ -2222,12 +2181,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Performance-oriented Tests
 -- ============================================================
 -- Large dataset with simple pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev46 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -2236,7 +2194,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev46'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -2265,9 +2223,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Large dataset with absorption
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev47 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -2276,7 +2233,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev47'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -2305,9 +2262,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- High state merge ratio
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev48 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2316,7 +2272,7 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev48'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a | b)+ c) 
@@ -2345,12 +2301,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- INITIAL vs no INITIAL comparison
 -- ============================================================
 -- With INITIAL keyword
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev49 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2360,7 +2315,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev49'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -2390,9 +2345,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Without INITIAL keyword (same behavior currently)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev50 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2401,7 +2355,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev50'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -2430,12 +2384,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Quantifier Variations
 -- ============================================================
 -- Plus quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev51 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -2444,7 +2397,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 4 <> 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev51'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line       
 -----------------
    PATTERN (a+) 
@@ -2473,9 +2426,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Star quantifier (zero or more)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev52 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -2484,7 +2436,7 @@ WINDOW w AS (
     PATTERN (A* B)
     DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev52'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a* b) 
@@ -2513,9 +2465,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Question mark (zero or one)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev53 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -2524,7 +2475,7 @@ WINDOW w AS (
     PATTERN (A? B C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev53'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a? b c) 
@@ -2553,9 +2504,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Exact count {n}
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev54 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2564,7 +2514,7 @@ WINDOW w AS (
     PATTERN (A{3} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev54'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a{3} b) 
@@ -2593,9 +2543,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Range {n,m}
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev55 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2604,7 +2553,7 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev55'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (a{2,4} b) 
@@ -2633,9 +2582,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- At least {n,}
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev56 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2644,7 +2592,7 @@ WINDOW w AS (
     PATTERN (A{3,} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev56'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line         
 ----------------------
    PATTERN (a{3,} b) 
@@ -2673,13 +2621,12 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Regression Tests for Statistics Accuracy
 -- ============================================================
 -- Verify state count accuracy
 -- Pattern A+ B with 20 rows should show predictable state behavior
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev57 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -2688,7 +2635,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev57'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -2717,9 +2664,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Verify context count with known absorption
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev58 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2728,7 +2674,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev58'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a+ b c) 
@@ -2757,9 +2703,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Verify match length with fixed-length pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev59 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2768,7 +2713,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev59'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a b c) 
@@ -2797,21 +2742,20 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Alternation Pattern Tests
 -- ============================================================
 -- Simple alternation
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev60 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A | B) C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev60'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN ((a | b) c) 
@@ -2820,7 +2764,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -2837,14 +2781,13 @@ WINDOW w AS (
    NFA Contexts: 3 peak, 101 total, 40 pruned
    NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+   ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Multiple items in alternation
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev61 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -2853,7 +2796,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev61'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN ((a | b | c | d) e) 
@@ -2862,7 +2805,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -2881,12 +2824,11 @@ WINDOW w AS (
    NFA Contexts: 3 peak, 101 total, 0 pruned
    NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+   ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Alternation with quantifiers
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev62 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2895,7 +2837,7 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev62'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a | b)+ c) 
@@ -2924,9 +2866,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Multiple alternatives (4+)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev63 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -2934,7 +2875,7 @@ WINDOW w AS (
     PATTERN (A | B | C | D | E)
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev63'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN (a | b | c | d | e) 
@@ -2961,9 +2902,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Alternation at start
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev64 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -2971,7 +2911,7 @@ WINDOW w AS (
     PATTERN ((A | B) C D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev64'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN ((a | b) c d) 
@@ -2999,9 +2939,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Multiple sequential alternations
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev65 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -3009,7 +2948,7 @@ WINDOW w AS (
     PATTERN ((A | B) C (D | E) F)
     DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev65'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line               
 ----------------------------------
    PATTERN ((a | b) c (d | e) f) 
@@ -3036,9 +2975,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Quantified alternatives
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev66 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3046,7 +2984,7 @@ WINDOW w AS (
     PATTERN ((A+ | B+) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev66'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN ((a+ | b+) c) 
@@ -3074,9 +3012,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Alternation at end
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev67 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3084,7 +3021,7 @@ WINDOW w AS (
     PATTERN (A B (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev67'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN (a b (c | d)) 
@@ -3112,10 +3049,9 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Nested ALT at start of branch inside outer ALT
 -- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev68 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -3123,7 +3059,7 @@ WINDOW w AS (
     PATTERN (A ((B | C) D | E))
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev68'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN (a ((b | c) d | e)) 
@@ -3150,10 +3086,9 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Nested ALT at end of branch inside outer ALT
 -- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev69 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -3161,7 +3096,7 @@ WINDOW w AS (
     PATTERN (C (A | B) | D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev69'), E'\n')) AS line WHERE line ~ 'PATTERN';
             line            
 ----------------------------
    PATTERN (c (a | b) | d) 
@@ -3188,12 +3123,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Group Pattern Tests
 -- ============================================================
 -- Simple group
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev70 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -3202,7 +3136,7 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev70'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN ((a b)+) 
@@ -3231,9 +3165,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Group with bounded quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev71 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -3242,7 +3175,7 @@ WINDOW w AS (
     PATTERN ((A B){2,4})
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev71'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a b){2,4}) 
@@ -3271,9 +3204,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Nested groups
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev72 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3282,7 +3214,7 @@ WINDOW w AS (
     PATTERN (((A B){2})+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev72'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN (((a b){2})+) 
@@ -3311,9 +3243,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Deep nesting (3+ levels)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev73 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -3321,7 +3252,7 @@ WINDOW w AS (
     PATTERN ((((A | B)+)+)+)
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev73'), E'\n')) AS line WHERE line ~ 'PATTERN';
             line             
 -----------------------------
    PATTERN ((((a | b)+)+)+) 
@@ -3349,9 +3280,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Bounded quantifier on alternation
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev74 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3359,7 +3289,7 @@ WINDOW w AS (
     PATTERN ((A | B){2,3} C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev74'), E'\n')) AS line WHERE line ~ 'PATTERN';
             line             
 -----------------------------
    PATTERN ((a | b){2,3} c) 
@@ -3387,9 +3317,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Nested groups with quantifiers
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev75 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3397,7 +3326,7 @@ WINDOW w AS (
     PATTERN (((A B)+ C)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev75'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN (((a b)+ c)*) 
@@ -3425,9 +3354,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Partial nested quantification
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev76 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3435,7 +3363,7 @@ WINDOW w AS (
     PATTERN ((A (B C)+)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev76'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN ((a (b c)+)*) 
@@ -3463,12 +3391,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Window Function Combinations
 -- ============================================================
 -- count(*) with pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev77 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -3477,7 +3404,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev77'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3506,9 +3433,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- first_value with pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev78 AS
 SELECT first_value(v) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -3517,7 +3443,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev78'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3546,9 +3472,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- last_value with pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev79 AS
 SELECT last_value(v) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -3557,7 +3482,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev79'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3586,9 +3511,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Multiple window functions
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev80 AS
 SELECT
     count(*) OVER w,
     first_value(v) OVER w,
@@ -3600,7 +3524,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev80'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3632,12 +3556,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- DEFINE Expression Variations
 -- ============================================================
 -- Complex boolean expressions
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev81 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -3648,7 +3571,7 @@ WINDOW w AS (
         A AS (v % 5 <> 0) AND (v % 3 <> 0),
         B AS (v % 5 = 0) OR (v % 3 = 0)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev81'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3679,9 +3602,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- Using PREV function
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev82 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -3693,7 +3615,7 @@ WINDOW w AS (
         U AS v > PREV(v),
         D AS v < PREV(v)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev82'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line         
 ----------------------
    PATTERN (s u+ d+) 
@@ -3724,9 +3646,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (8 rows)
 
-DROP VIEW rpr_v;
 -- Using NULL comparisons
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev83 AS
 SELECT count(*) OVER w
 FROM (
     SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
@@ -3738,7 +3659,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3770,12 +3691,11 @@ WINDOW w AS (
    ->  Function Scan on generate_series v (actual rows=30.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- ============================================================
 -- Large Scale Statistics Verification
 -- ============================================================
 -- 500 rows - verify statistics scale correctly
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev84 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3784,7 +3704,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a+ b c) 
@@ -3813,9 +3733,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- High match count scenario
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev85 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3824,7 +3743,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -3853,9 +3772,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
 -- High skip count scenario
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev86 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3869,7 +3787,7 @@ WINDOW w AS (
         D AS v % 100 = 4,
         E AS v % 100 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN (a b c d e) 
@@ -3903,7 +3821,3 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
-DROP VIEW rpr_v;
--- Cleanup
-DROP TABLE nfa_test;
-DROP TABLE nfa_complex;
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index da28de1a8f9..453860c1499 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -12,7 +12,7 @@
 --   Quantifiers Tests
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
---   Serialization/Deserialization Tests
+--   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
 --   Error Cases Tests
 --
 -- Planner Layer:
@@ -1408,8 +1408,6 @@ SELECT * FROM rpr_serial_v1 ORDER BY id;
 -- Verify deparsing
 SELECT pg_get_viewdef('rpr_serial_v1'::regclass);
 
-DROP VIEW rpr_serial_v1;
-
 -- Complex pattern with alternation
 CREATE VIEW rpr_serial_v2 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1424,8 +1422,6 @@ WINDOW w AS (
 SELECT * FROM rpr_serial_v2 ORDER BY id;
 SELECT pg_get_viewdef('rpr_serial_v2'::regclass);
 
-DROP VIEW rpr_serial_v2;
-
 -- Pattern with grouping and quantifiers
 CREATE VIEW rpr_serial_v3 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1443,8 +1439,6 @@ WINDOW w AS (
 SELECT * FROM rpr_serial_v3 ORDER BY id;
 SELECT pg_get_viewdef('rpr_serial_v3'::regclass);
 
-DROP VIEW rpr_serial_v3;
-
 -- All features combined
 CREATE VIEW rpr_serial_v4 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1465,8 +1459,6 @@ WINDOW w AS (
 SELECT * FROM rpr_serial_v4 ORDER BY id;
 SELECT pg_get_viewdef('rpr_serial_v4'::regclass);
 
-DROP VIEW rpr_serial_v4;
-
 -- Additional quantifiers for deparsing coverage
 
 -- ? quantifier (zero or one)
@@ -1483,8 +1475,6 @@ WINDOW w AS (
 SELECT * FROM rpr_serial_v5 ORDER BY id;
 SELECT pg_get_viewdef('rpr_serial_v5'::regclass);
 
-DROP VIEW rpr_serial_v5;
-
 -- {n,} quantifier (n or more)
 CREATE VIEW rpr_serial_v6 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1499,8 +1489,6 @@ WINDOW w AS (
 SELECT * FROM rpr_serial_v6 ORDER BY id;
 SELECT pg_get_viewdef('rpr_serial_v6'::regclass);
 
-DROP VIEW rpr_serial_v6;
-
 -- {n} quantifier (exactly n)
 CREATE VIEW rpr_serial_v7 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1515,8 +1503,6 @@ WINDOW w AS (
 SELECT * FROM rpr_serial_v7 ORDER BY id;
 SELECT pg_get_viewdef('rpr_serial_v7'::regclass);
 
-DROP VIEW rpr_serial_v7;
-
 -- Nested ALT pattern (tests deparse of complex nested structure)
 CREATE VIEW rpr_serial_v8 AS
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1531,8 +1517,6 @@ WINDOW w AS (
 SELECT * FROM rpr_serial_v8 ORDER BY id;
 SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
 
-DROP VIEW rpr_serial_v8;
-
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
@@ -1543,7 +1527,6 @@ WINDOW w AS (ORDER BY id
              PATTERN (A{1}? B)
              DEFINE A AS val > 0, B AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
-DROP VIEW rpr_quant_reluctant_v;
 
 -- Materialized view (if supported)
 
@@ -1567,9 +1550,6 @@ SELECT pg_get_viewdef('rpr_mview_v1'::regclass);
 REFRESH MATERIALIZED VIEW rpr_mview_v1;
 SELECT * FROM rpr_mview_v1 ORDER BY id;
 
-DROP MATERIALIZED VIEW rpr_mview_v1;
-DROP TABLE rpr_mview;
-
 -- CREATE TABLE AS SELECT with RPR
 CREATE TABLE rpr_ctas (id INT, val INT);
 INSERT INTO rpr_ctas VALUES (1, 10), (2, 20), (3, 15), (4, 25);
@@ -1863,9 +1843,6 @@ WINDOW
 SELECT * FROM rpr_multiwin_v ORDER BY id;
 SELECT pg_get_viewdef('rpr_multiwin_v'::regclass);
 
-DROP VIEW rpr_multiwin_v;
-DROP TABLE rpr_multiwin;
-
 -- {n} quantifier display in view
 CREATE VIEW rpr_quant_n_v AS
 SELECT id, val, count(*) OVER w
@@ -1876,7 +1853,6 @@ WINDOW w AS (ORDER BY id
              PATTERN (A{3})
              DEFINE A AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_n_v'::regclass);
-DROP VIEW rpr_quant_n_v;
 
 -- {n,} quantifier display in view
 CREATE VIEW rpr_quant_n_plus_v AS
@@ -1888,9 +1864,6 @@ WINDOW w AS (ORDER BY id
              PATTERN (A{2,})
              DEFINE A AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
-DROP VIEW rpr_quant_n_plus_v;
-
-DROP TABLE rpr_serial;
 
 -- ============================================================
 -- Error Cases Tests
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 4bb49650bb7..8e22382a68e 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -3,6 +3,9 @@
 -- Tests for Row Pattern Recognition EXPLAIN output
 -- ============================================================
 --
+-- Views and tables in this file are intentionally not dropped,
+-- so that pg_upgrade/pg_dump can test RPR syntax serialization.
+--
 -- This test suite validates EXPLAIN output for RPR queries,
 -- including NFA statistics shown in EXPLAIN ANALYZE:
 --   - NFA States: peak, total, merged
@@ -76,14 +79,14 @@ end;
 $$;
 
 -- Setup: Create test tables
-CREATE TEMP TABLE nfa_test (
+CREATE TABLE rpr_nfa_test (
     id serial,
     v int,
     cat char(1)
 );
 
 -- Insert test data: 100 rows with predictable pattern
-INSERT INTO nfa_test (v, cat)
+INSERT INTO rpr_nfa_test (v, cat)
 SELECT i,
        CASE
            WHEN i % 5 = 1 THEN 'A'
@@ -95,13 +98,13 @@ SELECT i,
 FROM generate_series(1, 100) i;
 
 -- Additional test table with more complex patterns
-CREATE TEMP TABLE nfa_complex (
+CREATE TABLE rpr_nfa_complex (
     id serial,
     price int,
     trend char(1)  -- U=up, D=down, S=stable
 );
 
-INSERT INTO nfa_complex (price, trend)
+INSERT INTO rpr_nfa_complex (price, trend)
 VALUES
     (100, 'S'), (105, 'U'), (110, 'U'), (108, 'D'), (112, 'U'),
     (115, 'U'), (113, 'D'), (111, 'D'), (109, 'D'), (110, 'U'),
@@ -115,77 +118,74 @@ VALUES
 -- ============================================================
 
 -- Simple pattern - should show basic statistics
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev01 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS cat = 'A', B AS cat = 'B'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev01'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS cat = ''A'', B AS cat = ''B''
 )');
-DROP VIEW rpr_v;
 
 -- Pattern with no matches - 0 matched
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev02 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (X Y Z)
     DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev02'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (X Y Z)
     DEFINE X AS cat = ''X'', Y AS cat = ''Y'', Z AS cat = ''Z''
 );');
-DROP VIEW rpr_v;
 
 -- Pattern matching every row - high match count
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev03 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (R)
     DEFINE R AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev03'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (R)
     DEFINE R AS TRUE
 );');
-DROP VIEW rpr_v;
 
 -- Regression test: Space before parenthesis in pattern deparse
 -- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev04 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -193,7 +193,7 @@ WINDOW w AS (
     PATTERN (A (B | C))
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev04'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -203,12 +203,11 @@ WINDOW w AS (
     PATTERN (A (B | C))
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Regression test: Sequential alternations at same depth
 -- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
 -- Previously failed due to missing parentheses on ALT depth decrease
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev05 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -216,7 +215,7 @@ WINDOW w AS (
     PATTERN (A ((B | C) (D | E))*)
     DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev05'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -226,14 +225,13 @@ WINDOW w AS (
     PATTERN (A ((B | C) (D | E))*)
     DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- State Statistics Tests (peak, total, merged)
 -- ============================================================
 
 -- Simple quantifier pattern - A+ with short matches (no merging)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev06 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -242,7 +240,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev06'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -253,12 +251,11 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 2 = 1
 );');
-DROP VIEW rpr_v;
 
 -- Alternation pattern - multiple state branches
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev07 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -267,11 +264,11 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev07'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -280,10 +277,9 @@ WINDOW w AS (
         A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
         D AS cat = ''D'', E AS cat = ''E''
 );');
-DROP VIEW rpr_v;
 
 -- Complex pattern with high state count
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev08 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -295,7 +291,7 @@ WINDOW w AS (
         B AS v % 3 = 2,
         C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev08'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -309,10 +305,9 @@ WINDOW w AS (
         B AS v % 3 = 2,
         C AS v % 3 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Grouped pattern with quantifier - state count with grouping
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev09 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -321,7 +316,7 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev09'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -332,11 +327,10 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
-DROP VIEW rpr_v;
 
 -- State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev10 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -345,7 +339,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev10'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -356,11 +350,10 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Consecutive ALT merge followed by different ALT
 -- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev11 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -369,7 +362,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev11'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -380,11 +373,10 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
-DROP VIEW rpr_v;
 
 -- Consecutive ALT merge followed by non-ALT element
 -- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev12 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -393,7 +385,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev12'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -404,10 +396,9 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-DROP VIEW rpr_v;
 
 -- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev13 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -416,7 +407,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B)+ (A | B))
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev13'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -427,10 +418,9 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B)+ (A | B))
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );');
-DROP VIEW rpr_v;
 
 -- High state count - alternation with plus quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev14 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -439,7 +429,7 @@ WINDOW w AS (
     PATTERN ((A | B | C)+ D)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev14'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -450,11 +440,10 @@ WINDOW w AS (
     PATTERN ((A | B | C)+ D)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Early termination: first ALT branch (A) reaches FIN immediately,
 -- pruning second branch (A B+) before it can accumulate B repetitions.
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev15 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -463,7 +452,7 @@ WINDOW w AS (
     PATTERN ((A | A B)+)
     DEFINE A AS v = 1, B AS v > 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev15'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -474,10 +463,9 @@ WINDOW w AS (
     PATTERN ((A | A B)+)
     DEFINE A AS v = 1, B AS v > 1
 );');
-DROP VIEW rpr_v;
 
 -- Nested quantifiers causing state growth
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev16 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -486,7 +474,7 @@ WINDOW w AS (
     PATTERN (((A | B)+)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev16'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -497,14 +485,13 @@ WINDOW w AS (
     PATTERN (((A | B)+)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
 
 -- Context absorption with unbounded quantifier at start
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev17 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -513,7 +500,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev17'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -524,10 +511,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- No absorption - bounded quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev18 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -536,7 +522,7 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev18'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -547,10 +533,9 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Contexts skipped by SKIP PAST LAST ROW
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev19 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -559,7 +544,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev19'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -570,10 +555,9 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
 );');
-DROP VIEW rpr_v;
 
 -- High context absorption - unbounded group
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev20 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -582,7 +566,7 @@ WINDOW w AS (
     PATTERN ((A B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -593,16 +577,15 @@ WINDOW w AS (
     PATTERN ((A B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Match Length Statistics Tests
 -- ============================================================
 
 -- Fixed length matches - all same length
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev21 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -611,11 +594,11 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev21'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -624,10 +607,9 @@ WINDOW w AS (
         A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
         D AS cat = ''D'', E AS cat = ''E''
 );');
-DROP VIEW rpr_v;
 
 -- Variable length matches - min/max/avg differ
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev22 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -636,7 +618,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev22'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -647,10 +629,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Very long matches
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev23 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 200) AS s(v)
 WINDOW w AS (
@@ -659,7 +640,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v <= 195, B AS v > 195
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev23'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -670,10 +651,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v <= 195, B AS v > 195
 );');
-DROP VIEW rpr_v;
 
 -- Uniform match length with mismatches from gap rows (v%20 = 11..15)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev24 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -684,7 +664,7 @@ WINDOW w AS (
         A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
         B AS v % 20 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev24'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -697,7 +677,6 @@ WINDOW w AS (
         A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
         B AS v % 20 = 0
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Mismatch Length Statistics Tests
@@ -705,7 +684,7 @@ DROP VIEW rpr_v;
 
 -- Pattern with complete match every cycle: 0 mismatched
 -- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev25 AS
 SELECT count(*) OVER w
 FROM (
     SELECT v,
@@ -721,7 +700,7 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev25'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -739,10 +718,9 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
 );');
-DROP VIEW rpr_v;
 
 -- Long partial matches that fail
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev26 AS
 SELECT count(*) OVER w
 FROM (
     SELECT i AS v,
@@ -763,7 +741,7 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev26'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -786,14 +764,13 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- JSON Format Tests
 -- ============================================================
 
 -- JSON format output with all statistics
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev27 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -802,7 +779,7 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev27'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -813,10 +790,9 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 )');
-DROP VIEW rpr_v;
 
 -- JSON format with match length statistics
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev28 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -825,7 +801,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev28'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -836,11 +812,10 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 )');
-DROP VIEW rpr_v;
 
 -- JSON format with mismatch statistics
 -- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev29 AS
 SELECT count(*) OVER w
 FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
 WINDOW w AS (
@@ -849,7 +824,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v = 1, B AS v = 2, C AS v = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev29'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -860,11 +835,10 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v = 1, B AS v = 2, C AS v = 3
 )');
-DROP VIEW rpr_v;
 
 -- JSON format with skipped context statistics
 -- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev30 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -873,7 +847,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev30'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -884,14 +858,13 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 )');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- XML Format Tests
 -- ============================================================
 
 -- XML format output
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev31 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -900,7 +873,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev31'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
 SELECT count(*) OVER w
@@ -911,14 +884,13 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 )');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Multiple Partitions Tests
 -- ============================================================
 
 -- Statistics across multiple partitions
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev32 AS
 SELECT count(*) OVER w
 FROM (
     SELECT p, v
@@ -932,7 +904,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev32'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -948,10 +920,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Different pattern behavior per partition
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev33 AS
 SELECT count(*) OVER w
 FROM (
     SELECT
@@ -966,7 +937,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS val < 5, B AS val >= 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev33'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -983,14 +954,13 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS val < 5, B AS val >= 5
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Edge Cases
 -- ============================================================
 
 -- Empty result set
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev34 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 0) AS s(v)
 WINDOW w AS (
@@ -999,7 +969,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v = 1, B AS v = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev34'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1010,10 +980,9 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v = 1, B AS v = 2
 );');
-DROP VIEW rpr_v;
 
 -- Single row
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev35 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1) AS s(v)
 WINDOW w AS (
@@ -1022,7 +991,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev35'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1033,10 +1002,9 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS TRUE
 );');
-DROP VIEW rpr_v;
 
 -- Pattern longer than data
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev36 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 5) AS s(v)
 WINDOW w AS (
@@ -1047,7 +1015,7 @@ WINDOW w AS (
         A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
         F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev36'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1060,10 +1028,9 @@ WINDOW w AS (
         A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
         F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
 );');
-DROP VIEW rpr_v;
 
 -- All rows match as single match
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev37 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1072,7 +1039,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev37'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1083,14 +1050,13 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS TRUE
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Complex Pattern Tests
 -- ============================================================
 
 -- Nested groups
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev38 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1099,7 +1065,7 @@ WINDOW w AS (
     PATTERN (((A B) C)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev38'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1110,12 +1076,11 @@ WINDOW w AS (
     PATTERN (((A B) C)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Multiple alternations
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev39 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -1124,11 +1089,11 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev39'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -1137,10 +1102,9 @@ WINDOW w AS (
         A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
         D AS cat = ''D'', E AS cat = ''E''
 );');
-DROP VIEW rpr_v;
 
 -- Optional elements
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev40 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1149,7 +1113,7 @@ WINDOW w AS (
     PATTERN (A B? C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev40'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1160,10 +1124,9 @@ WINDOW w AS (
     PATTERN (A B? C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );');
-DROP VIEW rpr_v;
 
 -- Bounded quantifiers
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev41 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1172,7 +1135,7 @@ WINDOW w AS (
     PATTERN (A{2,5} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev41'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1183,10 +1146,9 @@ WINDOW w AS (
     PATTERN (A{2,5} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Star quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev42 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1195,7 +1157,7 @@ WINDOW w AS (
     PATTERN (A B* C)
     DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev42'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1206,60 +1168,57 @@ WINDOW w AS (
     PATTERN (A B* C)
     DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Real-world Pattern Examples
 -- ============================================================
 
 -- Stock price pattern - V-shape (down then up)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev43 AS
 SELECT count(*) OVER w
-FROM nfa_complex
+FROM rpr_nfa_complex
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (D+ U+)
     DEFINE D AS trend = 'D', U AS trend = 'U'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev43'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_complex
+FROM rpr_nfa_complex
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (D+ U+)
     DEFINE D AS trend = ''D'', U AS trend = ''U''
 );');
-DROP VIEW rpr_v;
 
 -- Stock price pattern - peak (up, stable, down)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev44 AS
 SELECT count(*) OVER w
-FROM nfa_complex
+FROM rpr_nfa_complex
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (U+ S* D+)
     DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev44'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_complex
+FROM rpr_nfa_complex
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (U+ S* D+)
     DEFINE U AS trend = ''U'', S AS trend = ''S'', D AS trend = ''D''
 );');
-DROP VIEW rpr_v;
 
 -- Consecutive increasing values (using PREV)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev45 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1268,7 +1227,7 @@ WINDOW w AS (
     PATTERN (A{3,})
     DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev45'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1279,14 +1238,13 @@ WINDOW w AS (
     PATTERN (A{3,})
     DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Performance-oriented Tests
 -- ============================================================
 
 -- Large dataset with simple pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev46 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -1295,7 +1253,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev46'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1306,10 +1264,9 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Large dataset with absorption
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev47 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -1318,7 +1275,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev47'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1329,10 +1286,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
 );');
-DROP VIEW rpr_v;
 
 -- High state merge ratio
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev48 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -1341,7 +1297,7 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev48'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1352,14 +1308,13 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- INITIAL vs no INITIAL comparison
 -- ============================================================
 
 -- With INITIAL keyword
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev49 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1369,7 +1324,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev49'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1381,10 +1336,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Without INITIAL keyword (same behavior currently)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev50 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1393,7 +1347,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev50'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1404,14 +1358,13 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Quantifier Variations
 -- ============================================================
 
 -- Plus quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev51 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1420,7 +1373,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 4 <> 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev51'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1431,10 +1384,9 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 4 <> 0
 );');
-DROP VIEW rpr_v;
 
 -- Star quantifier (zero or more)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev52 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1443,7 +1395,7 @@ WINDOW w AS (
     PATTERN (A* B)
     DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev52'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1454,10 +1406,9 @@ WINDOW w AS (
     PATTERN (A* B)
     DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
 );');
-DROP VIEW rpr_v;
 
 -- Question mark (zero or one)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev53 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1466,7 +1417,7 @@ WINDOW w AS (
     PATTERN (A? B C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev53'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1477,10 +1428,9 @@ WINDOW w AS (
     PATTERN (A? B C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );');
-DROP VIEW rpr_v;
 
 -- Exact count {n}
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev54 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1489,7 +1439,7 @@ WINDOW w AS (
     PATTERN (A{3} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev54'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1500,10 +1450,9 @@ WINDOW w AS (
     PATTERN (A{3} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Range {n,m}
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev55 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1512,7 +1461,7 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev55'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1523,10 +1472,9 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- At least {n,}
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev56 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1535,7 +1483,7 @@ WINDOW w AS (
     PATTERN (A{3,} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev56'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1546,7 +1494,6 @@ WINDOW w AS (
     PATTERN (A{3,} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Regression Tests for Statistics Accuracy
@@ -1554,7 +1501,7 @@ DROP VIEW rpr_v;
 
 -- Verify state count accuracy
 -- Pattern A+ B with 20 rows should show predictable state behavior
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev57 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -1563,7 +1510,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev57'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1574,10 +1521,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Verify context count with known absorption
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev58 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -1586,7 +1532,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev58'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1597,10 +1543,9 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
 );');
-DROP VIEW rpr_v;
 
 -- Verify match length with fixed-length pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev59 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -1609,7 +1554,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev59'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1620,39 +1565,37 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Alternation Pattern Tests
 -- ============================================================
 
 -- Simple alternation
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev60 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A | B) C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev60'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A | B) C)
     DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
 );');
-DROP VIEW rpr_v;
 
 -- Multiple items in alternation
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev61 AS
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -1661,11 +1604,11 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev61'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
-FROM nfa_test
+FROM rpr_nfa_test
 WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
@@ -1674,10 +1617,9 @@ WINDOW w AS (
         A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
         D AS cat = ''D'', E AS cat = ''E''
 );');
-DROP VIEW rpr_v;
 
 -- Alternation with quantifiers
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev62 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1686,7 +1628,7 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev62'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1697,10 +1639,9 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Multiple alternatives (4+)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev63 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1708,7 +1649,7 @@ WINDOW w AS (
     PATTERN (A | B | C | D | E)
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev63'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1718,10 +1659,9 @@ WINDOW w AS (
     PATTERN (A | B | C | D | E)
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );');
-DROP VIEW rpr_v;
 
 -- Alternation at start
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev64 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1729,7 +1669,7 @@ WINDOW w AS (
     PATTERN ((A | B) C D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev64'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1739,10 +1679,9 @@ WINDOW w AS (
     PATTERN ((A | B) C D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
-DROP VIEW rpr_v;
 
 -- Multiple sequential alternations
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev65 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1750,7 +1689,7 @@ WINDOW w AS (
     PATTERN ((A | B) C (D | E) F)
     DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev65'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1760,10 +1699,9 @@ WINDOW w AS (
     PATTERN ((A | B) C (D | E) F)
     DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
 );');
-DROP VIEW rpr_v;
 
 -- Quantified alternatives
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev66 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1771,7 +1709,7 @@ WINDOW w AS (
     PATTERN ((A+ | B+) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev66'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1781,10 +1719,9 @@ WINDOW w AS (
     PATTERN ((A+ | B+) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-DROP VIEW rpr_v;
 
 -- Alternation at end
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev67 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1792,7 +1729,7 @@ WINDOW w AS (
     PATTERN (A B (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev67'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1802,11 +1739,10 @@ WINDOW w AS (
     PATTERN (A B (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
-DROP VIEW rpr_v;
 
 -- Nested ALT at start of branch inside outer ALT
 -- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev68 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -1814,7 +1750,7 @@ WINDOW w AS (
     PATTERN (A ((B | C) D | E))
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev68'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1824,11 +1760,10 @@ WINDOW w AS (
     PATTERN (A ((B | C) D | E))
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );');
-DROP VIEW rpr_v;
 
 -- Nested ALT at end of branch inside outer ALT
 -- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev69 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -1836,7 +1771,7 @@ WINDOW w AS (
     PATTERN (C (A | B) | D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev69'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1846,14 +1781,13 @@ WINDOW w AS (
     PATTERN (C (A | B) | D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Group Pattern Tests
 -- ============================================================
 
 -- Simple group
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev70 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1862,7 +1796,7 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev70'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1873,10 +1807,9 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Group with bounded quantifier
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev71 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1885,7 +1818,7 @@ WINDOW w AS (
     PATTERN ((A B){2,4})
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev71'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1896,10 +1829,9 @@ WINDOW w AS (
     PATTERN ((A B){2,4})
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Nested groups
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev72 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1908,7 +1840,7 @@ WINDOW w AS (
     PATTERN (((A B){2})+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev72'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1919,10 +1851,9 @@ WINDOW w AS (
     PATTERN (((A B){2})+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Deep nesting (3+ levels)
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev73 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1930,7 +1861,7 @@ WINDOW w AS (
     PATTERN ((((A | B)+)+)+)
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev73'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1940,10 +1871,9 @@ WINDOW w AS (
     PATTERN ((((A | B)+)+)+)
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );');
-DROP VIEW rpr_v;
 
 -- Bounded quantifier on alternation
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev74 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1951,7 +1881,7 @@ WINDOW w AS (
     PATTERN ((A | B){2,3} C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev74'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1961,10 +1891,9 @@ WINDOW w AS (
     PATTERN ((A | B){2,3} C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-DROP VIEW rpr_v;
 
 -- Nested groups with quantifiers
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev75 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1972,7 +1901,7 @@ WINDOW w AS (
     PATTERN (((A B)+ C)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev75'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1982,10 +1911,9 @@ WINDOW w AS (
     PATTERN (((A B)+ C)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-DROP VIEW rpr_v;
 
 -- Partial nested quantification
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev76 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1993,7 +1921,7 @@ WINDOW w AS (
     PATTERN ((A (B C)+)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev76'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2003,14 +1931,13 @@ WINDOW w AS (
     PATTERN ((A (B C)+)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Window Function Combinations
 -- ============================================================
 
 -- count(*) with pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev77 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2019,7 +1946,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev77'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2030,10 +1957,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- first_value with pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev78 AS
 SELECT first_value(v) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2042,7 +1968,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev78'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT first_value(v) OVER w
@@ -2053,10 +1979,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- last_value with pattern
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev79 AS
 SELECT last_value(v) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2065,7 +1990,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev79'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT last_value(v) OVER w
@@ -2076,10 +2001,9 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- Multiple window functions
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev80 AS
 SELECT
     count(*) OVER w,
     first_value(v) OVER w,
@@ -2091,7 +2015,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev80'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT
@@ -2105,14 +2029,13 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- DEFINE Expression Variations
 -- ============================================================
 
 -- Complex boolean expressions
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev81 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2123,7 +2046,7 @@ WINDOW w AS (
         A AS (v % 5 <> 0) AND (v % 3 <> 0),
         B AS (v % 5 = 0) OR (v % 3 = 0)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev81'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2136,10 +2059,9 @@ WINDOW w AS (
         A AS (v % 5 <> 0) AND (v % 3 <> 0),
         B AS (v % 5 = 0) OR (v % 3 = 0)
 );');
-DROP VIEW rpr_v;
 
 -- Using PREV function
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev82 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2151,7 +2073,7 @@ WINDOW w AS (
         U AS v > PREV(v),
         D AS v < PREV(v)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev82'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2165,10 +2087,9 @@ WINDOW w AS (
         U AS v > PREV(v),
         D AS v < PREV(v)
 );');
-DROP VIEW rpr_v;
 
 -- Using NULL comparisons
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev83 AS
 SELECT count(*) OVER w
 FROM (
     SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
@@ -2180,7 +2101,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2194,14 +2115,13 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
 );');
-DROP VIEW rpr_v;
 
 -- ============================================================
 -- Large Scale Statistics Verification
 -- ============================================================
 
 -- 500 rows - verify statistics scale correctly
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev84 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2210,7 +2130,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2221,10 +2141,9 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
 );');
-DROP VIEW rpr_v;
 
 -- High match count scenario
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev85 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2233,7 +2152,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2244,10 +2163,9 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
-DROP VIEW rpr_v;
 
 -- High skip count scenario
-CREATE TEMP VIEW rpr_v AS
+CREATE VIEW rpr_ev86 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2261,7 +2179,7 @@ WINDOW w AS (
         D AS v % 100 = 4,
         E AS v % 100 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2277,8 +2195,4 @@ WINDOW w AS (
         D AS v % 100 = 4,
         E AS v % 100 = 5
 );');
-DROP VIEW rpr_v;
 
--- Cleanup
-DROP TABLE nfa_test;
-DROP TABLE nfa_complex;
-- 
2.50.1 (Apple Git-155)

From 81a9b83b9d61704e85d97bc79b10ac6a00d73def Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 4 Mar 2026 15:53:33 +0900
Subject: [PATCH 5/8] Disable run condition pushdown for RPR windows

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 90275e25872..b67c35af39a 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -2453,6 +2453,17 @@ find_window_run_conditions(Query *subquery, AttrNumber attno,
 	wclause = (WindowClause *) list_nth(subquery->windowClause,
 										wfunc->winref - 1);
 
+	/*
+	 * If a DEFINE clause exists, we cannot push down a run condition. In the
+	 * case, a window partition (or frame) is divided into multiple reduced
+	 * frames and each frame should be evaluated to the end of the partition
+	 * (or full frame end). This means we cannot apply the run condition
+	 * optimization because it stops evaluation window functions in certain
+	 * cases.
+	 */
+	if (wclause->defineClause != NIL)
+		return false;
+
 	req.type = T_SupportRequestWFuncMonotonic;
 	req.window_func = wfunc;
 	req.window_clause = wclause;
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index ae6d9c9b937..a7c536625f1 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2465,7 +2465,9 @@ WHERE cnt > 0
 ORDER BY id;
  id | val | cnt 
 ----+-----+-----
-(0 rows)
+  2 |  20 |   2
+  4 |  40 |   1
+(2 rows)
 
 -- Nested subqueries
 SELECT *
-- 
2.50.1 (Apple Git-155)

From 08910348e4c3770880fa098b50596805b15e38f7 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 4 Mar 2026 15:59:45 +0900
Subject: [PATCH 6/8] Disable frame optimization for RPR windows

diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 04faf919033..29a02f3affb 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -5907,6 +5907,14 @@ optimize_window_clauses(PlannerInfo *root, WindowFuncLists *wflists)
 		if (wflists->windowFuncs[wc->winref] == NIL)
 			continue;
 
+		/*
+		 * If a DEFINE clause exists, do not let support functions replace the
+		 * frame with a non-RPR-compatible one.  RPR windows require ROWS
+		 * BETWEEN CURRENT ROW AND ...
+		 */
+		if (wc->defineClause != NIL)
+			continue;
+
 		foreach(lc2, wflists->windowFuncs[wc->winref])
 		{
 			SupportRequestOptimizeWindowClause req;
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 3c70a12874a..ef184b7950b 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3821,3 +3821,117 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
+--
+-- Planner optimization: optimize_window_clauses must not alter RPR frame
+--
+-- optimize_window_clauses() replaces frame options via prosupport functions.
+-- Affected functions: row_number, rank, dense_rank, percent_rank, cume_dist,
+-- ntile.  All would change the frame to ROWS UNBOUNDED PRECEDING, breaking
+-- RPR's required ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING.
+-- Test with row_number() as representative case.
+--
+-- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
+CREATE VIEW rpr_ev87 AS
+SELECT row_number() OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ORDER BY v
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+);
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Subquery Scan on rpr_ev87
+   ->  WindowAgg
+         Window: w AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING)
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(6 rows)
+
+-- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+CREATE VIEW rpr_ev88 AS
+SELECT row_number() OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ORDER BY v
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        B AS v > PREV(v)
+);
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Subquery Scan on rpr_ev88
+   ->  WindowAgg
+         Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(7 rows)
+
+--
+-- Planner optimization: find_window_run_conditions must not push down
+-- RPR window function results as Run Conditions.
+--
+-- 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:
+--   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.
+--
+-- Without RPR: count(*) > 0 is pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM generate_series(1, 10) AS s(v)
+    WINDOW w AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    )
+) t WHERE cnt > 0;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Subquery Scan on t
+   ->  WindowAgg
+         Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Run Condition: (count(*) OVER w > 0)
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(7 rows)
+
+-- With RPR: count(*) > 0 must not be pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM generate_series(1, 10) AS s(v)
+    WINDOW w AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (A B+)
+        DEFINE
+            B AS v > PREV(v)
+    )
+) t WHERE cnt > 0;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Subquery Scan on t
+   Filter: (t.cnt > 0)
+   ->  WindowAgg
+         Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(8 rows)
+
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 8e22382a68e..640d328957b 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2196,3 +2196,81 @@ WINDOW w AS (
         E AS v % 100 = 5
 );');
 
+--
+-- Planner optimization: optimize_window_clauses must not alter RPR frame
+--
+-- optimize_window_clauses() replaces frame options via prosupport functions.
+-- Affected functions: row_number, rank, dense_rank, percent_rank, cume_dist,
+-- ntile.  All would change the frame to ROWS UNBOUNDED PRECEDING, breaking
+-- RPR's required ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING.
+-- Test with row_number() as representative case.
+--
+
+-- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
+CREATE VIEW rpr_ev87 AS
+SELECT row_number() OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ORDER BY v
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+);
+
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+
+-- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+CREATE VIEW rpr_ev88 AS
+SELECT row_number() OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ORDER BY v
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        B AS v > PREV(v)
+);
+
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+
+--
+-- Planner optimization: find_window_run_conditions must not push down
+-- RPR window function results as Run Conditions.
+--
+-- 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:
+--   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.
+--
+
+-- Without RPR: count(*) > 0 is pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM generate_series(1, 10) AS s(v)
+    WINDOW w AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    )
+) t WHERE cnt > 0;
+
+-- With RPR: count(*) > 0 must not be pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM generate_series(1, 10) AS s(v)
+    WINDOW w AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (A B+)
+        DEFINE
+            B AS v > PREV(v)
+    )
+) t WHERE cnt > 0;
+
-- 
2.50.1 (Apple Git-155)

From a276a393cae83a4839ff7482fa5a6d3e9fde49cb Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 22:17:29 +0900
Subject: [PATCH] Add stock scenario tests for RPR pattern matching

diff --git a/src/test/regress/data/stock.data b/src/test/regress/data/stock.data
new file mode 100644
index 00000000000..645bb643df7
--- /dev/null
+++ b/src/test/regress/data/stock.data
@@ -0,0 +1,1632 @@
+1	1	364.750	15800	0.000	0.000	0.000
+1	2	368.250	21200	0.000	0.000	0.000
+1	3	368.500	24400	0.000	0.000	0.000
+1	4	368.750	22900	0.000	0.000	0.000
+1	5	369.500	35400	0.000	0.000	0.000
+1	6	369.000	29300	0.000	0.000	0.000
+1	7	367.750	19000	0.000	0.000	0.000
+1	8	374.125	37200	0.000	0.000	0.000
+2	9	343.250	56800	0.000	0.000	0.000
+2	10	338.500	45200	0.000	0.000	0.000
+2	11	338.750	40900	0.000	0.000	0.000
+2	12	344.750	35200	0.000	0.000	0.000
+2	13	349.750	28500	0.000	0.000	0.000
+2	14	346.000	25600	0.000	0.000	0.000
+3	15	349.000	24900	0.000	0.000	0.000
+3	16	349.250	20700	0.000	0.000	0.000
+3	17	349.750	22200	0.000	0.000	0.000
+3	18	351.250	19600	0.000	0.000	0.000
+3	19	352.000	32500	0.000	0.000	0.000
+3	20	349.500	32200	0.000	0.000	0.000
+3	21	344.500	18600	0.000	0.000	0.000
+3	22	345.000	61200	0.000	0.000	0.000
+4	23	238.250	37100	0.000	0.000	0.000
+4	24	230.625	61500	0.000	0.000	0.000
+4	25	223.000	161700	0.000	0.000	0.000
+4	26	229.875	55300	0.000	0.000	0.000
+4	27	235.250	38500	0.000	0.000	0.000
+5	28	310.000	36000	0.000	0.000	0.000
+5	29	311.750	26800	0.000	0.000	0.000
+5	30	312.750	31900	0.000	0.000	0.000
+5	31	314.750	21100	0.000	0.000	0.000
+5	32	315.500	27400	0.000	0.000	0.000
+5	33	316.500	40700	0.000	0.000	0.000
+5	34	321.750	53500	0.000	0.000	0.000
+5	35	326.000	48700	0.000	0.000	0.000
+5	36	322.500	46000	0.000	0.000	0.000
+5	37	317.500	35600	0.000	0.000	0.000
+5	38	325.500	34000	0.000	0.000	0.000
+5	39	327.250	30700	0.000	0.000	0.000
+5	40	327.750	36700	0.000	0.000	0.000
+5	41	328.625	23200	0.000	0.000	0.000
+5	42	330.500	29700	0.000	0.000	0.000
+5	43	336.500	39200	0.000	0.000	0.000
+5	44	340.750	34400	0.000	0.000	0.000
+5	45	338.500	32200	0.000	0.000	0.000
+6	46	353.000	27800	0.000	0.000	0.000
+6	47	352.000	55600	0.000	0.000	0.000
+6	48	355.000	31300	0.000	0.000	0.000
+6	49	356.000	26900	0.000	0.000	0.000
+6	50	358.000	18900	0.000	0.000	0.000
+6	51	361.250	26400	0.000	0.000	0.000
+7	52	311.750	25100	0.000	0.000	0.000
+7	53	314.000	37900	0.000	0.000	0.000
+7	54	317.000	34000	0.000	0.000	0.000
+7	55	317.750	23700	0.000	0.000	0.000
+7	56	319.000	23400	0.000	0.000	0.000
+7	57	316.500	24200	0.000	0.000	0.000
+8	58	316.000	37200	0.000	0.000	0.000
+8	59	315.000	31500	0.000	0.000	0.000
+8	60	302.000	124700	0.000	0.000	0.000
+8	61	300.000	196100	0.000	0.000	0.000
+8	62	298.000	70000	0.000	0.000	0.000
+8	63	294.500	88200	0.000	0.000	0.000
+8	64	296.500	53800	0.000	0.000	0.000
+8	65	299.000	47900	0.000	0.000	0.000
+8	66	301.750	33300	0.000	0.000	0.000
+8	67	299.500	31800	0.000	0.000	0.000
+9	68	299.500	76700	0.000	0.000	0.000
+9	69	296.750	41900	0.000	0.000	0.000
+9	70	291.250	46700	0.000	0.000	0.000
+9	71	293.750	31100	0.000	0.000	0.000
+9	72	290.000	48900	0.000	0.000	0.000
+9	73	287.000	56300	0.000	0.000	0.000
+9	74	287.000	40700	0.000	0.000	0.000
+9	75	285.500	27900	0.000	0.000	0.000
+9	76	284.000	27400	0.000	0.000	0.000
+9	77	286.000	23500	0.000	0.000	0.000
+9	78	291.250	38000	0.000	0.000	0.000
+9	79	296.750	42900	0.000	0.000	0.000
+9	80	295.000	19500	0.000	0.000	0.000
+9	81	314.000	102900	0.000	0.000	0.000
+9	82	315.750	84100	0.000	0.000	0.000
+10	83	302.000	44600	0.000	0.000	0.000
+10	84	301.000	45000	0.000	0.000	0.000
+10	85	294.750	27900	0.000	0.000	0.000
+10	86	293.500	17600	0.000	0.000	0.000
+10	87	291.750	21600	0.000	0.000	0.000
+10	88	290.500	30300	0.000	0.000	0.000
+10	89	291.750	22700	0.000	0.000	0.000
+10	90	298.750	16200	0.000	0.000	0.000
+10	91	302.250	49100	0.000	0.000	0.000
+10	92	305.500	35500	0.000	0.000	0.000
+10	93	309.750	63200	0.000	0.000	0.000
+10	94	309.500	43200	0.000	0.000	0.000
+11	95	383.750	19300	0.000	0.000	0.000
+11	96	389.500	28900	0.000	0.000	0.000
+11	97	394.500	41700	0.000	0.000	0.000
+11	98	396.250	28200	0.000	0.000	0.000
+11	99	401.000	55000	0.000	0.000	0.000
+11	100	402.000	41100	0.000	0.000	0.000
+11	101	403.000	38500	0.000	0.000	0.000
+11	102	404.000	33100	0.000	0.000	0.000
+11	103	404.000	33800	0.000	0.000	0.000
+12	104	388.000	25800	0.000	0.000	0.000
+12	105	388.500	44900	0.000	0.000	0.000
+12	106	390.000	38400	0.000	0.000	0.000
+12	107	390.750	23200	0.000	0.000	0.000
+12	108	396.500	25100	0.000	0.000	0.000
+12	109	398.750	38000	0.000	0.000	0.000
+12	110	398.000	33600	0.000	0.000	0.000
+12	111	394.500	54000	0.000	0.000	0.000
+12	112	397.000	20900	0.000	0.000	0.000
+13	113	398.125	28500	0.000	396.000	401.000
+13	114	394.500	39900	0.000	392.500	397.500
+13	115	390.000	37600	0.000	390.000	393.750
+13	116	389.500	31000	0.000	386.000	392.750
+13	117	385.500	25800	0.000	385.500	389.750
+13	118	390.500	18000	0.000	385.000	390.500
+13	119	391.750	14600	0.000	391.500	395.500
+13	120	399.000	27800	0.000	392.250	399.000
+13	121	402.000	46500	0.000	399.000	403.000
+13	122	409.000	36300	0.000	404.500	409.500
+13	123	409.250	54500	0.000	409.250	413.750
+13	124	411.750	46200	0.000	406.000	412.500
+13	125	418.750	60200	0.000	412.750	419.500
+13	126	418.750	32700	0.000	416.000	420.000
+14	127	411.500	24800	0.000	411.250	415.500
+14	128	415.250	53000	0.000	412.000	423.000
+14	129	420.000	49700	0.000	417.250	425.000
+14	130	423.250	68700	0.000	422.750	429.000
+14	131	422.500	45400	0.000	420.750	424.750
+14	132	424.000	20900	0.000	420.750	424.250
+14	133	428.875	37600	0.000	427.000	430.000
+14	134	439.000	59800	0.000	427.250	439.000
+15	135	448.750	29300	0.000	444.750	449.500
+15	136	444.000	27400	0.000	444.000	449.750
+15	137	443.500	30900	0.000	440.500	448.500
+15	138	437.875	26500	0.000	437.500	442.875
+15	139	437.750	29000	0.000	433.500	440.000
+15	140	430.500	27500	0.000	430.250	441.250
+15	141	425.500	52300	0.000	424.250	428.880
+15	142	428.000	59900	0.000	419.500	429.000
+15	143	432.500	34600	0.000	427.250	435.000
+15	144	437.500	33100	0.000	432.000	438.500
+15	145	434.500	24300	0.000	434.000	437.750
+15	146	431.500	28500	0.000	430.500	437.500
+15	147	428.000	15500	0.000	427.000	432.750
+15	148	424.250	44900	0.000	424.000	428.750
+15	149	423.000	34000	0.000	420.000	426.500
+15	150	422.875	37800	0.000	420.000	424.000
+15	151	423.500	24000	0.000	423.250	428.000
+15	152	429.500	23200	0.000	420.500	429.500
+15	153	434.250	31800	0.000	429.750	435.000
+15	154	430.250	36500	0.000	426.500	436.250
+16	155	309.750	41400	0.000	307.125	310.750
+16	156	317.000	44500	0.000	309.750	317.000
+16	157	315.000	39900	0.000	313.125	319.750
+16	158	322.000	40600	0.000	318.500	322.500
+16	159	324.250	35300	0.000	321.000	325.250
+16	160	323.000	32300	0.000	322.500	327.000
+16	161	329.250	47700	0.000	323.250	330.000
+16	162	329.500	56700	0.000	328.500	333.500
+16	163	321.000	45400	0.000	320.000	328.500
+17	164	298.000	31900	0.000	290.500	298.000
+17	165	272.000	165000	0.000	270.750	285.125
+17	166	259.500	290400	0.000	252.750	261.000
+17	167	270.750	136000	0.000	266.000	270.750
+17	168	268.750	96600	0.000	266.000	271.750
+17	169	260.000	99100	0.000	258.000	264.500
+17	170	251.500	100900	0.000	250.500	260.750
+17	171	259.500	78100	0.000	253.000	261.000
+17	172	260.500	64600	0.000	260.500	267.500
+17	173	260.000	51400	0.000	259.000	264.500
+17	174	258.000	40800	0.000	257.000	261.000
+17	175	257.750	39400	0.000	255.000	257.750
+17	176	255.500	47500	0.000	253.500	257.500
+17	177	248.500	147400	0.000	247.250	254.000
+17	178	251.500	107100	0.000	245.000	251.500
+17	179	259.500	70100	0.000	252.500	262.000
+17	180	261.750	49800	0.000	255.000	263.500
+17	181	274.000	146700	0.000	270.500	284.500
+17	182	281.000	82800	0.000	277.500	283.750
+17	183	282.000	52000	0.000	281.000	284.500
+17	184	279.250	39600	0.000	278.000	280.250
+18	185	248.000	38000	0.000	247.750	250.000
+18	186	246.500	25100	0.000	244.000	248.250
+18	187	247.000	27100	0.000	245.000	248.750
+18	188	244.375	30500	0.000	244.000	248.000
+18	189	243.250	31600	0.000	240.500	244.250
+18	190	246.250	34900	0.000	245.250	248.500
+18	191	245.250	35300	0.000	245.130	249.750
+18	192	241.250	30700	0.000	240.250	244.500
+19	193	240.500	34800	0.000	237.500	243.250
+19	194	246.500	43600	0.000	236.500	246.500
+19	195	249.500	68300	0.000	246.500	251.500
+19	196	250.000	35100	0.000	249.125	254.000
+19	197	248.750	56400	0.000	247.000	251.500
+19	198	249.250	32100	0.000	246.750	250.500
+19	199	247.500	29300	0.000	247.375	250.750
+19	200	245.750	20000	0.000	245.250	247.500
+19	201	246.250	22500	0.000	245.125	246.750
+19	202	243.000	38800	0.000	242.500	247.750
+20	203	182.250	84300	0.000	176.500	183.500
+20	204	179.000	63300	0.000	179.000	185.500
+20	205	173.000	59200	0.000	171.750	178.500
+20	206	171.500	69600	0.000	170.250	175.250
+20	207	165.125	70500	0.000	165.125	172.250
+20	208	156.500	113200	0.000	155.250	165.000
+20	209	152.000	131000	0.000	150.500	157.250
+20	210	159.625	109100	0.000	150.500	159.750
+20	211	161.880	87800	0.000	161.750	168.500
+20	212	166.500	75800	0.000	158.000	166.500
+20	213	170.500	95900	0.000	169.250	173.750
+20	214	171.500	80600	0.000	166.500	173.500
+20	215	169.250	59500	0.000	168.250	175.000
+20	216	166.750	47000	0.000	164.750	168.000
+20	217	165.000	59700	0.000	165.000	171.000
+20	218	164.000	25800	0.000	160.250	164.000
+20	219	160.875	51900	0.000	160.000	166.750
+20	220	159.000	84100	0.000	153.500	159.500
+20	221	160.250	125800	0.000	154.250	161.750
+20	222	159.500	63900	0.000	158.500	163.500
+20	223	157.750	56100	0.000	155.625	160.500
+20	224	156.250	74200	0.000	154.250	159.500
+20	225	164.125	60900	0.000	159.250	164.750
+20	226	162.000	87000	0.000	160.750	164.500
+21	227	162.250	51800	0.000	161.750	166.000
+21	228	164.250	65300	0.000	160.130	164.250
+21	229	168.000	48900	0.000	166.000	168.875
+21	230	168.875	45100	0.000	168.250	171.375
+21	231	167.500	78000	0.000	166.750	171.250
+21	232	166.625	53000	0.000	165.500	170.250
+21	233	165.750	49000	0.000	163.500	166.500
+21	234	163.250	58700	0.000	163.250	167.125
+21	235	168.500	56700	0.000	162.630	168.500
+22	236	212.000	89200	0.000	212.000	217.750
+22	237	207.500	117700	0.000	206.000	209.750
+22	238	212.000	95100	0.000	207.500	212.500
+22	239	212.250	74000	0.000	211.500	214.000
+22	240	215.500	68400	0.000	211.880	215.750
+22	241	220.000	79200	0.000	216.000	220.000
+23	242	222.500	45700	0.000	222.250	224.630
+23	243	223.250	43000	0.000	221.500	223.500
+23	244	224.250	47700	0.000	223.500	225.250
+23	245	226.500	43800	0.000	223.380	226.500
+23	246	230.000	88800	0.000	226.250	230.000
+23	247	229.380	118000	0.000	229.000	230.880
+23	248	228.500	127100	0.000	228.375	231.000
+23	249	230.380	136400	0.000	228.500	230.750
+24	250	249.880	97500	0.000	249.630	255.000
+24	251	251.000	90400	0.000	248.130	252.250
+24	252	256.500	90400	0.000	251.000	257.000
+24	253	257.750	111000	0.000	257.625	259.750
+24	254	259.250	87200	0.000	256.000	259.250
+24	255	261.000	111100	0.000	258.250	261.250
+24	256	263.750	75900	0.000	261.250	264.500
+24	257	258.000	121100	0.000	257.750	263.250
+24	258	254.000	125500	0.000	254.000	257.250
+24	259	253.500	78400	0.000	252.750	257.750
+24	260	258.000	74700	0.000	253.000	258.000
+25	261	279.750	62700	0.000	277.130	280.000
+25	262	277.625	85500	0.000	276.750	280.375
+25	263	276.630	99800	0.000	276.630	280.380
+25	264	276.250	113600	0.000	276.000	279.500
+25	265	274.250	187100	0.000	272.750	276.380
+25	266	272.000	105300	0.000	272.000	275.500
+25	267	270.500	111000	0.000	268.000	272.750
+25	268	271.000	66000	0.000	270.500	272.500
+25	269	272.500	43000	0.000	270.250	272.500
+25	270	273.000	56200	0.000	272.000	273.880
+25	271	275.500	55200	0.000	273.750	275.500
+25	272	272.250	73400	0.000	272.000	275.500
+26	273	262.250	89000	0.000	262.000	267.750
+26	274	259.500	146100	0.000	257.500	261.750
+26	275	260.250	90300	0.000	257.500	260.625
+26	276	261.000	74100	0.000	258.880	261.880
+26	277	264.500	44400	0.000	260.250	264.880
+26	278	262.000	78900	0.000	261.250	264.500
+27	279	272.500	132000	0.000	270.130	272.500
+27	280	272.000	79800	0.000	271.375	272.625
+27	281	271.130	75600	0.000	270.750	272.880
+27	282	270.000	113800	0.000	269.880	271.500
+27	283	264.000	179500	0.000	263.500	270.500
+27	284	258.000	215300	0.000	257.500	263.250
+27	285	258.250	154200	0.000	256.500	259.625
+27	286	259.500	95600	0.000	257.500	260.500
+27	287	261.250	96500	0.000	259.250	262.000
+27	288	259.750	119000	0.000	259.250	261.625
+27	289	258.750	81500	0.000	257.130	259.750
+28	290	257.380	53700	0.000	257.250	259.250
+28	291	256.000	52200	0.000	255.375	257.500
+28	292	257.750	76200	0.000	255.630	258.630
+28	293	258.750	93400	0.000	257.380	259.500
+28	294	258.750	99000	0.000	258.500	259.630
+28	295	257.250	123100	0.000	255.750	257.500
+28	296	260.500	188300	0.000	257.000	261.000
+28	297	260.250	103300	0.000	259.000	261.250
+29	298	264.250	110100	0.000	261.630	264.380
+29	299	266.000	98100	0.000	264.250	266.750
+29	300	267.500	146100	0.000	266.625	268.000
+29	301	270.250	199400	0.000	267.500	270.250
+29	302	270.250	89800	0.000	269.000	270.625
+29	303	271.000	78600	0.000	269.250	271.250
+29	304	272.750	107600	0.000	269.880	273.500
+29	305	273.500	119200	0.000	272.750	275.250
+29	306	273.500	0	0.000	0.000	0.000
+30	307	236.250	146100	0.000	236.250	242.000
+30	308	239.500	358100	0.000	235.500	239.500
+30	309	243.500	241500	0.000	240.500	244.000
+30	310	251.125	304800	0.000	248.375	253.625
+30	311	251.250	231000	0.000	248.875	253.000
+30	312	253.000	152800	0.000	249.625	254.000
+30	313	253.500	175100	0.000	252.500	257.880
+30	314	254.000	141800	0.000	252.125	255.500
+30	315	258.000	110000	0.000	253.000	258.250
+30	316	261.750	374200	0.000	261.250	264.000
+30	317	262.380	275100	0.000	261.380	265.000
+30	318	261.500	169700	0.000	259.875	261.750
+31	319	262.500	87100	0.000	262.250	266.500
+31	320	258.250	91100	0.000	257.750	262.500
+31	321	260.880	105600	0.000	258.250	262.000
+31	322	261.750	85000	0.000	258.500	261.880
+31	323	259.630	68400	0.000	259.000	262.250
+31	324	257.250	45500	0.000	257.250	260.250
+31	325	256.750	39900	0.000	255.000	257.500
+31	326	258.000	102400	0.000	255.250	259.500
+32	327	268.125	180700	0.000	267.750	272.000
+32	328	270.250	80900	0.000	267.000	270.250
+32	329	271.000	107200	0.000	267.750	271.000
+32	330	273.000	149400	0.000	269.250	273.000
+32	331	273.500	262900	0.000	273.000	276.750
+32	332	276.250	150300	0.000	273.000	276.250
+32	333	278.630	172800	0.000	275.500	279.500
+32	334	281.000	132700	0.000	278.750	281.500
+32	335	279.125	178400	0.000	279.000	281.750
+33	336	311.000	96900	0.000	308.130	311.000
+33	337	308.250	85000	0.000	307.625	311.000
+33	338	305.750	119200	0.000	304.500	306.625
+33	339	303.500	133200	0.000	303.000	306.500
+33	340	298.500	168400	0.000	297.130	299.750
+33	341	299.630	77900	0.000	299.130	300.630
+33	342	301.750	91100	0.000	300.130	301.880
+33	343	306.630	78700	0.000	299.750	307.000
+33	344	308.125	132600	0.000	306.750	310.000
+33	345	305.500	82400	0.000	304.000	308.250
+34	346	308.875	80600	0.000	308.250	312.875
+34	347	306.750	124000	0.000	306.250	309.250
+34	348	310.000	123300	0.000	307.000	310.250
+34	349	310.250	102600	0.000	309.750	312.875
+34	350	312.000	80400	0.000	309.880	312.250
+34	351	308.880	115900	0.000	308.500	312.000
+34	352	310.500	98300	0.000	307.500	310.500
+34	353	314.250	116900	0.000	309.750	314.250
+34	354	315.625	118600	0.000	314.250	317.500
+34	355	318.000	98700	0.000	315.000	318.500
+34	356	319.750	149000	0.000	318.000	321.500
+34	357	311.250	121900	0.000	311.000	317.500
+34	358	309.000	84400	0.000	308.130	309.880
+34	359	312.000	126500	0.000	308.625	312.250
+35	360	78.000	512100	0.000	77.500	78.500
+35	361	75.375	607700	0.000	75.250	78.000
+35	362	74.625	628300	0.000	73.750	75.250
+35	363	74.250	517300	0.000	73.875	75.000
+35	364	72.750	358300	0.000	72.625	74.000
+35	365	72.630	394600	0.000	72.250	73.500
+35	366	73.000	599200	0.000	72.130	73.750
+35	367	73.500	450200	0.000	72.875	74.500
+35	368	74.130	304400	0.000	73.880	74.250
+35	369	73.875	289200	0.000	73.375	74.500
+36	370	72.250	498300	0.000	69.875	72.375
+36	371	71.630	476800	0.000	71.000	72.500
+36	372	71.250	282600	0.000	71.000	71.880
+36	373	71.630	350300	0.000	70.880	71.880
+36	374	70.880	261500	0.000	70.500	71.630
+36	375	70.500	531700	0.000	70.380	71.130
+36	376	69.625	391900	0.000	69.500	70.750
+36	377	69.625	337000	0.000	69.125	70.500
+36	378	70.000	338900	0.000	69.500	70.625
+36	379	70.000	393400	0.000	69.625	70.125
+36	380	69.630	293200	0.000	69.630	70.250
+36	381	69.500	205300	0.000	69.500	70.000
+36	382	70.000	268300	0.000	69.750	70.000
+36	383	68.630	381500	0.000	68.500	69.880
+36	384	68.250	693200	0.000	67.250	68.500
+36	385	67.630	316100	0.000	67.500	68.250
+36	386	67.750	406600	0.000	67.380	68.000
+36	387	67.500	305800	0.000	67.375	67.875
+36	388	66.500	466500	0.000	66.125	67.750
+36	389	66.250	652800	0.000	65.750	66.625
+36	390	66.750	399300	0.000	66.125	66.875
+36	391	67.880	502100	0.000	66.500	68.380
+36	392	68.500	538900	0.000	68.380	69.250
+36	393	67.880	407700	0.000	67.750	68.500
+36	394	67.750	396500	0.000	67.750	68.630
+36	395	71.250	439900	0.000	67.625	71.375
+36	396	70.000	626100	0.000	69.380	71.000
+37	397	65.000	208900	0.000	64.500	65.125
+37	398	65.380	291800	0.000	64.880	65.380
+37	399	65.750	377600	0.000	65.500	66.380
+37	400	67.250	283500	0.000	65.380	67.250
+37	401	66.630	582400	0.000	66.380	67.630
+37	402	66.375	257200	0.000	66.125	66.625
+37	403	65.625	393900	0.000	65.625	66.750
+37	404	65.250	295500	0.000	65.130	65.750
+37	405	65.000	329700	0.000	64.630	65.250
+37	406	66.000	412400	0.000	64.880	66.130
+38	407	64.875	176300	0.000	64.000	64.875
+38	408	64.875	163300	0.000	64.375	65.125
+38	409	64.750	253800	0.000	64.250	65.000
+38	410	64.250	279200	0.000	64.000	64.630
+38	411	64.380	308600	0.000	64.000	64.630
+38	412	62.500	412000	0.000	62.500	64.500
+38	413	63.500	610300	0.000	61.250	63.500
+38	414	63.380	472800	0.000	63.000	63.750
+39	415	54.625	512900	0.000	53.375	54.750
+39	416	54.250	504900	0.000	54.000	55.250
+39	417	53.500	651700	0.000	53.500	55.000
+39	418	53.000	334100	0.000	52.750	53.750
+39	419	52.000	579900	0.000	51.875	53.500
+39	420	51.125	672300	0.000	51.000	52.875
+39	421	51.250	451000	0.000	50.380	51.630
+39	422	51.250	411900	0.000	50.875	51.750
+39	423	51.250	663800	0.000	50.500	51.625
+39	424	53.875	821800	0.000	52.000	54.500
+39	425	53.500	761400	0.000	53.250	54.375
+40	426	66.000	437200	0.000	64.250	66.000
+40	427	65.130	553000	0.000	64.750	66.130
+40	428	64.880	449000	0.000	64.380	65.380
+40	429	64.750	397400	0.000	64.250	65.000
+40	430	64.380	375800	0.000	63.880	64.630
+40	431	65.000	270800	0.000	64.000	65.250
+40	432	65.125	410600	0.000	64.375	65.500
+40	433	65.500	555700	0.000	65.130	66.000
+40	434	65.250	644100	0.000	64.380	65.380
+41	435	56.750	492500	0.000	56.250	56.880
+41	436	56.630	616200	0.000	55.750	56.630
+41	437	56.380	348000	0.000	56.130	56.630
+41	438	55.625	461800	0.000	55.500	56.375
+41	439	55.500	438400	0.000	55.250	55.875
+41	440	56.130	514900	0.000	55.130	56.380
+41	441	57.880	673900	0.000	55.880	58.130
+41	442	58.000	700200	0.000	57.250	58.500
+41	443	58.750	796900	0.000	57.880	59.130
+41	444	60.250	1089200	0.000	59.750	60.750
+41	445	59.880	747600	0.000	59.880	60.500
+42	446	55.875	387300	0.000	55.000	56.750
+42	447	55.500	396600	0.000	55.500	56.500
+42	448	54.880	372500	0.000	54.500	55.380
+42	449	54.130	551600	0.000	53.500	55.130
+42	450	54.000	346100	0.000	53.875	54.500
+42	451	54.375	427300	0.000	54.125	55.000
+42	452	54.500	389800	0.000	53.380	54.630
+42	453	54.625	497600	0.000	54.250	55.250
+42	454	53.625	524200	0.000	53.250	54.250
+43	455	64.625	1058800	0.000	63.375	64.625
+43	456	64.500	689400	0.000	64.250	64.880
+43	457	63.750	1140600	0.000	63.625	65.125
+43	458	63.250	892800	0.000	63.000	63.880
+43	459	63.875	512200	0.000	63.250	63.875
+43	460	64.375	645800	0.000	64.000	64.750
+43	461	65.750	1214100	0.000	64.500	65.750
+43	462	66.000	911700	0.000	65.000	66.130
+43	463	65.125	685600	0.000	64.625	65.750
+43	464	64.880	717200	0.000	64.500	65.250
+43	465	64.125	491600	0.000	64.125	64.625
+43	466	64.250	759200	0.000	64.000	64.750
+43	467	64.500	384600	0.000	64.000	64.750
+43	468	64.750	497500	0.000	64.500	65.250
+43	469	65.125	855600	0.000	64.750	65.250
+43	470	64.875	777200	0.000	64.750	65.125
+43	471	64.750	487900	0.000	64.750	65.130
+43	472	63.875	569800	0.000	63.875	64.875
+43	473	64.380	372400	0.000	64.000	64.750
+44	474	60.000	290100	0.000	60.000	60.630
+44	475	60.380	358800	0.000	59.380	60.750
+44	476	61.000	638300	0.000	60.380	61.250
+44	477	61.500	963900	0.000	59.750	61.630
+44	478	62.500	1247100	0.000	61.380	62.500
+44	479	63.500	985100	0.000	62.880	63.630
+44	480	64.500	1556300	0.000	63.125	65.000
+44	481	66.875	1688100	0.000	64.750	66.875
+44	482	66.750	1578700	0.000	66.375	67.500
+45	483	76.750	1321700	0.000	74.750	76.750
+45	484	75.500	1604100	0.000	75.500	77.630
+45	485	76.380	1254000	0.000	75.250	76.630
+45	486	76.000	887100	0.000	75.630	76.500
+45	487	76.630	490500	0.000	75.750	77.000
+45	488	76.250	868300	0.000	76.250	77.250
+45	489	74.750	831000	0.000	74.500	76.375
+45	490	73.375	997900	0.000	73.250	74.875
+45	491	74.130	747500	0.000	73.380	74.630
+45	492	74.500	614900	0.000	73.250	74.630
+45	493	75.130	752300	0.000	74.630	76.130
+45	494	79.000	1346100	0.000	75.375	79.000
+45	495	80.380	2207600	0.000	78.380	80.380
+45	496	81.130	1342500	0.000	79.250	81.130
+45	497	83.500	1439300	0.000	81.380	84.500
+45	498	82.750	1500400	0.000	82.375	84.375
+46	499	96.130	725400	0.000	95.750	98.000
+46	500	96.250	373700	0.000	96.125	97.125
+46	501	93.000	760600	0.000	92.500	97.000
+46	502	95.625	915100	0.000	92.375	95.625
+46	503	96.000	950900	0.000	95.000	96.750
+46	504	97.625	1024200	0.000	95.500	98.500
+46	505	96.630	1060600	0.000	96.500	98.380
+46	506	98.000	1114200	0.000	96.000	99.250
+46	507	98.000	850900	0.000	97.000	98.380
+47	508	102.000	711800	0.000	101.880	103.250
+47	509	102.625	858000	0.000	101.250	102.875
+47	510	103.250	607100	0.000	102.000	103.375
+47	511	103.875	894200	0.000	102.625	104.000
+47	512	106.625	1651200	0.000	104.625	106.750
+47	513	107.000	1049000	0.000	106.000	107.380
+47	514	108.750	978800	0.000	107.375	109.000
+47	515	109.500	1007500	0.000	107.250	110.000
+47	516	110.125	794100	0.000	109.625	110.500
+47	517	112.630	1020400	0.000	110.000	112.630
+47	518	111.500	929400	0.000	111.000	112.130
+48	519	116.500	424700	0.000	115.625	116.625
+48	520	115.250	668900	0.000	114.000	115.250
+48	521	114.000	722000	0.000	113.125	115.000
+48	522	111.750	802300	0.000	110.750	114.750
+48	523	111.250	673500	0.000	110.500	112.500
+48	524	110.750	538900	0.000	110.250	111.750
+48	525	112.500	813000	0.000	109.750	112.875
+48	526	114.630	665400	0.000	113.000	115.000
+48	527	115.630	843600	0.000	113.380	115.630
+48	528	114.380	647800	0.000	114.250	115.880
+48	529	113.000	404400	0.000	112.875	115.000
+48	530	111.250	734800	0.000	110.250	112.880
+48	531	111.875	890300	0.000	110.250	111.875
+48	532	113.500	803000	0.000	111.250	113.875
+48	533	114.000	662100	0.000	113.000	114.880
+48	534	115.750	573900	0.000	113.630	116.130
+48	535	113.130	791400	0.000	113.000	116.250
+48	536	113.375	831700	0.000	112.625	114.750
+48	537	113.750	672300	0.000	113.130	114.500
+48	538	114.250	419600	0.000	113.630	114.630
+48	539	117.250	737800	0.000	114.125	117.250
+48	540	118.000	971500	0.000	117.000	119.750
+48	541	121.000	1170300	0.000	118.130	121.000
+48	542	121.750	1233400	0.000	121.500	122.875
+48	543	121.130	625500	0.000	120.250	122.250
+49	544	120.875	542900	0.000	120.250	122.750
+49	545	118.000	865300	0.000	117.750	121.380
+49	546	120.000	669800	0.000	117.880	120.880
+49	547	120.250	556200	0.000	119.750	121.000
+49	548	121.000	373100	0.000	120.375	121.250
+49	549	119.250	943300	0.000	118.625	119.625
+50	550	122.880	814300	0.000	121.630	123.500
+50	551	121.750	691100	0.000	121.500	123.250
+50	552	123.125	819700	0.000	121.500	124.250
+50	553	123.750	911500	0.000	122.625	124.875
+50	554	123.875	942300	0.000	123.125	124.500
+50	555	123.000	1070900	0.000	122.875	124.125
+50	556	124.625	1099300	0.000	122.750	124.750
+50	557	126.625	1111900	0.000	124.250	126.625
+50	558	128.625	2097500	0.000	126.000	129.500
+50	559	127.875	1271500	0.000	126.500	128.375
+51	560	129.875	1190200	0.000	128.875	130.125
+51	561	131.130	1391200	0.000	129.130	131.250
+51	562	132.130	1104600	0.000	131.000	132.250
+51	563	132.250	803300	0.000	131.750	132.500
+51	564	134.250	658400	0.000	131.500	134.250
+51	565	133.000	687100	0.000	132.750	134.000
+52	566	126.750	1413200	0.000	125.000	127.250
+52	567	126.500	946400	0.000	125.500	127.000
+52	568	123.880	922600	0.000	123.500	126.500
+52	569	122.250	1474500	0.000	121.750	123.250
+52	570	122.125	803600	0.000	122.000	123.375
+52	571	122.250	650900	0.000	121.880	122.880
+52	572	123.625	858900	0.000	122.375	123.625
+52	573	125.250	1165800	0.000	124.000	125.750
+52	574	126.875	784800	0.000	125.125	127.250
+52	575	127.500	1030600	0.000	127.000	128.250
+52	576	124.500	1354300	0.000	124.000	127.630
+53	577	111.250	1043200	0.000	109.250	111.500
+53	578	112.880	1458900	0.000	110.750	113.500
+53	579	110.000	1241300	0.000	109.875	112.500
+53	580	110.250	1117000	0.000	109.375	111.750
+53	581	111.500	812900	0.000	109.750	111.625
+53	582	112.000	1005000	0.000	111.625	112.875
+53	583	111.000	779400	0.000	110.375	111.500
+53	584	109.250	868500	0.000	109.125	111.625
+54	585	108.000	729600	0.000	107.375	108.625
+54	586	110.250	1455900	0.000	108.500	110.500
+54	587	110.625	1432300	0.000	110.375	112.000
+54	588	111.000	990700	0.000	110.375	111.500
+54	589	112.000	954700	0.000	111.125	112.500
+54	590	113.750	2110500	0.000	112.750	114.250
+54	591	114.000	817600	0.000	112.500	114.000
+54	592	114.750	1241000	0.000	113.500	115.380
+54	593	114.000	914900	0.000	114.000	115.500
+54	594	112.750	881400	0.000	112.375	114.000
+54	595	112.250	836700	0.000	111.500	113.000
+54	596	112.250	694200	0.000	111.750	113.250
+55	597	112.750	770500	0.000	110.630	112.880
+55	598	112.250	931500	0.000	112.000	113.500
+55	599	111.125	720700	0.000	110.500	112.250
+55	600	110.750	1028700	0.000	110.125	111.125
+55	601	109.880	836400	0.000	109.630	111.380
+55	602	110.750	1104400	0.000	109.500	111.625
+55	603	111.875	899400	0.000	110.625	112.250
+55	604	113.250	1090000	0.000	111.875	113.500
+55	605	113.380	1022900	0.000	112.630	114.250
+55	606	113.750	1122500	0.000	112.750	114.630
+55	607	116.500	1463500	0.000	114.000	116.500
+55	608	116.250	1337900	0.000	116.000	117.250
+56	609	107.250	704700	0.000	105.375	107.500
+56	610	107.125	802400	0.000	106.250	107.375
+56	611	105.250	897500	0.000	105.250	107.130
+56	612	104.375	1076900	0.000	104.125	105.625
+56	613	105.250	856100	0.000	104.000	105.750
+56	614	107.250	925700	0.000	104.500	107.250
+56	615	108.130	1047100	0.000	106.750	108.380
+56	616	107.500	625100	0.000	107.125	108.125
+56	617	107.000	895900	0.000	106.250	107.375
+56	618	106.500	810000	0.000	105.500	107.250
+56	619	106.125	931100	0.000	105.125	106.375
+56	620	105.380	710200	0.000	105.130	107.000
+56	621	107.125	1591800	0.000	104.500	107.750
+56	622	108.250	1002700	0.000	106.750	108.750
+56	623	109.000	1059300	0.000	107.875	109.625
+56	624	108.380	610400	0.000	108.250	109.380
+57	625	124.625	929500	0.000	124.625	126.125
+57	626	125.750	1200100	0.000	125.250	126.875
+57	627	125.880	763200	0.000	125.750	126.500
+57	628	126.750	589700	0.000	123.750	126.875
+57	629	127.500	1270600	0.000	126.500	127.750
+57	630	125.750	1668400	0.000	125.375	127.125
+57	631	124.880	1056700	0.000	124.250	125.750
+57	632	123.250	944300	0.000	123.250	125.500
+57	633	124.000	782700	0.000	122.750	124.000
+58	634	124.250	754200	0.000	123.250	124.500
+58	635	123.750	418300	0.000	123.625	124.250
+58	636	123.125	505200	0.000	122.750	124.250
+58	637	123.500	573600	0.000	122.750	124.000
+58	638	123.125	685000	0.000	122.875	123.875
+58	639	121.000	872500	0.000	120.750	122.630
+58	640	120.000	1388100	0.000	119.875	122.250
+58	641	119.750	1007000	0.000	119.000	120.375
+59	642	123.625	1069600	0.000	123.250	124.625
+59	643	124.130	1163300	0.000	123.630	125.000
+59	644	128.250	2138800	0.000	124.630	128.500
+59	645	129.630	2539000	0.000	129.130	130.630
+59	646	132.500	2270200	0.000	129.375	132.625
+59	647	133.130	1825200	0.000	132.380	134.500
+59	648	133.250	1212300	0.000	132.625	134.125
+59	649	134.625	1936500	0.000	133.250	135.250
+59	650	137.000	1128700	0.000	133.750	137.000
+59	651	136.630	1563600	0.000	136.130	137.630
+60	652	130.500	1037200	0.000	128.250	131.125
+60	653	129.375	1118800	0.000	129.000	130.625
+60	654	128.750	1552200	0.000	128.125	130.000
+60	655	127.750	1197800	0.000	127.750	128.750
+60	656	124.500	2057900	0.000	124.500	128.000
+60	657	125.375	1404400	0.000	124.125	125.625
+60	658	126.380	1098400	0.000	125.500	126.630
+60	659	126.625	993400	0.000	126.375	127.625
+60	660	128.125	840800	0.000	126.875	128.250
+60	661	127.750	949000	0.000	127.500	129.500
+61	662	126.500	1280300	0.000	125.500	126.750
+61	663	125.630	1135000	0.000	125.130	126.880
+61	664	125.375	1258800	0.000	124.750	126.375
+61	665	125.000	770500	0.000	124.630	125.630
+61	666	124.750	579400	0.000	124.750	125.625
+61	667	125.750	772900	0.000	125.000	126.250
+61	668	127.125	925700	0.000	125.375	127.250
+61	669	127.880	978300	0.000	127.000	128.250
+61	670	130.130	2239000	0.000	128.500	130.880
+61	671	130.250	1157600	0.000	129.750	130.625
+61	672	128.000	1551200	0.000	127.750	130.875
+62	673	133.000	1215000	0.000	132.380	133.250
+62	674	132.375	857100	0.000	131.875	132.750
+62	675	131.375	829300	0.000	131.250	132.375
+62	676	130.880	675200	0.000	130.750	131.630
+62	677	130.375	921900	0.000	129.750	131.750
+62	678	129.750	939700	0.000	129.250	130.250
+62	679	128.000	1666600	0.000	127.250	129.500
+62	680	128.625	1371900	0.000	127.250	130.000
+62	681	129.125	1265600	0.000	128.250	129.500
+62	682	129.750	1176100	0.000	128.750	130.130
+62	683	128.500	1644500	0.000	128.500	130.630
+63	684	121.130	1753000	0.000	119.380	121.130
+63	685	120.130	1316600	0.000	119.250	120.500
+63	686	119.875	1819000	0.000	119.125	121.625
+63	687	119.375	1268900	0.000	119.250	120.875
+63	688	118.750	1400300	0.000	117.375	119.750
+63	689	119.380	1365100	0.000	118.380	119.750
+63	690	120.875	1125300	0.000	118.750	121.125
+63	691	122.000	1753000	0.000	121.380	123.380
+63	692	123.130	914300	0.000	122.000	123.250
+63	693	124.000	1151500	0.000	123.125	124.250
+63	694	123.750	820500	0.000	123.500	124.500
+64	695	141.750	1638400	0.000	140.250	141.750
+64	696	144.630	1662500	0.000	142.130	144.880
+64	697	146.625	1940200	0.000	144.250	148.000
+64	698	149.000	1985600	0.000	146.625	149.375
+64	699	148.630	1407600	0.000	147.000	149.380
+64	700	150.250	1890100	0.000	149.130	151.500
+64	701	152.250	1586100	0.000	150.000	153.500
+64	702	152.500	1786100	0.000	151.500	154.500
+64	703	152.875	1524100	0.000	151.625	153.875
+64	704	153.750	1105900	0.000	152.375	154.000
+64	705	154.250	2127800	0.000	153.750	156.625
+64	706	154.000	1220900	0.000	153.380	155.130
+64	707	152.750	1011600	0.000	151.250	154.630
+64	708	153.250	562700	0.000	152.125	153.750
+65	709	141.130	1858600	0.000	138.880	141.250
+65	710	141.125	1561600	0.000	140.500	142.000
+65	711	140.000	1229000	0.000	139.500	140.380
+65	712	138.750	887600	0.000	138.000	140.000
+65	713	136.000	1938300	0.000	135.500	138.875
+65	714	136.125	1628600	0.000	135.125	136.375
+65	715	139.380	1630000	0.000	136.000	139.500
+65	716	140.375	2618100	0.000	138.500	141.000
+65	717	143.750	2629100	0.000	140.875	143.875
+65	718	143.880	2251100	0.000	143.500	145.500
+65	719	144.130	1766900	0.000	142.880	144.500
+65	720	139.250	2866100	0.000	138.750	142.880
+65	721	137.380	4379500	0.000	136.250	139.880
+65	722	137.000	2194500	0.000	136.000	138.500
+65	723	138.250	1413100	0.000	135.625	138.500
+66	724	123.625	2431100	0.000	122.375	124.500
+66	725	122.000	1609300	0.000	121.880	125.250
+66	726	120.880	3142200	0.000	119.750	123.000
+66	727	123.000	2239500	0.000	120.375	123.000
+66	728	121.130	2220400	0.000	120.880	123.380
+66	729	121.875	1947600	0.000	120.500	122.375
+66	730	120.250	1484700	0.000	119.875	121.250
+66	731	121.375	1697000	0.000	120.250	121.375
+66	732	120.375	1283900	0.000	120.125	121.500
+67	733	123.130	2107700	0.000	122.500	124.750
+67	734	121.380	2581200	0.000	119.250	122.000
+67	735	121.500	1103400	0.000	120.000	121.880
+67	736	123.125	998200	0.000	121.250	123.250
+67	737	123.750	751400	0.000	122.875	124.250
+67	738	122.750	1130400	0.000	121.875	124.375
+68	739	151.750	1804700	0.000	151.500	154.130
+68	740	154.250	3308900	0.000	149.500	156.000
+68	741	155.250	2945900	0.000	154.625	156.625
+68	742	158.630	3248800	0.000	155.880	159.630
+68	743	160.125	3272200	0.000	157.750	161.500
+68	744	160.630	2385000	0.000	159.130	162.380
+68	745	163.000	1561700	0.000	159.750	163.000
+68	746	166.875	2792500	0.000	164.250	167.625
+68	747	166.750	2187900	0.000	166.000	167.630
+68	748	164.880	1652200	0.000	163.880	166.750
+68	749	163.750	1430500	0.000	163.250	165.875
+68	750	163.000	2269600	0.000	163.000	167.380
+68	751	165.380	1998200	0.000	162.250	166.130
+69	752	159.250	2057900	0.000	158.750	160.880
+69	753	159.380	1668400	0.000	158.500	160.000
+69	754	160.250	2435800	0.000	159.750	161.000
+69	755	163.000	2284000	0.000	159.125	163.250
+69	756	164.375	2386900	0.000	163.000	165.625
+69	757	166.750	1632200	0.000	164.500	166.750
+69	758	169.000	3203300	0.000	167.130	169.630
+69	759	170.380	3299900	0.000	168.000	172.130
+69	760	173.380	2586600	0.000	170.380	174.000
+69	761	173.250	2109200	0.000	172.000	175.000
+70	762	150.500	2630500	0.000	149.500	153.750
+70	763	150.750	3073300	0.000	148.500	151.880
+70	764	154.630	1908400	0.000	151.000	154.750
+70	765	155.250	1868500	0.000	154.750	156.880
+70	766	156.500	1544300	0.000	154.500	157.250
+70	767	151.000	2044200	0.000	151.000	156.375
+71	768	145.250	2922100	0.000	144.130	147.380
+71	769	140.125	3923900	0.000	140.000	145.750
+71	770	135.000	4665000	0.000	132.000	141.250
+71	771	104.000	6384400	0.000	102.000	130.750
+71	772	115.000	0	0.000	110.130	122.000
+71	773	122.750	5386600	0.000	120.000	125.500
+72	774	118.880	2233600	0.000	115.000	119.250
+72	775	115.500	1824700	0.000	115.000	119.500
+72	776	117.250	2798900	0.000	115.250	118.250
+72	777	118.250	1550200	0.000	116.880	119.500
+72	778	118.250	1217000	0.000	116.500	118.375
+72	779	119.500	1646400	0.000	118.750	120.380
+72	780	119.875	630300	0.000	119.000	119.875
+72	781	115.750	1086800	0.000	115.130	117.630
+73	782	111.630	1230300	0.000	110.750	112.130
+73	783	112.250	769000	0.000	111.500	112.750
+73	784	113.380	867400	0.000	112.750	114.000
+73	785	112.875	896800	0.000	112.625	113.750
+73	786	111.500	932200	0.000	110.750	113.500
+73	787	110.250	1489000	0.000	109.500	111.130
+73	788	114.000	1564900	0.000	111.630	114.750
+73	789	112.875	958200	0.000	112.375	113.875
+74	790	114.125	1652700	0.000	112.500	114.375
+74	791	113.500	947400	0.000	113.375	114.125
+74	792	112.750	1100200	0.000	112.500	114.380
+74	793	112.500	2195900	0.000	110.630	112.630
+74	794	113.000	1558100	0.000	111.880	113.000
+74	795	113.380	1187700	0.000	113.130	114.250
+74	796	115.000	1365300	0.000	113.630	115.250
+74	797	114.625	1992000	0.000	114.500	116.500
+74	798	114.000	1582100	0.000	113.625	115.000
+74	799	113.750	2225000	0.000	112.625	114.000
+74	800	115.500	2406400	0.000	113.250	115.750
+74	801	117.375	2209500	0.000	115.750	117.625
+74	802	118.250	2648200	0.000	117.750	119.375
+74	803	117.125	1619800	0.000	117.125	119.125
+75	804	116.380	1333800	0.000	116.000	118.380
+75	805	116.875	1509800	0.000	116.125	117.500
+75	806	117.130	939000	0.000	116.880	117.630
+75	807	117.380	1049800	0.000	116.880	118.250
+75	808	117.750	1708300	0.000	117.375	118.750
+75	809	116.750	1105600	0.000	116.000	117.630
+75	810	116.250	1141200	0.000	115.875	117.000
+75	811	116.000	1173400	0.000	115.000	116.380
+75	812	116.250	946600	0.000	115.375	116.375
+76	813	98.750	1249700	0.000	98.000	99.380
+76	814	99.500	1127900	0.000	98.500	99.750
+76	815	100.125	1129400	0.000	99.375	100.500
+76	816	100.375	638800	0.000	100.250	100.750
+76	817	99.380	1245400	0.000	98.250	100.750
+76	818	98.880	1157700	0.000	98.500	99.380
+76	819	97.750	956500	0.000	97.500	98.625
+76	820	97.630	1001300	0.000	96.880	98.130
+76	821	97.130	1434200	0.000	96.880	98.130
+76	822	99.250	2026900	0.000	97.380	99.500
+77	823	95.375	1005100	0.000	95.125	95.750
+77	824	94.500	682200	0.000	94.250	95.500
+77	825	94.750	1165700	0.000	94.500	95.375
+77	826	94.130	1192300	0.000	94.000	95.130
+77	827	94.130	1637100	0.000	94.000	94.630
+77	828	98.000	1760400	0.000	94.500	98.130
+77	829	98.875	2366000	0.000	98.375	99.500
+77	830	100.000	2418700	0.000	98.880	100.380
+77	831	99.750	1890500	0.000	99.500	101.630
+78	832	106.625	1050400	0.000	106.000	106.750
+78	833	106.500	1716200	0.000	105.880	107.750
+78	834	106.130	868400	0.000	105.880	106.880
+78	835	105.875	752300	0.000	105.250	106.375
+78	836	105.500	782800	0.000	105.250	105.880
+78	837	105.875	934800	0.000	105.125	106.375
+78	838	106.375	645200	0.000	105.875	106.500
+78	839	107.125	921600	0.000	107.000	108.000
+78	840	110.880	4120700	0.000	110.130	111.250
+78	841	111.125	1916900	0.000	109.875	111.250
+78	842	109.875	1531000	0.000	109.500	110.875
+79	843	102.380	1347000	0.000	101.500	103.500
+79	844	105.500	1634700	0.000	101.880	105.500
+79	845	105.880	1609400	0.000	105.500	107.630
+79	846	107.500	1438200	0.000	105.880	107.750
+79	847	107.880	1192900	0.000	107.130	108.380
+79	848	105.000	1077000	0.000	104.750	107.750
+80	849	107.375	1101700	0.000	107.125	109.000
+80	850	107.880	1162000	0.000	106.380	108.250
+80	851	108.250	1570000	0.000	106.000	109.000
+80	852	109.500	901300	0.000	108.625	109.500
+80	853	105.375	1418600	0.000	105.375	108.625
+80	854	103.500	2258600	0.000	102.875	106.625
+80	855	101.000	3113800	0.000	99.750	103.880
+80	856	100.250	2308100	0.000	99.500	102.000
+80	857	99.250	3121400	0.000	96.250	100.875
+80	858	99.380	1690200	0.000	98.250	100.380
+80	859	100.750	1330300	0.000	99.380	101.630
+80	860	105.375	1866500	0.000	101.750	105.375
+80	861	107.625	2658600	0.000	104.625	109.000
+80	862	107.750	1918300	0.000	106.500	108.750
+80	863	106.750	1270700	0.000	106.625	108.250
+81	864	111.500	3389800	0.000	111.250	116.000
+81	865	112.500	1909400	0.000	110.750	112.875
+81	866	113.380	1173800	0.000	112.130	113.750
+81	867	112.875	997900	0.000	112.375	113.125
+81	868	114.375	1471900	0.000	112.625	114.500
+81	869	112.880	1093100	0.000	112.880	114.630
+81	870	111.250	1435200	0.000	111.000	112.500
+81	871	111.500	954900	0.000	110.625	111.750
+81	872	113.500	1617100	0.000	111.500	113.500
+81	873	112.750	2463900	0.000	112.130	114.000
+81	874	113.750	1896000	0.000	112.250	114.630
+81	875	113.875	2084600	0.000	113.375	114.375
+81	876	113.880	341300	0.000	113.750	114.250
+81	877	113.500	396800	0.000	113.500	114.250
+81	878	113.630	714100	0.000	113.000	113.880
+81	879	113.375	728200	0.000	113.250	113.875
+81	880	113.000	461300	0.000	112.880	113.500
+81	881	112.125	1037700	0.000	112.125	113.750
+81	882	112.500	1307400	0.000	112.250	113.880
+81	883	112.130	1085100	0.000	111.880	113.000
+81	884	110.250	1189400	0.000	110.000	111.875
+81	885	109.000	1715500	0.000	108.750	110.375
+81	886	106.880	2078000	0.000	106.750	110.750
+81	887	108.380	962000	0.000	107.250	108.380
+81	888	108.130	608700	0.000	107.750	108.250
+81	889	106.750	1746500	0.000	105.500	107.130
+81	890	107.500	1168100	0.000	106.750	107.750
+81	891	109.130	1582000	0.000	106.630	109.250
+81	892	115.750	4330400	0.000	114.250	116.130
+81	893	117.625	4592500	0.000	114.875	118.250
+82	894	118.000	1963700	0.000	117.750	119.625
+82	895	119.250	2211000	0.000	117.875	120.125
+82	896	121.000	2841200	0.000	119.880	121.630
+82	897	122.625	2441100	0.000	120.500	122.625
+82	898	124.250	2478400	0.000	122.380	125.000
+82	899	124.500	1949200	0.000	123.000	125.380
+82	900	127.000	2524300	0.000	124.630	127.000
+82	901	126.750	1718300	0.000	126.250	127.130
+82	902	126.880	1919100	0.000	125.630	127.630
+83	903	128.500	2532000	0.000	128.380	131.000
+83	904	129.500	1751700	0.000	128.250	129.875
+83	905	132.375	1865300	0.000	129.625	132.500
+83	906	132.750	2372400	0.000	131.500	133.875
+83	907	134.630	1923800	0.000	132.250	135.000
+83	908	135.000	2485400	0.000	133.880	136.250
+83	909	137.375	2738900	0.000	135.375	137.750
+83	910	139.500	2278100	0.000	137.000	139.750
+83	911	137.880	1854200	0.000	137.880	138.750
+84	912	110.630	2214700	0.000	110.130	111.880
+84	913	108.500	3555500	0.000	108.130	111.000
+84	914	106.875	3026800	0.000	105.625	108.375
+84	915	109.250	2086700	0.000	106.750	109.250
+84	916	109.880	2089000	0.000	109.250	110.750
+84	917	109.750	1463800	0.000	109.375	110.750
+84	918	109.500	2109300	0.000	109.000	110.250
+84	919	108.500	1570400	0.000	108.125	109.375
+85	920	98.000	1438800	0.000	97.630	99.380
+85	921	97.250	1909500	0.000	96.750	98.250
+85	922	99.750	1417200	0.000	97.000	99.875
+85	923	99.880	893600	0.000	99.500	100.500
+85	924	101.250	791000	0.000	99.125	101.625
+85	925	104.250	3033600	0.000	99.750	104.750
+85	926	101.625	2025900	0.000	101.375	103.750
+85	927	99.875	1631900	0.000	99.375	101.250
+85	928	100.380	1315700	0.000	99.630	100.380
+86	929	94.625	1973000	92.000	91.875	94.750
+86	930	92.380	2515900	94.000	92.250	95.000
+86	931	91.250	2040100	92.625	91.250	94.250
+86	932	90.875	1611500	91.375	90.625	92.500
+86	933	90.250	1333300	90.750	89.750	90.875
+86	934	92.375	1338400	90.375	90.250	92.500
+86	935	95.380	3103900	94.000	93.500	95.750
+86	936	95.500	2253200	95.125	95.000	97.125
+86	937	96.375	3582000	95.000	92.625	98.125
+86	938	95.250	1209500	96.250	95.250	96.375
+86	939	92.880	1426400	95.250	92.880	95.500
+86	940	95.630	1273400	92.880	92.880	95.630
+87	941	92.250	1502600	91.125	90.000	92.250
+87	942	91.250	1632500	91.750	91.000	92.130
+87	943	89.880	1401700	90.380	89.630	90.880
+87	944	89.250	1724200	90.000	88.625	90.375
+87	945	89.750	945800	89.625	89.125	89.875
+87	946	90.250	1130700	89.750	89.750	90.875
+87	947	92.125	1581500	90.500	89.875	92.375
+87	948	90.750	1954000	92.750	90.130	93.250
+87	949	89.750	1295100	90.500	89.500	91.130
+87	950	89.625	1314500	90.000	89.125	91.125
+87	951	89.630	1148400	90.000	89.380	90.750
+87	952	89.750	1363200	89.750	89.125	90.625
+87	953	89.875	1446100	89.750	89.250	90.375
+87	954	89.380	1056500	89.880	88.750	90.250
+88	955	82.380	1926400	83.380	82.250	84.880
+88	956	84.875	3020100	82.625	82.625	85.000
+88	957	85.125	2275600	85.375	84.625	85.875
+88	958	86.000	2052900	85.630	84.500	86.130
+88	959	87.500	1712300	86.130	85.880	87.500
+88	960	88.250	3316600	89.500	88.125	89.750
+88	961	88.500	2215600	88.750	87.625	88.875
+88	962	88.625	2063100	88.500	87.500	88.625
+88	963	89.500	1792800	88.500	88.250	89.750
+88	964	90.000	2117200	89.630	89.500	90.250
+88	965	90.125	1324900	89.875	89.625	90.125
+88	966	89.250	1475400	90.000	89.000	90.630
+89	967	89.250	1475400	90.000	89.000	90.630
+89	968	88.130	993100	89.130	88.130	89.250
+89	969	88.125	1000900	87.875	87.750	88.750
+89	970	88.500	1108900	88.130	87.380	88.500
+89	971	88.130	1268600	88.880	87.750	88.880
+89	972	90.750	1681900	88.500	88.250	90.750
+89	973	90.625	2074400	91.250	90.375	92.125
+89	974	92.500	2341200	92.500	92.125	93.000
+89	975	93.875	2044900	92.500	92.375	94.000
+90	976	62.880	2541800	62.380	62.380	63.380
+90	977	56.130	12193500	0.000	56.000	59.880
+90	978	51.875	13421200	53.625	50.750	54.875
+90	979	53.000	8025400	0.000	51.250	53.380
+90	980	51.375	7658000	53.125	50.250	53.250
+90	981	48.875	7384600	51.250	48.750	51.625
+90	982	51.750	6115700	50.000	49.000	52.000
+90	983	51.250	4186200	51.625	51.000	52.625
+91	984	46.380	3638900	47.000	45.880	47.000
+91	985	48.625	3420500	46.500	46.500	48.750
+91	986	48.875	3054500	48.750	47.500	49.250
+91	987	49.000	7802400	52.875	48.750	53.250
+91	988	49.625	3307500	49.125	48.250	50.000
+91	989	50.250	3095700	50.000	49.750	50.875
+91	990	51.500	2981500	51.000	50.875	51.625
+91	991	52.625	3039500	52.000	52.000	52.625
+91	992	52.125	2635400	52.250	52.000	53.000
+92	993	42.250	2494200	43.625	42.000	43.625
+92	994	42.375	3922300	41.875	41.125	42.375
+92	995	45.630	6260300	44.880	44.500	45.880
+92	996	44.000	4216700	46.000	43.875	46.375
+93	997	51.875	2331700	52.250	51.500	52.625
+93	998	52.000	2595300	51.875	51.625	52.500
+93	999	53.125	2067200	52.375	51.875	53.250
+93	1000	55.125	4184000	54.000	53.750	55.250
+93	1001	55.750	1211000	55.500	55.125	56.000
+93	1002	54.380	2487900	56.000	54.380	56.130
+93	1003	53.875	1976800	54.500	53.750	55.125
+93	1004	53.130	2385300	54.750	53.130	54.750
+93	1005	53.750	1865800	53.250	53.250	54.125
+94	1006	59.000	1846800	58.130	58.000	59.130
+94	1007	59.500	2570000	59.000	58.500	59.875
+94	1008	58.500	2379800	60.000	58.500	60.000
+94	1009	58.875	1458600	58.500	58.000	59.375
+94	1010	59.250	1734900	58.875	58.500	59.500
+94	1011	58.630	1690600	59.130	58.500	59.380
+94	1012	58.130	1503100	58.750	58.130	58.880
+94	1013	58.750	1804200	58.250	58.250	59.000
+94	1014	58.630	1586000	58.750	58.380	59.500
+94	1015	57.500	1528100	58.500	57.130	58.500
+94	1016	57.125	2255900	57.375	56.375	57.375
+94	1017	56.000	1959500	56.750	55.750	57.000
+94	1018	55.250	2278500	55.875	55.125	55.875
+94	1019	55.250	3176600	55.125	54.750	55.625
+94	1020	58.630	4219300	55.750	55.750	58.750
+94	1021	58.250	10154900	58.880	55.380	59.380
+95	1022	52.880	1565600	53.380	52.750	53.630
+95	1023	53.250	1985500	52.500	52.000	53.380
+95	1024	54.000	1873700	53.375	53.375	54.625
+95	1025	54.500	1727400	54.380	54.000	54.630
+95	1026	54.625	1495700	55.250	54.500	55.250
+95	1027	52.750	2290200	54.880	52.500	55.000
+96	1028	53.380	2097500	53.500	51.380	53.750
+96	1029	52.250	2207700	53.250	51.750	53.500
+96	1030	58.380	8758100	55.000	54.000	58.630
+96	1031	58.750	5660500	58.375	58.000	59.500
+97	1032	58.500	2217200	58.880	58.500	59.250
+97	1033	57.125	2297500	57.875	56.750	58.250
+97	1034	57.500	1908600	57.375	56.875	57.875
+97	1035	58.000	1608500	57.500	56.875	58.375
+97	1036	58.380	1376600	58.130	57.880	58.630
+97	1037	57.750	1500300	58.250	57.500	58.250
+98	1038	55.625	1817200	56.750	55.625	56.875
+98	1039	55.880	3189300	55.750	54.500	56.250
+98	1040	62.380	8807500	59.000	58.000	62.630
+98	1041	61.380	3714100	62.250	61.130	62.380
+99	1042	72.250	2341600	73.625	72.250	74.125
+99	1043	73.125	2040800	72.500	71.875	73.125
+99	1044	73.250	1536400	72.880	72.750	73.750
+99	1045	74.375	2463400	73.500	73.375	74.500
+99	1046	75.380	3933900	74.000	73.750	75.750
+99	1047	75.000	5216900	75.500	73.875	76.125
+99	1048	74.625	3372500	74.625	74.125	75.375
+99	1049	73.000	2615200	74.625	72.875	74.625
+99	1050	73.880	2829600	72.630	72.630	74.380
+100	1051	71.750	1418500	72.750	71.500	73.000
+100	1052	72.130	2742400	72.000	70.250	72.880
+100	1053	73.625	3292000	72.375	72.250	73.875
+100	1054	74.380	1458600	73.630	73.500	74.380
+100	1055	74.625	2562800	74.875	74.500	75.250
+100	1056	74.380	1352100	74.380	74.130	74.880
+100	1057	74.250	1450700	74.250	73.875	74.375
+100	1058	74.250	1496400	74.125	74.125	74.875
+101	1059	87.130	2576400	87.880	86.000	88.250
+101	1060	89.000	5977700	90.500	88.630	90.500
+101	1061	91.625	5886400	89.375	89.125	92.000
+101	1062	93.500	4318900	91.500	91.000	93.880
+101	1063	93.625	3346300	93.125	92.875	93.875
+101	1064	95.250	3694500	93.125	92.875	95.625
+102	1065	89.375	3394000	90.125	88.375	90.750
+102	1066	89.130	3308400	89.630	88.630	90.250
+102	1067	90.250	2995700	90.500	89.750	90.750
+102	1068	91.750	2905200	91.380	90.880	92.250
+102	1069	93.000	2562500	91.875	91.875	93.375
+102	1070	93.000	2192900	93.630	93.000	94.000
+103	1071	109.000	3285600	106.500	105.625	109.500
+103	1072	108.625	1257200	109.125	108.500	109.750
+103	1073	108.875	1430800	109.125	108.625	109.375
+103	1074	109.630	1795300	108.880	108.880	110.000
+103	1075	109.875	1966800	110.000	109.625	110.375
+103	1076	109.125	1971400	109.875	108.875	110.375
+103	1077	109.250	2327700	109.500	108.380	109.630
+103	1078	110.750	1748300	109.250	109.250	110.875
+103	1079	112.000	2701000	111.000	110.250	112.125
+103	1080	112.375	2946800	112.125	110.750	112.750
+103	1081	113.625	3029700	112.375	112.250	114.625
+103	1082	111.375	3698100	113.875	111.125	114.000
+103	1083	106.750	4700700	111.380	106.500	111.380
+103	1084	107.630	5514300	107.000	105.630	108.250
+103	1085	106.625	3239000	108.125	106.500	108.500
+104	1086	91.880	3744400	89.130	89.130	91.880
+104	1087	89.375	3605800	92.000	89.125	92.000
+104	1088	90.875	2674000	89.875	89.750	91.125
+104	1089	91.250	1877400	90.880	90.880	91.880
+104	1090	91.750	945400	92.000	91.625	92.125
+104	1091	92.000	1572000	91.750	90.750	92.000
+105	1092	87.250	4624600	88.130	86.000	88.500
+105	1093	86.375	2664400	87.250	86.125	87.250
+105	1094	83.125	3742800	86.500	83.125	86.500
+105	1095	87.000	4613000	84.125	83.500	87.750
+105	1096	87.625	4821100	86.750	85.750	89.250
+105	1097	96.250	9856900	90.000	89.625	96.250
+105	1098	102.000	13145900	96.625	96.500	104.125
+105	1099	102.250	5293000	101.500	101.375	103.875
+105	1100	103.000	4959100	100.750	100.750	104.500
+105	1101	107.000	7989000	103.130	103.130	109.000
+105	1102	104.125	4721800	107.625	104.125	108.000
+106	1103	113.625	3467900	114.000	113.500	114.875
+106	1104	114.250	3237400	113.880	113.750	115.000
+106	1105	117.625	5835300	114.375	114.250	118.500
+106	1106	118.000	4985900	118.000	117.500	118.880
+106	1107	119.125	5322200	116.750	116.750	120.875
+106	1108	120.250	4097300	119.000	118.630	120.630
+106	1109	124.125	5355800	121.750	121.250	125.625
+106	1110	125.630	4126200	125.000	124.000	126.130
+106	1111	124.880	2882500	123.880	123.880	125.500
+107	1112	108.750	4722900	114.875	108.375	115.125
+107	1113	110.750	4558900	108.375	107.625	112.000
+107	1114	111.250	3619800	111.130	110.250	113.750
+107	1115	109.750	3046600	111.875	109.500	112.375
+107	1116	111.250	2931600	110.380	110.380	113.750
+107	1117	110.380	2379500	112.130	110.000	112.750
+107	1118	117.625	5407700	111.000	110.500	118.125
+107	1119	119.750	4149900	117.500	116.130	120.500
+108	1120	91.750	3683600	89.630	89.130	92.630
+108	1121	103.630	9014900	96.000	95.630	103.750
+108	1122	103.875	3990400	103.125	101.625	104.125
+108	1123	104.750	3338200	103.500	103.380	105.500
+108	1124	107.380	3992800	105.250	103.750	108.000
+108	1125	107.500	3505300	107.125	107.000	108.875
+108	1126	107.250	3120600	108.000	107.250	109.000
+108	1127	108.875	2265800	108.375	107.750	109.375
+108	1128	109.250	2151100	108.000	108.000	109.375
+109	1129	124.750	1312200	124.750	124.000	125.500
+109	1130	124.500	1864400	124.875	124.125	127.125
+109	1131	123.750	2446500	124.000	123.130	125.250
+109	1132	125.880	2013200	124.250	124.000	125.880
+109	1133	125.000	1845100	125.750	124.625	126.250
+109	1134	126.625	2302300	125.875	125.625	127.125
+109	1135	127.875	1835400	126.875	126.875	128.000
+109	1136	128.380	3148000	128.000	127.630	129.500
+109	1137	126.250	2402400	129.000	126.130	129.380
+109	1138	127.750	2248500	125.500	125.500	128.000
+109	1139	129.875	2858300	128.500	128.250	130.125
+109	1140	130.000	1400900	129.880	129.130	130.000
+109	1141	129.500	2863900	130.750	128.630	131.250
+109	1142	127.625	1875700	129.500	127.375	129.500
+109	1143	125.625	2135400	128.750	125.250	129.125
+109	1144	129.375	3473700	125.500	124.000	129.500
+110	1145	133.375	2886400	130.625	130.250	133.875
+110	1146	133.750	3481600	133.125	132.750	135.125
+110	1147	134.375	1569400	133.375	132.875	134.500
+110	1148	134.500	1429200	134.375	134.000	134.875
+110	1149	133.250	2724100	135.000	133.000	135.750
+110	1150	134.750	2392300	133.500	133.250	135.875
+110	1151	136.880	2774500	134.630	134.630	137.630
+110	1152	145.000	7803800	137.630	137.380	145.500
+110	1153	146.750	5777100	144.750	143.000	150.000
+111	1154	157.625	2833000	158.125	156.125	158.500
+111	1155	158.000	5515400	157.500	157.000	160.750
+111	1156	158.125	2799700	158.250	157.750	160.250
+111	1157	159.375	931300	158.125	158.125	159.625
+111	1158	163.000	3316000	159.630	159.630	163.500
+111	1159	162.625	4677800	163.625	162.625	166.000
+111	1160	162.000	4437400	162.875	159.125	164.000
+111	1161	158.500	3036300	161.375	158.375	162.625
+111	1162	155.625	6051900	151.000	151.000	158.000
+111	1163	160.000	2769900	156.630	156.250	160.130
+112	1164	141.250	3583000	143.130	140.250	143.500
+112	1165	137.625	6308500	140.750	137.125	140.875
+112	1166	143.875	4282700	137.625	0.000	144.000
+112	1167	146.500	4198400	144.380	144.130	148.000
+112	1168	146.875	3670400	147.000	144.000	147.500
+112	1169	143.000	1772200	146.880	143.000	146.880
+113	1170	136.500	2634400	134.500	133.250	136.880
+113	1171	137.880	3931900	138.630	134.380	139.000
+113	1172	137.500	1845300	137.380	135.750	138.380
+113	1173	137.750	2655100	137.375	136.750	139.750
+113	1174	139.750	2343200	138.000	137.750	140.130
+113	1175	137.380	2356700	139.630	136.630	141.250
+113	1176	140.000	3022200	137.250	134.750	140.130
+113	1177	142.380	3917100	140.000	139.000	143.750
+113	1178	153.625	10659100	150.500	149.500	157.000
+113	1179	150.750	3005000	152.250	150.625	152.875
+114	1180	98.625	2949600	102.437	98.625	104.125
+114	1181	99.130	2060000	100.130	99.000	100.810
+114	1182	101.690	1069100	99.380	99.250	101.750
+114	1183	102.750	2307300	102.875	102.750	103.687
+114	1184	103.130	3558000	103.690	102.940	105.500
+114	1185	104.625	2736500	104.000	103.187	105.187
+114	1186	105.625	2635000	104.500	104.062	105.750
+114	1187	106.437	5017000	105.562	105.000	106.812
+114	1188	105.250	3555600	105.375	105.062	107.187
+114	1189	104.250	4308200	105.000	102.625	105.000
+114	1190	104.190	4059400	103.500	102.940	105.500
+114	1191	100.062	7182800	104.312	99.375	104.875
+114	1192	100.130	6410700	98.250	97.500	101.130
+114	1193	102.130	3414500	100.560	100.310	102.440
+114	1194	102.625	4275400	103.500	101.250	104.000
+114	1195	103.437	6321700	103.187	102.375	104.937
+114	1196	105.000	4189100	104.875	104.500	105.187
+114	1197	108.380	4970600	105.190	104.690	108.380
+114	1198	100.125	13907800	100.250	99.625	102.000
+114	1199	99.380	6583500	99.810	98.500	100.630
+114	1200	99.187	3911500	99.812	98.937	100.000
+114	1201	98.125	2807900	99.250	98.000	99.937
+114	1202	96.500	7019700	96.750	95.630	97.880
+114	1203	97.000	5192400	97.130	96.500	98.000
+114	1204	98.187	4413600	97.000	97.000	99.312
+114	1205	98.750	2900100	98.312	98.000	99.562
+114	1206	100.562	3437700	100.000	100.000	101.250
+114	1207	99.380	3585600	100.130	98.690	100.250
+115	1208	98.125	5205100	99.125	98.125	99.812
+115	1209	96.125	6464200	97.125	95.875	98.250
+115	1210	97.500	4167100	97.380	96.690	98.190
+115	1211	99.130	3403400	97.880	97.440	99.880
+115	1212	100.250	2951700	99.187	98.187	100.625
+115	1213	99.630	2925300	100.880	99.000	101.130
+115	1214	101.250	3420700	99.690	99.250	101.380
+115	1215	100.880	3585100	101.310	99.560	101.810
+115	1216	101.560	3594700	100.630	100.560	102.310
+115	1217	102.875	2585000	101.687	101.437	103.000
+115	1218	102.062	4107800	103.000	101.437	103.250
+116	1219	108.812	4240700	109.000	107.625	109.125
+116	1220	106.130	4884000	108.750	106.000	109.250
+116	1221	108.130	4276900	106.250	106.190	109.500
+116	1222	111.750	4039200	110.000	109.940	112.310
+116	1223	112.000	3030000	111.880	110.060	112.130
+116	1224	112.440	3187800	112.500	112.000	114.690
+116	1225	113.000	1636400	112.380	112.310	113.440
+116	1226	114.125	2184600	114.125	113.562	114.812
+116	1227	114.810	4356000	114.130	113.500	116.310
+116	1228	116.750	3166400	116.000	115.500	116.937
+116	1229	115.190	3219800	116.560	114.500	116.630
+117	1230	115.000	2258200	113.750	113.630	115.560
+117	1231	117.062	3189500	114.750	114.625	118.500
+117	1232	118.500	3070100	118.250	117.375	119.250
+117	1233	119.380	2339800	118.500	118.250	120.000
+117	1234	119.625	2268600	119.750	119.000	120.625
+117	1235	117.000	2557800	119.250	117.000	120.000
+117	1236	118.380	2180100	117.130	116.000	118.940
+117	1237	120.187	3168400	118.562	118.500	120.375
+117	1238	122.000	4489200	121.000	120.312	123.000
+117	1239	128.130	12308600	128.250	127.750	131.000
+117	1240	127.440	6049900	126.000	124.500	128.440
+117	1241	123.880	4153100	127.190	123.690	129.000
+117	1242	124.250	2874600	125.875	122.500	125.937
+118	1243	123.500	4994700	125.750	123.375	127.437
+118	1244	122.500	5579800	122.130	118.190	122.500
+118	1245	126.562	4965100	121.375	120.625	126.562
+118	1246	129.130	4557100	128.000	127.630	129.940
+118	1247	130.000	3246800	129.000	128.500	130.687
+118	1248	130.500	3319400	130.000	128.190	130.940
+119	1249	133.625	3820400	134.500	132.187	136.187
+119	1250	131.437	3544900	133.750	130.000	134.125
+119	1251	128.500	4149700	130.750	127.630	131.500
+119	1252	125.375	6268700	125.062	123.375	126.437
+119	1253	124.810	7302600	124.130	118.940	125.250
+119	1254	120.250	6721000	123.750	117.310	123.750
+119	1255	119.250	7183700	124.000	118.750	124.000
+119	1256	120.750	6582400	119.500	118.125	122.875
+119	1257	123.500	9156300	120.750	116.812	123.500
+119	1258	127.312	5841600	124.437	122.500	127.937
+119	1259	130.875	5402500	130.125	129.562	132.875
+119	1260	128.190	4696800	130.500	126.560	130.500
+120	1261	147.380	3456500	148.500	147.190	149.940
+120	1262	147.880	4096300	148.880	145.750	149.750
+120	1263	149.000	3293000	147.690	146.810	149.560
+120	1264	149.940	3569000	148.880	147.500	150.940
+120	1265	151.380	3788700	150.750	149.940	152.190
+120	1266	156.000	4371500	150.880	150.690	156.750
+120	1267	157.125	5109100	157.000	156.500	158.875
+120	1268	157.880	4333900	156.690	156.630	159.880
+120	1269	157.437	2997600	157.000	155.750	157.500
+121	1270	165.000	3885800	163.500	162.500	165.810
+121	1271	164.375	2826400	166.875	164.312	166.875
+121	1272	166.062	4381600	165.437	165.062	169.625
+121	1273	171.560	4908400	166.630	166.630	172.190
+121	1274	176.375	3739300	171.562	171.562	178.937
+121	1275	182.250	4352300	177.500	175.250	183.000
+121	1276	185.000	3535000	182.690	181.130	185.380
+121	1277	187.937	1524900	184.750	184.062	187.937
+121	1278	189.250	2634600	186.500	186.000	189.937
+121	1279	187.125	1881100	188.625	187.000	188.937
+121	1280	186.750	2406600	186.875	186.312	188.625
+121	1281	184.375	1932400	186.750	183.500	187.187
+121	1282	183.000	4074800	185.000	181.500	186.500
+121	1283	189.630	4953800	183.000	182.810	189.880
+122	1284	177.937	4600100	172.250	171.062	177.937
+122	1285	176.940	4554600	178.000	175.750	180.000
+122	1286	173.750	4318700	177.250	173.130	179.690
+122	1287	173.625	5234800	172.000	169.312	173.937
+122	1288	169.750	7399000	171.000	166.500	171.880
+122	1289	168.375	4795400	169.500	165.125	169.500
+122	1290	167.750	4371200	170.500	166.880	170.940
+122	1291	166.750	3871500	168.500	165.062	168.937
+122	1292	171.000	10750500	173.500	170.125	177.000
+122	1293	178.380	5803200	173.880	173.750	178.940
+122	1294	178.875	3779900	178.625	177.062	180.000
+122	1295	182.190	5009500	179.880	178.560	184.880
+122	1296	181.500	3504900	181.630	180.250	182.880
+123	1297	182.000	3951400	179.187	177.125	182.312
+123	1298	181.000	3303000	182.000	180.190	183.500
+123	1299	178.060	3452700	179.500	178.000	180.000
+123	1300	177.630	7252500	177.500	172.940	177.750
+123	1301	168.562	9748400	176.625	168.000	178.000
+123	1302	167.000	5948300	168.687	166.625	170.500
+123	1303	165.380	5007500	167.000	164.000	167.630
+123	1304	169.500	5910500	168.000	166.500	170.500
+123	1305	171.312	4914400	172.000	169.500	173.250
+123	1306	172.380	4133200	171.060	170.560	175.880
+123	1307	178.562	3733400	177.000	176.875	181.250
+123	1308	177.250	2917600	180.000	176.500	181.500
+124	1309	187.000	4265500	186.000	182.312	188.062
+124	1310	186.310	3055900	185.630	184.000	188.060
+124	1311	183.440	4489900	183.500	181.060	183.630
+124	1312	180.000	3423200	184.000	179.940	184.000
+124	1313	179.380	3630500	182.750	177.630	183.880
+124	1314	177.750	4165200	179.500	173.500	179.500
+124	1315	170.375	5964400	177.500	170.125	177.500
+124	1316	166.750	7691800	170.880	164.060	172.310
+124	1317	169.750	7358000	167.000	163.000	169.750
+124	1318	171.875	6580800	169.750	165.125	171.875
+124	1319	194.500	19584900	197.250	192.687	198.750
+124	1320	199.750	10375500	193.380	192.940	206.560
+124	1321	209.880	9333000	204.750	202.310	210.750
+124	1322	212.000	10464600	212.000	208.500	215.250
+124	1323	205.000	5670900	209.250	203.500	210.500
+125	1324	209.250	4169800	211.880	208.000	214.000
+125	1325	217.250	4993400	211.250	211.062	217.500
+125	1326	218.630	4774200	217.250	217.060	221.880
+125	1327	221.000	4186400	220.500	217.500	221.875
+125	1328	225.500	6033400	221.125	215.500	228.437
+125	1329	246.000	12165700	234.000	234.000	246.000
+125	1330	239.250	9036900	241.500	236.000	243.500
+125	1331	237.500	5780200	236.000	232.000	239.625
+125	1332	238.500	4316900	238.250	237.000	240.625
+125	1333	235.875	3895800	238.250	233.062	238.750
+125	1334	232.875	3220900	235.625	232.500	236.375
+125	1335	230.380	3452300	232.750	229.190	233.380
+125	1336	223.750	4299100	230.310	221.750	230.690
+125	1337	221.187	4702500	222.500	221.000	226.000
+125	1338	236.250	8314000	223.000	221.440	236.630
+125	1339	116.000	10552600	116.687	112.625	116.875
+125	1340	116.000	6379500	116.000	114.187	116.750
+126	1341	124.440	6043500	122.750	122.310	124.880
+126	1342	122.000	8442100	123.375	121.000	123.625
+126	1343	122.380	6463000	122.000	120.880	124.060
+126	1344	122.940	6245300	122.630	122.060	125.880
+126	1345	124.000	4354600	125.000	122.750	125.190
+126	1346	123.187	4773000	124.250	122.687	125.625
+127	1347	98.000	18720400	94.000	93.562	98.250
+127	1348	104.750	19673300	99.000	98.380	105.000
+127	1349	107.880	13681500	105.500	104.440	108.440
+127	1350	106.125	10665000	108.812	106.000	109.875
+127	1351	104.060	9710300	105.000	101.810	105.000
+127	1352	105.000	3108400	105.937	104.125	106.000
+127	1353	104.187	5677700	104.937	103.375	104.937
+127	1354	103.060	5719900	103.690	102.130	104.500
+127	1355	103.750	5336000	102.562	102.250	104.437
+127	1356	105.560	6208600	103.440	103.380	106.310
+127	1357	112.000	14679900	109.812	107.937	112.875
+127	1358	116.000	9924400	113.000	112.500	116.500
+128	1359	109.190	7117300	108.000	105.750	109.690
+128	1360	109.125	7680700	110.687	108.625	111.062
+128	1361	108.060	4834900	109.060	107.750	110.440
+128	1362	110.125	4773500	108.500	108.062	110.125
+128	1363	108.375	4005500	109.875	108.250	110.312
+128	1364	108.625	4528700	109.125	108.187	110.437
+128	1365	109.750	3740700	109.687	108.125	110.000
+128	1366	109.250	4081300	109.560	109.060	110.750
+128	1367	109.060	2683100	110.440	108.750	110.500
+128	1368	108.750	3435100	109.690	108.560	110.500
+128	1369	107.875	2870300	109.187	106.625	109.500
+128	1370	115.625	10346800	112.437	111.875	115.812
+128	1371	112.060	8227800	114.000	110.880	114.500
+129	1372	107.000	5665900	107.500	106.130	108.250
+129	1373	109.000	6622500	109.000	107.880	109.810
+129	1374	110.000	8856400	108.250	107.687	111.687
+129	1375	112.750	6563500	110.000	109.940	113.380
+129	1376	113.500	6771400	112.810	111.940	114.690
+129	1377	114.250	6137800	114.500	113.875	115.375
+129	1378	115.250	6492400	115.875	111.625	115.875
+129	1379	121.500	12124300	115.375	115.062	122.250
+129	1380	126.880	11862900	125.000	124.630	128.250
+129	1381	122.500	8163000	125.750	122.060	127.000
+130	1382	102.940	6402800	104.130	102.630	104.810
+130	1383	103.500	7428100	102.380	101.000	104.380
+130	1384	117.000	22927900	112.000	111.750	117.812
+130	1385	114.440	8764700	117.000	113.750	117.000
+131	1386	90.130	7705300	90.190	89.810	94.440
+131	1387	86.000	12579500	85.880	84.810	88.000
+131	1388	81.560	12724900	85.500	80.060	87.940
+131	1389	89.000	9514000	84.500	84.500	89.375
+131	1390	84.810	6052300	89.000	84.250	89.560
+132	1391	92.750	5671800	93.750	91.810	94.000
+132	1392	96.687	9712900	95.375	94.312	97.750
+132	1393	108.312	25243300	104.375	103.500	110.000
+132	1394	111.250	14760400	107.500	107.250	113.937
+133	1395	106.470	6793100	107.170	105.370	107.340
+133	1396	99.290	16688100	105.000	97.905	105.010
+133	1397	95.490	14494800	97.900	95.020	98.400
+133	1398	98.390	12127900	95.100	94.200	98.440
+133	1399	94.960	14246600	95.400	93.340	98.900
+134	1400	99.700	9957300	97.000	97.000	99.880
+134	1401	106.500	19849500	103.000	102.300	110.000
+134	1402	114.470	25822400	112.000	110.450	115.900
+134	1403	114.830	12759100	114.350	113.750	116.400
+135	1404	119.600	7726900	117.500	117.500	119.600
+135	1405	117.800	5629100	119.520	117.760	119.900
+135	1406	115.270	5818000	117.350	115.080	117.590
+135	1407	112.650	9598700	114.000	112.000	114.100
+135	1408	111.800	6757600	112.650	111.640	113.860
+135	1409	112.890	5138900	112.500	111.900	113.600
+135	1410	113.640	3812200	113.250	112.850	114.750
+135	1411	116.970	6154400	113.640	113.560	117.600
+135	1412	117.500	9625700	117.600	116.500	119.000
+135	1413	117.250	6423700	117.000	117.000	118.250
+135	1414	116.100	3099100	117.500	115.600	117.500
+135	1415	117.360	6542700	116.300	116.100	118.140
+136	1416	104.720	9312600	106.500	103.650	106.950
+136	1417	101.960	10266800	104.730	101.600	105.400
+136	1418	103.850	8894800	101.600	101.560	104.130
+136	1419	107.250	8767200	105.900	105.620	107.800
+136	1420	108.530	7841800	106.150	106.110	109.300
+136	1421	107.820	5330100	108.530	107.280	109.380
+137	1422	108.800	5675100	108.500	107.210	109.470
+137	1423	108.180	3704100	107.800	107.100	110.090
+137	1424	106.510	4176800	107.250	105.790	107.370
+137	1425	106.250	4028300	105.800	105.260	107.370
+137	1426	104.190	6398000	105.000	104.100	106.100
+137	1427	104.080	6763200	103.700	102.700	104.540
+137	1428	104.950	5254800	103.850	102.810	104.950
+137	1429	105.860	6024400	105.000	104.650	106.640
+137	1430	106.200	5730200	106.500	105.400	106.850
+137	1431	105.010	4713100	106.350	105.010	106.590
+138	1432	91.300	13783100	94.450	90.300	94.450
+138	1433	90.000	13172200	91.300	87.490	91.500
+138	1434	91.720	11077200	90.200	89.900	92.710
+138	1435	92.710	8676200	91.720	91.340	93.480
+138	1436	93.770	7221200	92.300	92.100	93.900
+138	1437	96.950	11118800	92.900	92.400	97.620
+138	1438	97.310	9785100	97.150	96.800	98.880
+138	1439	98.020	9733000	96.650	95.370	98.450
+138	1440	98.500	7093400	98.030	96.750	99.000
+138	1441	97.140	7525300	98.500	96.760	98.500
+139	1442	123.200	7950400	121.120	120.060	123.210
+139	1443	120.250	8061000	121.550	120.120	122.300
+139	1444	121.100	7036000	120.800	119.410	121.480
+139	1445	121.340	6225500	120.150	120.150	122.140
+139	1446	122.200	5596200	121.510	121.350	122.970
+139	1447	123.890	8054200	121.900	121.010	124.700
+140	1448	75.490	7953000	77.850	75.200	78.250
+140	1449	74.650	13296100	75.400	73.250	75.950
+140	1450	75.600	8909700	75.450	75.300	77.400
+140	1451	76.170	8592000	74.200	73.250	76.770
+140	1452	77.140	7775800	76.180	75.160	77.750
+140	1453	75.940	7240900	75.600	75.470	76.900
+141	1454	72.000	10945600	71.350	71.250	73.620
+141	1455	67.600	12056000	72.010	67.160	72.410
+141	1456	68.580	12012900	67.850	67.700	69.400
+141	1457	70.510	8469900	67.950	67.870	70.710
+141	1458	73.500	5243000	72.450	72.000	73.900
+141	1459	71.300	9488100	73.000	71.030	73.480
+141	1460	69.670	11165100	71.400	69.450	72.250
+141	1461	68.760	10073400	70.600	68.530	71.250
+141	1462	69.410	11034600	68.000	67.550	69.750
+142	1463	69.350	10989800	68.900	67.300	70.000
+142	1464	66.400	11576400	68.750	65.760	69.470
+142	1465	71.180	11918600	70.370	68.000	71.400
+142	1466	71.790	10136400	70.500	70.020	72.700
+142	1467	70.400	12708100	70.750	69.110	71.600
+142	1468	68.250	8832000	70.400	67.220	70.700
+142	1469	67.880	6915000	68.210	67.123	68.230
+142	1470	65.990	7126900	67.940	65.850	68.350
+142	1471	67.900	8370800	67.000	66.610	69.420
+142	1472	69.170	8036800	69.000	66.800	69.900
+142	1473	71.610	8432500	69.200	68.350	72.050
+142	1474	71.830	6940900	70.610	70.400	74.300
+142	1475	71.770	5263300	71.050	70.610	72.640
+143	1476	55.070	12155800	56.050	54.810	56.700
+143	1477	57.580	12672600	54.650	54.010	58.480
+143	1478	63.920	17852500	62.000	61.500	63.920
+143	1479	63.420	7925300	61.540	61.540	63.800
+143	1480	68.480	14600200	67.750	66.580	68.480
+143	1481	64.900	15828900	66.600	64.240	67.005
+143	1482	72.200	21344500	72.800	71.230	73.000
+143	1483	74.250	12532700	71.730	70.260	74.250
+144	1484	72.100	10570000	75.500	71.750	75.550
+144	1485	74.560	7538800	72.500	72.160	74.620
+144	1486	76.560	12544800	75.150	74.560	77.500
+144	1487	76.740	11648500	76.560	74.200	77.050
+144	1488	78.670	12865800	77.000	76.020	79.400
+144	1489	78.940	9844300	78.670	78.500	79.790
+144	1490	80.400	9395200	78.900	78.320	80.500
+144	1491	82.500	12005100	82.000	81.700	83.810
+144	1492	81.680	10466700	81.750	80.470	82.010
+145	1493	80.260	5922700	79.750	79.440	81.507
+145	1494	79.760	2420200	80.200	79.760	81.000
+145	1495	78.500	5784300	79.810	78.260	80.700
+145	1496	77.360	6334400	78.500	76.610	79.180
+145	1497	76.250	8066600	77.000	75.600	77.430
+145	1498	77.500	7871400	77.100	77.100	78.460
+145	1499	80.570	7864500	78.800	78.190	80.570
+145	1500	81.650	5963400	80.700	80.210	81.650
+145	1501	83.590	7923300	81.900	81.810	84.590
+145	1502	86.000	11907000	83.950	83.750	86.180
+145	1503	84.190	9509700	85.550	84.070	85.690
+146	1504	76.500	5617700	77.500	76.500	78.090
+146	1505	75.860	11218600	76.100	74.310	76.350
+146	1506	77.450	8300100	76.400	75.350	77.450
+146	1507	79.330	7793000	78.020	77.910	79.510
+146	1508	79.510	5441300	79.080	78.826	79.700
+146	1509	79.040	6295600	79.570	78.710	80.050
+147	1510	79.850	7024700	81.130	79.800	81.330
+147	1511	79.750	7472200	79.700	78.730	80.680
+147	1512	80.690	5298500	79.690	79.380	80.820
+147	1513	80.880	4615600	81.050	80.350	81.270
+147	1514	81.020	4262200	80.870	80.280	81.500
+147	1515	81.510	4634200	81.450	80.650	81.540
+148	1516	89.700	8842500	91.450	89.010	91.510
+148	1517	90.310	5529700	89.900	89.750	90.460
+148	1518	94.020	19813600	95.070	93.550	95.650
+148	1519	95.320	9302100	95.000	94.710	95.350
+148	1520	97.100	9285300	96.000	95.730	97.440
+148	1521	97.700	6952700	97.230	96.640	98.040
+148	1522	97.510	4371600	97.840	97.320	98.160
+149	1523	99.300	3611700	100.060	99.300	100.300
+149	1524	99.710	3562400	99.100	99.080	100.090
+149	1525	99.370	3930500	99.990	99.320	100.000
+149	1526	98.420	4830500	99.310	98.150	99.770
+149	1527	97.800	5447700	98.420	97.520	99.230
+149	1528	97.310	5690200	98.600	97.190	98.600
+149	1529	95.960	6629600	97.400	95.460	97.510
+149	1530	96.790	7012100	95.200	95.200	97.460
+149	1531	96.540	3362500	96.500	96.230	97.090
+150	1532	96.840	4807000	96.570	95.600	96.890
+150	1533	96.390	3548500	96.580	96.130	96.920
+150	1534	96.450	3971800	95.950	95.560	96.980
+150	1535	94.590	5084200	96.490	94.590	96.880
+150	1536	94.530	6391400	94.300	93.770	95.280
+150	1537	93.060	6536400	94.380	92.680	94.740
+150	1538	91.210	8560800	92.000	91.150	92.980
+150	1539	93.300	6290600	92.000	91.680	93.380
+151	1540	85.350	5677300	85.300	85.050	85.940
+151	1541	83.650	7530100	85.000	83.580	85.010
+151	1542	83.890	6595200	84.400	83.510	84.500
+151	1543	84.950	6009200	84.000	83.420	85.250
+151	1544	85.250	6001500	85.900	85.200	86.090
+151	1545	84.130	6298700	84.840	83.780	85.240
+152	1546	83.690	5268500	83.700	83.100	83.980
+152	1547	82.210	7134900	83.050	81.900	83.050
+152	1548	83.910	5916900	82.540	82.510	83.940
+152	1549	84.020	4361500	83.700	83.510	84.560
+152	1550	84.040	3559500	84.100	83.660	84.540
+152	1551	85.130	4833700	83.600	83.570	85.130
+153	1552	84.160	4650300	84.100	83.980	84.440
+153	1553	84.480	3874200	84.350	83.880	84.650
+153	1554	84.980	4204600	84.480	84.150	84.980
+153	1555	85.740	5198000	85.140	85.010	85.980
+153	1556	86.720	4542700	85.950	85.880	86.980
+153	1557	87.160	5001400	87.000	86.720	88.100
+153	1558	87.320	5150800	87.950	87.130	88.030
+153	1559	88.040	3984400	87.140	87.100	88.100
+153	1560	87.420	3076900	88.040	87.400	88.100
+153	1561	86.710	4090000	87.430	86.510	87.910
+153	1562	86.630	3016300	86.770	86.270	87.200
+153	1563	86.000	4626600	86.020	85.580	86.200
+153	1564	84.980	6665200	86.260	84.430	86.480
+153	1565	84.780	4233700	84.750	84.300	84.980
+153	1566	84.850	5928500	84.780	84.600	85.250
+153	1567	85.920	7275600	84.300	84.290	86.150
+153	1568	89.370	13824500	88.200	88.000	89.730
+153	1569	88.820	7079800	88.450	88.290	89.190
+153	1570	88.100	6272100	88.400	87.660	88.760
+153	1571	87.390	5998900	88.220	87.290	88.450
+153	1572	88.430	5775000	87.360	87.310	88.900
+153	1573	89.000	7337300	88.330	88.250	89.570
+153	1574	90.000	6035100	88.580	88.500	90.270
+153	1575	89.500	4226500	89.800	89.430	90.240
+153	1576	89.750	4518500	89.400	88.950	89.900
+153	1577	90.110	5160900	89.330	89.230	90.600
+153	1578	90.470	5391100	89.550	89.500	91.220
+153	1579	91.200	6553300	91.250	90.970	91.900
+153	1580	92.380	6952100	91.050	90.820	92.700
+153	1581	93.280	6708500	92.400	92.400	93.520
+153	1582	93.370	4907300	92.500	92.500	93.700
+153	1583	93.370	4513100	93.000	93.000	93.950
+154	1584	94.130	3541200	94.280	94.130	94.640
+154	1585	92.700	5664200	94.140	92.540	94.830
+154	1586	92.760	4838700	92.950	92.300	93.100
+154	1587	93.300	4233000	92.700	92.500	93.970
+154	1588	93.570	2868800	93.160	93.160	94.020
+154	1589	94.330	4299200	93.500	93.480	94.670
+155	1590	93.300	3821900	92.640	92.590	93.430
+155	1591	92.920	4541200	92.750	92.750	93.730
+155	1592	92.410	4037900	93.150	92.200	93.210
+155	1593	92.370	4754000	92.940	92.360	93.180
+155	1594	91.600	8051000	92.350	91.590	92.510
+155	1595	92.130	5087800	91.700	91.700	92.560
+155	1596	92.350	4513900	92.020	92.010	93.000
+155	1597	92.410	3424800	92.350	92.090	92.800
+155	1598	91.510	4494300	92.250	91.200	92.410
+156	1599	91.380	4419100	90.460	90.220	91.410
+156	1600	90.440	5740800	91.490	90.040	91.760
+156	1601	90.320	3737900	90.080	89.773	90.620
+156	1602	89.570	4579300	90.230	89.260	90.330
+156	1603	89.000	7836800	89.050	88.706	89.380
+156	1604	88.440	6376500	89.000	88.100	89.200
+156	1605	87.600	5273400	88.280	87.500	88.460
+156	1606	86.200	8296500	87.350	86.090	87.560
+156	1607	85.750	8450000	85.900	85.170	86.240
+156	1608	84.570	7090800	85.760	84.240	85.972
+156	1609	83.640	10708300	84.630	83.470	85.410
+156	1610	76.700	27932400	79.490	76.330	79.660
+156	1611	76.650	13256100	77.150	76.140	77.750
+156	1612	75.480	9292700	76.980	75.250	77.200
+156	1613	72.010	20389800	75.480	71.850	75.870
+156	1614	74.030	16273600	72.990	72.800	74.100
+156	1615	74.210	10985200	74.050	73.260	74.700
+156	1616	74.610	10223400	75.240	74.050	75.720
+156	1617	75.430	12500200	74.680	74.650	76.980
+156	1618	77.050	11706800	75.690	75.500	77.180
+156	1619	75.910	8638300	77.050	75.650	77.110
+157	1620	83.140	5340900	82.930	82.500	83.780
+157	1621	84.000	4490200	83.140	83.000	84.080
+157	1622	84.190	4712900	83.710	83.560	84.630
+157	1623	84.190	5210600	84.150	83.790	84.690
+157	1624	84.700	4661700	84.200	84.100	84.900
+157	1625	86.080	7788600	84.450	84.440	86.210
+157	1626	86.710	8633600	86.080	85.750	87.600
+157	1627	86.950	13328300	86.330	86.220	87.940
+157	1628	89.820	27876200	90.510	89.730	92.040
+157	1629	89.860	10676400	89.800	89.310	90.980
+157	1630	90.480	7391600	90.040	89.550	90.700
+157	1631	91.560	8862300	90.050	90.010	92.000
+157	1632	91.490	9222500	90.910	90.800	91.950
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 35b90a04492..ecbe835cbb4 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1,11 +1,25 @@
 --
 -- Test for row pattern recognition: WINDOW clause integration and
--- real-world scenario tests using stock price data.
+-- scenario tests using synthetic stock data.
 --
 -- Parser/planner tests: rpr_base.sql
 -- NFA engine tests: rpr_nfa.sql
 -- EXPLAIN statistics tests: rpr_explain.sql
 --
+\getenv abs_srcdir PG_ABS_SRCDIR
+-- Synthetic stock data for RPR pattern matching tests
+CREATE TABLE rpr_stock (
+       part_id integer,
+       rn      integer,
+       price   numeric(10,3),
+       volume  bigint,
+       open    numeric(10,3),
+       low     numeric(10,3),
+       high    numeric(10,3)
+);
+\set filename :abs_srcdir '/data/stock.data'
+COPY rpr_stock FROM :'filename';
+ANALYZE rpr_stock;
 CREATE TEMP TABLE stock (
        company TEXT,
        tdate DATE,
@@ -1839,3 +1853,504 @@ WINDOW w AS (
 (6 rows)
 
 DROP TABLE rpr_consec_null;
+-- ============================================================
+-- Stock Scenario Tests (1632 rows, partitioned regions)
+-- ============================================================
+-- Consecutive rising days: find streaks of 7+ days
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (UP{7,})
+        DEFINE UP AS price > PREV(price)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | days 
+----------+--------+------
+       29 |     35 |    7
+       38 |     44 |    7
+       96 |    102 |    7
+      118 |    125 |    8
+      308 |    317 |   10
+      328 |    334 |    7
+      475 |    481 |    7
+      491 |    497 |    7
+      509 |    517 |    9
+      536 |    542 |    7
+      586 |    592 |    7
+      643 |    650 |    8
+      740 |    746 |    7
+      753 |    760 |    8
+      904 |    910 |    7
+      956 |    965 |   10
+      985 |    991 |    7
+     1095 |   1101 |    7
+     1104 |   1110 |    7
+     1181 |   1187 |    7
+     1221 |   1228 |    8
+     1262 |   1268 |    7
+     1272 |   1278 |    7
+     1373 |   1380 |    8
+     1434 |   1440 |    7
+     1485 |   1491 |    7
+     1553 |   1559 |    7
+     1576 |   1582 |    7
+     1624 |   1631 |    8
+(29 rows)
+
+-- V-shape recovery: 4+ days decline followed by 4+ days rise
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (DECLINE{4,} RISE{4,})
+        DEFINE
+            DECLINE AS price < PREV(price),
+            RISE AS price > PREV(price)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_price | end_price | days 
+----------+--------+-------------+-----------+------
+       84 |     93 |     301.000 |   309.750 |   10
+      114 |    125 |     394.500 |   418.750 |   12
+      173 |    183 |     260.000 |   282.000 |   11
+      204 |    214 |     179.000 |   171.500 |   11
+      262 |    271 |     277.625 |   275.500 |   10
+      337 |    344 |     308.250 |   308.125 |    8
+      436 |    444 |      56.630 |    60.250 |    9
+      567 |    575 |     126.500 |   127.500 |    9
+      598 |    607 |     112.250 |   116.500 |   10
+      653 |    660 |     129.375 |   128.125 |    8
+      663 |    671 |     125.630 |   130.250 |    9
+      685 |    693 |     120.130 |   124.000 |    9
+      710 |    719 |     141.125 |   144.130 |   10
+      833 |    841 |     106.500 |   111.125 |    9
+      853 |    862 |     105.375 |   107.750 |   10
+      930 |    937 |      92.380 |    96.375 |    8
+     1188 |   1197 |     105.250 |   108.380 |   10
+     1198 |   1206 |     100.125 |   100.562 |    9
+     1250 |   1259 |     131.437 |   130.875 |   10
+     1285 |   1295 |     176.940 |   182.190 |   11
+     1298 |   1307 |     181.000 |   178.562 |   10
+     1310 |   1322 |     186.310 |   212.000 |   13
+     1405 |   1412 |     117.800 |   117.500 |    8
+     1467 |   1474 |      70.400 |    71.830 |    8
+     1494 |   1502 |      79.760 |    86.000 |    9
+     1600 |   1618 |      90.440 |    77.050 |   19
+(26 rows)
+
+-- W-bottom: decline, bounce, re-decline, recovery
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (DECLINE{3,} BOUNCE{3,} DIP{3,} RECOVER{3,})
+        DEFINE
+            DECLINE AS price < PREV(price),
+            BOUNCE AS price > PREV(price),
+            DIP AS price < PREV(price),
+            RECOVER AS price > PREV(price)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_price | end_price | days 
+----------+--------+-------------+-----------+------
+      136 |    153 |     444.000 |   434.250 |   18
+      456 |    469 |      64.500 |    65.125 |   14
+      520 |    534 |     115.250 |   115.750 |   15
+      610 |    623 |     107.125 |   109.000 |   14
+      791 |    802 |     113.500 |   118.250 |   12
+      942 |    953 |      91.250 |    89.875 |   12
+     1188 |   1206 |     105.250 |   100.562 |   19
+     1560 |   1574 |      87.420 |    90.000 |   15
+(8 rows)
+
+-- Volume surge streak: 6+ consecutive days of increasing volume
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(volume) OVER w AS start_vol,
+           last_value(volume) OVER w AS end_vol,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (INIT SURGE{5,})
+        DEFINE
+            SURGE AS volume > PREV(volume)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_vol | end_vol  | days 
+----------+--------+-----------+----------+------
+      186 |    191 |     25100 |    35300 |    6
+      291 |    296 |     52200 |   188300 |    6
+      408 |    413 |    163300 |   610300 |    6
+      439 |    444 |    438400 |  1089200 |    6
+      500 |    506 |    373700 |  1114200 |    7
+      551 |    558 |    691100 |  2097500 |    8
+      635 |    640 |    418300 |  1388100 |    6
+      783 |    788 |    769000 |  1564900 |    6
+      824 |    830 |    682200 |  2418700 |    7
+      968 |    974 |    993100 |  2341200 |    7
+     1072 |   1077 |   1257200 |  2327700 |    6
+     1078 |   1084 |   1748300 |  5514300 |    7
+     1093 |   1098 |   2664400 | 13145900 |    6
+     1334 |   1339 |   3220900 | 10552600 |    6
+     1524 |   1530 |   3562400 |  7012100 |    7
+     1533 |   1538 |   3548500 |  8560800 |    6
+     1575 |   1580 |   4226500 |  6952100 |    6
+(17 rows)
+
+-- Volatility squeeze: consecutive narrowing of daily price range
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(high - low) OVER w AS start_range,
+           last_value(high - low) OVER w AS end_range,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (INIT NARROW{5,})
+        DEFINE
+            NARROW AS (high - low) < PREV(high) - PREV(low)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_range | end_range | days 
+----------+--------+-------------+-----------+------
+      128 |    133 |      11.000 |     3.000 |    6
+      170 |    175 |      10.250 |     2.750 |    6
+      194 |    201 |      10.000 |     1.625 |    8
+      283 |    288 |       7.000 |     2.375 |    6
+      320 |    325 |       4.750 |     2.500 |    6
+      578 |    583 |       2.750 |     1.125 |    6
+      725 |    731 |       3.370 |     1.125 |    7
+      775 |    780 |       4.500 |     0.875 |    6
+      913 |    918 |       2.870 |     1.250 |    6
+     1130 |   1135 |       3.000 |     1.125 |    6
+     1348 |   1353 |       6.620 |     1.562 |    6
+(11 rows)
+
+-- Gap up: open significantly higher than previous close (5%+)
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS gap_rn,
+           first_value(price) OVER w AS prev_close,
+           last_value(open) OVER w AS gap_open,
+           count(*) OVER w AS cnt
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (PREV_DAY GAP_UP)
+        DEFINE
+            GAP_UP AS open > PREV(price) * 1.05
+    )
+) t WHERE cnt > 0 ORDER BY gap_rn;
+ gap_rn | prev_close | gap_open | cnt 
+--------+------------+----------+-----
+    986 |     48.875 |   52.875 |   2
+    994 |     42.375 |   44.880 |   2
+   1029 |     52.250 |   55.000 |   2
+   1039 |     55.880 |   59.000 |   2
+   1177 |    142.380 |  150.500 |   2
+   1238 |    122.000 |  128.250 |   2
+   1318 |    171.875 |  197.250 |   2
+   1383 |    103.500 |  112.000 |   2
+   1392 |     96.687 |  104.375 |   2
+   1401 |    106.500 |  112.000 |   2
+   1464 |     66.400 |   70.370 |   2
+   1477 |     57.580 |   62.000 |   2
+   1479 |     63.420 |   67.750 |   2
+   1481 |     64.900 |   72.800 |   2
+   1517 |     90.310 |   95.070 |   2
+(15 rows)
+
+-- Price-volume divergence: price rising while volume declining (bearish signal)
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (INIT DIVERGE{3,})
+        DEFINE
+            DIVERGE AS price > PREV(price) AND volume < PREV(volume)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_price | end_price | days 
+----------+--------+-------------+-----------+------
+       10 |     13 |     338.500 |   349.750 |    4
+       47 |     50 |     352.000 |   358.000 |    4
+       53 |     56 |     314.000 |   319.000 |    4
+       63 |     66 |     294.500 |   301.750 |    4
+       99 |    102 |     401.000 |   404.000 |    4
+      177 |    180 |     248.500 |   261.750 |    4
+      209 |    212 |     152.000 |   166.500 |    4
+      237 |    240 |     207.500 |   215.500 |    4
+      274 |    277 |     259.500 |   264.500 |    4
+      347 |    350 |     306.750 |   312.000 |    4
+      531 |    534 |     111.875 |   115.750 |    4
+      545 |    548 |     118.000 |   121.000 |    4
+      561 |    564 |     131.130 |   134.250 |    4
+      586 |    589 |     110.250 |   112.000 |    4
+      645 |    648 |     129.630 |   133.250 |    4
+      656 |    660 |     124.500 |   128.125 |    5
+      679 |    682 |     128.000 |   129.750 |    4
+      734 |    737 |     121.380 |   123.750 |    4
+      763 |    766 |     150.750 |   156.500 |    4
+      844 |    847 |     105.500 |   107.880 |    4
+      921 |    924 |      97.250 |   101.250 |    4
+      956 |    959 |      84.875 |    87.500 |    4
+      960 |    963 |      88.250 |    89.500 |    4
+      987 |    990 |      49.000 |    51.500 |    4
+     1023 |   1026 |      53.250 |    54.625 |    4
+     1033 |   1036 |      57.125 |    58.380 |    4
+     1060 |   1063 |      89.000 |    93.625 |    4
+     1066 |   1069 |      89.130 |    93.000 |    4
+     1087 |   1090 |      89.375 |    91.750 |    4
+     1165 |   1168 |     137.625 |   146.875 |    4
+     1202 |   1205 |      96.500 |    98.750 |    4
+     1209 |   1212 |      96.125 |   100.250 |    4
+     1220 |   1223 |     106.130 |   112.000 |    4
+     1231 |   1234 |     117.062 |   119.625 |    4
+     1244 |   1247 |     122.500 |   130.000 |    4
+     1304 |   1307 |     169.500 |   178.562 |    4
+     1342 |   1345 |     122.000 |   124.000 |    4
+     1417 |   1420 |     101.960 |   108.530 |    4
+     1433 |   1436 |      90.000 |    93.770 |    4
+     1437 |   1440 |      96.950 |    98.500 |    4
+     1443 |   1446 |     120.250 |   122.200 |    4
+     1449 |   1452 |      74.650 |    77.140 |    4
+     1455 |   1458 |      67.600 |    73.500 |    4
+     1497 |   1500 |      76.250 |    81.650 |    4
+     1505 |   1508 |      75.860 |    79.510 |    4
+     1511 |   1514 |      79.750 |    81.020 |    4
+     1518 |   1521 |      94.020 |    97.700 |    4
+     1541 |   1544 |      83.650 |    85.250 |    4
+     1547 |   1550 |      82.210 |    84.040 |    4
+     1585 |   1588 |      92.700 |    93.570 |    4
+     1594 |   1597 |      91.600 |    92.410 |    4
+     1613 |   1616 |      72.010 |    74.610 |    4
+(52 rows)
+
+-- Consolidation then breakout: sideways movement followed by sharp rise
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (FLAT{5,} BREAKOUT)
+        DEFINE
+            FLAT AS price BETWEEN PREV(price) * 0.98 AND PREV(price) * 1.02,
+            BREAKOUT AS price > PREV(price) * 1.05
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_price | end_price | days 
+----------+--------+-------------+-----------+------
+       69 |     81 |     296.750 |   314.000 |   13
+      214 |    225 |     171.500 |   164.125 |   12
+      371 |    395 |      71.630 |    71.250 |   25
+      416 |    424 |      54.250 |    53.875 |    9
+      484 |    494 |      75.500 |    79.000 |   11
+      865 |    892 |     112.500 |   115.750 |   28
+     1007 |   1020 |      59.500 |    58.630 |   14
+     1113 |   1118 |     110.750 |   117.625 |    6
+     1146 |   1152 |     133.750 |   145.000 |    7
+     1171 |   1178 |     137.880 |   153.625 |    8
+     1350 |   1357 |     106.125 |   112.000 |    8
+     1360 |   1370 |     109.125 |   115.625 |   11
+(12 rows)
+
+-- Dead cat bounce: decline followed by weak recovery (<1% per day)
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (DECLINE{4,} BOUNCE{3,})
+        DEFINE
+            DECLINE AS price < PREV(price),
+            BOUNCE AS price > PREV(price) AND price < PREV(price) * 1.01
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_price | end_price | days 
+----------+--------+-------------+-----------+------
+       59 |     66 |     315.000 |   301.750 |    8
+      262 |    271 |     277.625 |   275.500 |   10
+      280 |    287 |     272.000 |   261.250 |    8
+      361 |    368 |      75.375 |    74.130 |    8
+      427 |    433 |      65.130 |    65.500 |    7
+      447 |    453 |      55.500 |    54.625 |    7
+      653 |    659 |     129.375 |   126.625 |    7
+      674 |    682 |     132.375 |   129.750 |    9
+      833 |    839 |     106.500 |   107.125 |    7
+     1423 |   1430 |     108.180 |   106.200 |    8
+     1591 |   1597 |      92.920 |    92.410 |    7
+(11 rows)
+
+-- Uptrend: 7+ consecutive days of higher highs AND higher lows
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (UPTREND{7,})
+        DEFINE
+            UPTREND AS high > PREV(high) AND low > PREV(low)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_price | end_price | days 
+----------+--------+-------------+-----------+------
+      156 |    162 |     317.000 |   329.500 |    7
+      299 |    305 |     266.000 |   273.500 |    7
+      696 |    702 |     144.630 |   152.500 |    7
+      741 |    747 |     155.250 |   166.750 |    7
+      895 |    901 |     119.250 |   126.750 |    7
+     1121 |   1127 |     103.630 |   108.875 |    7
+     1211 |   1217 |      99.130 |   102.875 |    7
+     1271 |   1278 |     164.375 |   189.250 |    8
+     1621 |   1628 |      84.000 |    89.820 |    8
+(9 rows)
+
+-- Panic and snap-back: 3%+ daily drops followed by 2%+ rebound
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (PANIC{2,} SNAP)
+        DEFINE
+            PANIC AS price < PREV(price) * 0.97,
+            SNAP AS price > PREV(price) * 1.02
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_price | end_price | days 
+----------+--------+-------------+-----------+------
+       24 |     26 |     230.625 |   229.875 |    3
+      165 |    167 |     272.000 |   270.750 |    3
+      169 |    171 |     260.000 |   259.500 |    3
+      769 |    772 |     140.125 |   115.000 |    4
+      977 |    979 |      56.130 |    53.000 |    3
+      980 |    982 |      51.375 |    51.750 |    3
+     1387 |   1389 |      86.000 |    89.000 |    3
+     1396 |   1398 |      99.290 |    98.390 |    3
+(8 rows)
+
+-- Volume climax reversal: uptrend, volume spike (1.5x), then decline
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (RALLY{3,} CLIMAX SELLOFF{2,})
+        DEFINE
+            RALLY AS price > PREV(price),
+            CLIMAX AS volume > PREV(volume) * 1.5,
+            SELLOFF AS price < PREV(price)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+ start_rn | end_rn | start_price | end_price | days 
+----------+--------+-------------+-----------+------
+        2 |      7 |     368.250 |   367.750 |    6
+       16 |     21 |     349.250 |   344.500 |    6
+      105 |    111 |     388.500 |   394.500 |    7
+      228 |    234 |     164.250 |   163.250 |    7
+      243 |    248 |     223.250 |   228.500 |    6
+      251 |    259 |     251.000 |   253.500 |    9
+      352 |    358 |     310.500 |   309.000 |    7
+      398 |    405 |      65.380 |    65.000 |    8
+      466 |    472 |      64.250 |    63.875 |    7
+      586 |    595 |     110.250 |   112.250 |   10
+      626 |    632 |     125.750 |   123.250 |    7
+      700 |    707 |     150.250 |   152.750 |    8
+      714 |    722 |     136.125 |   137.000 |    9
+      740 |    750 |     154.250 |   163.000 |   11
+      805 |    811 |     116.875 |   116.000 |    7
+      814 |    821 |      99.500 |    97.130 |    8
+      850 |    857 |     107.880 |    99.250 |    8
+      922 |    927 |      99.750 |    99.875 |    6
+      934 |    939 |      92.375 |    92.880 |    6
+      998 |   1004 |      52.000 |    53.130 |    7
+     1043 |   1049 |      73.125 |    73.000 |    7
+     1052 |   1057 |      72.130 |    74.250 |    6
+     1138 |   1143 |     127.750 |   125.625 |    6
+     1155 |   1162 |     158.000 |   155.625 |    8
+     1181 |   1191 |      99.130 |   100.062 |   11
+     1192 |   1202 |     100.130 |    96.500 |   11
+     1236 |   1241 |     118.380 |   123.880 |    6
+     1272 |   1282 |     166.062 |   183.000 |   11
+     1325 |   1331 |     217.250 |   237.500 |    7
+     1409 |   1414 |     112.890 |   116.100 |    6
+     1456 |   1461 |      68.580 |    68.760 |    6
+(31 rows)
+
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 8dbd3f9036a..b308f8d7cb4 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1,12 +1,29 @@
 --
 -- Test for row pattern recognition: WINDOW clause integration and
--- real-world scenario tests using stock price data.
+-- scenario tests using synthetic stock data.
 --
 -- Parser/planner tests: rpr_base.sql
 -- NFA engine tests: rpr_nfa.sql
 -- EXPLAIN statistics tests: rpr_explain.sql
 --
 
+\getenv abs_srcdir PG_ABS_SRCDIR
+
+-- Synthetic stock data for RPR pattern matching tests
+CREATE TABLE rpr_stock (
+       part_id integer,
+       rn      integer,
+       price   numeric(10,3),
+       volume  bigint,
+       open    numeric(10,3),
+       low     numeric(10,3),
+       high    numeric(10,3)
+);
+
+\set filename :abs_srcdir '/data/stock.data'
+COPY rpr_stock FROM :'filename';
+ANALYZE rpr_stock;
+
 CREATE TEMP TABLE stock (
        company TEXT,
        tdate DATE,
@@ -908,3 +925,240 @@ WINDOW w AS (
 );
 
 DROP TABLE rpr_consec_null;
+
+-- ============================================================
+-- Stock Scenario Tests (1632 rows, partitioned regions)
+-- ============================================================
+
+-- Consecutive rising days: find streaks of 7+ days
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (UP{7,})
+        DEFINE UP AS price > PREV(price)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- V-shape recovery: 4+ days decline followed by 4+ days rise
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (DECLINE{4,} RISE{4,})
+        DEFINE
+            DECLINE AS price < PREV(price),
+            RISE AS price > PREV(price)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- W-bottom: decline, bounce, re-decline, recovery
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (DECLINE{3,} BOUNCE{3,} DIP{3,} RECOVER{3,})
+        DEFINE
+            DECLINE AS price < PREV(price),
+            BOUNCE AS price > PREV(price),
+            DIP AS price < PREV(price),
+            RECOVER AS price > PREV(price)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- Volume surge streak: 6+ consecutive days of increasing volume
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(volume) OVER w AS start_vol,
+           last_value(volume) OVER w AS end_vol,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (INIT SURGE{5,})
+        DEFINE
+            SURGE AS volume > PREV(volume)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- Volatility squeeze: consecutive narrowing of daily price range
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(high - low) OVER w AS start_range,
+           last_value(high - low) OVER w AS end_range,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (INIT NARROW{5,})
+        DEFINE
+            NARROW AS (high - low) < PREV(high) - PREV(low)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- Gap up: open significantly higher than previous close (5%+)
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS gap_rn,
+           first_value(price) OVER w AS prev_close,
+           last_value(open) OVER w AS gap_open,
+           count(*) OVER w AS cnt
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (PREV_DAY GAP_UP)
+        DEFINE
+            GAP_UP AS open > PREV(price) * 1.05
+    )
+) t WHERE cnt > 0 ORDER BY gap_rn;
+
+-- Price-volume divergence: price rising while volume declining (bearish signal)
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (INIT DIVERGE{3,})
+        DEFINE
+            DIVERGE AS price > PREV(price) AND volume < PREV(volume)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- Consolidation then breakout: sideways movement followed by sharp rise
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (FLAT{5,} BREAKOUT)
+        DEFINE
+            FLAT AS price BETWEEN PREV(price) * 0.98 AND PREV(price) * 1.02,
+            BREAKOUT AS price > PREV(price) * 1.05
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- Dead cat bounce: decline followed by weak recovery (<1% per day)
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (DECLINE{4,} BOUNCE{3,})
+        DEFINE
+            DECLINE AS price < PREV(price),
+            BOUNCE AS price > PREV(price) AND price < PREV(price) * 1.01
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- Uptrend: 7+ consecutive days of higher highs AND higher lows
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (UPTREND{7,})
+        DEFINE
+            UPTREND AS high > PREV(high) AND low > PREV(low)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- Panic and snap-back: 3%+ daily drops followed by 2%+ rebound
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (PANIC{2,} SNAP)
+        DEFINE
+            PANIC AS price < PREV(price) * 0.97,
+            SNAP AS price > PREV(price) * 1.02
+    )
+) t WHERE days > 0 ORDER BY start_rn;
+
+-- Volume climax reversal: uptrend, volume spike (1.5x), then decline
+SELECT * FROM (
+    SELECT first_value(rn) OVER w AS start_rn,
+           last_value(rn) OVER w AS end_rn,
+           first_value(price) OVER w AS start_price,
+           last_value(price) OVER w AS end_price,
+           count(*) OVER w AS days
+    FROM rpr_stock
+    WINDOW w AS (
+        PARTITION BY part_id
+        ORDER BY rn
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (RALLY{3,} CLIMAX SELLOFF{2,})
+        DEFINE
+            RALLY AS price > PREV(price),
+            CLIMAX AS volume > PREV(volume) * 1.5,
+            SELLOFF AS price < PREV(price)
+    )
+) t WHERE days > 0 ORDER BY start_rn;
-- 
2.50.1 (Apple Git-155)

From d13cf490ce7585fd99a7690911a75d90a51017bb Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 4 Mar 2026 14:08:47 +0900
Subject: [PATCH 8/8] Fix zero-min reluctant quantifier to produce zero-length
 match

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4441b4d9c51..3075c51789c 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -307,24 +307,24 @@ static inline bool nfa_eval_var_match(WindowAggState *winstate,
 static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
 					  bool *varMatched);
 static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
-							  RPRNFAState *state, int64 currentPos, bool initialAdvance);
+							  RPRNFAState *state, int64 currentPos);
 static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 							  RPRNFAState *state, RPRPatternElement *nextElem,
-							  int64 currentPos, bool initialAdvance);
+							  int64 currentPos);
 static void nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 							RPRNFAState *state, RPRPatternElement *elem,
-							int64 currentPos, bool initialAdvance);
+							int64 currentPos);
 static void nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 							  RPRNFAState *state, RPRPatternElement *elem,
-							  int64 currentPos, bool initialAdvance);
+							  int64 currentPos);
 static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 							RPRNFAState *state, RPRPatternElement *elem,
-							int64 currentPos, bool initialAdvance);
+							int64 currentPos);
 static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 							RPRNFAState *state, RPRPatternElement *elem,
-							int64 currentPos, bool initialAdvance);
+							int64 currentPos);
 static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
-						int64 currentPos, bool initialAdvance);
+						int64 currentPos);
 
 /*
  * Not null info bit array consists of 2-bit items
@@ -5008,9 +5008,14 @@ register_result:
  *   handles nested groups like "((A|B)+)+" correctly - exiting the inner
  *   group counts as one iteration of the outer group.
  *
- * - initialAdvance flag: The first advance after context creation must
- *   skip FIN recording. Reaching FIN without evaluating any VAR would
- *   create a zero-length match, which is invalid.
+ * - Zero-length match handling: The initial advance uses currentPos =
+ *   startPos - 1 (before any row is consumed). If FIN is reached via
+ *   epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow,
+ *   resulting in UNMATCHED. For reluctant min=0 patterns (A*?, A??),
+ *   the skip path reaches FIN first and early termination prunes enter
+ *   paths, yielding an immediate zero-length (unmatched) result. For
+ *   greedy patterns (A*), the enter path adds VAR states first, then
+ *   the skip FIN is recorded but VAR states survive for later matching.
  *
  * Context Absorption Runtime:
  * ---------------------------
@@ -5122,7 +5127,7 @@ nfa_process_row(WindowAggState *winstate, int64 currentPos,
 		Assert(!hasLimitedFrame ||
 			   currentPos < ctx->matchStartRow + frameOffset + 1);
 
-		nfa_advance(winstate, ctx, currentPos, false);
+		nfa_advance(winstate, ctx, currentPos);
 	}
 }
 
@@ -5467,10 +5472,11 @@ nfa_start_context(WindowAggState *winstate, int64 startPos)
 	 * states for VAR elements with min=0. This prepares the context for the
 	 * first row's match phase.
 	 *
-	 * Pass initialAdvance=true to prevent recording zero-length matches when
-	 * optional patterns can skip all VARs to reach FIN immediately.
+	 * Use startPos - 1 as currentPos since no row has been consumed yet. If
+	 * FIN is reached via epsilon transitions, matchEndRow = startPos - 1
+	 * which is less than matchStartRow, resulting in UNMATCHED treatment.
 	 */
-	nfa_advance(winstate, ctx, startPos, true);
+	nfa_advance(winstate, ctx, startPos - 1);
 
 	return ctx;
 }
@@ -5711,7 +5717,7 @@ nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos)
 		if (ctx->states != NULL)
 		{
 			nfa_match(winstate, ctx, NULL);
-			nfa_advance(winstate, ctx, lastPos, false);
+			nfa_advance(winstate, ctx, lastPos);
 		}
 	}
 }
@@ -6047,7 +6053,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 static void
 nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 				  RPRNFAState *state, RPRPatternElement *nextElem,
-				  int64 currentPos, bool initialAdvance)
+				  int64 currentPos)
 {
 	if (RPRElemIsVar(nextElem))
 	{
@@ -6061,11 +6067,11 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 		nfa_add_state_unique(winstate, ctx, state);
 
 		if (skipState != NULL)
-			nfa_advance_state(winstate, ctx, skipState, currentPos, initialAdvance);
+			nfa_advance_state(winstate, ctx, skipState, currentPos);
 	}
 	else
 	{
-		nfa_advance_state(winstate, ctx, state, currentPos, initialAdvance);
+		nfa_advance_state(winstate, ctx, state, currentPos);
 	}
 }
 
@@ -6077,7 +6083,7 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 static void
 nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 				RPRNFAState *state, RPRPatternElement *elem,
-				int64 currentPos, bool initialAdvance)
+				int64 currentPos)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elements = pattern->elements;
@@ -6097,7 +6103,7 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 									state->counts, state->isAbsorbable);
 
 		/* Recursively process this branch before next */
-		nfa_advance_state(winstate, ctx, newState, currentPos, initialAdvance);
+		nfa_advance_state(winstate, ctx, newState, currentPos);
 		altIdx = altElem->jump;
 	}
 
@@ -6115,7 +6121,7 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 static void
 nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 				  RPRNFAState *state, RPRPatternElement *elem,
-				  int64 currentPos, bool initialAdvance)
+				  int64 currentPos)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elements = pattern->elements;
@@ -6136,7 +6142,7 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 
 		/* Reluctant: skip first (prefer fewer iterations), enter second */
 		nfa_route_to_elem(winstate, ctx, skipState,
-						  &elements[elem->jump], currentPos, initialAdvance);
+						  &elements[elem->jump], currentPos);
 
 		/*
 		 * If skip path reached FIN, shortest match is found. Skip group entry
@@ -6150,19 +6156,19 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 
 		state->elemIdx = elem->next;
 		nfa_route_to_elem(winstate, ctx, state,
-						  &elements[state->elemIdx], currentPos, initialAdvance);
+						  &elements[state->elemIdx], currentPos);
 	}
 	else
 	{
 		/* Greedy: enter first, skip second */
 		state->elemIdx = elem->next;
 		nfa_route_to_elem(winstate, ctx, state,
-						  &elements[state->elemIdx], currentPos, initialAdvance);
+						  &elements[state->elemIdx], currentPos);
 
 		if (skipState != NULL)
 		{
 			nfa_route_to_elem(winstate, ctx, skipState,
-							  &elements[elem->jump], currentPos, initialAdvance);
+							  &elements[elem->jump], currentPos);
 		}
 	}
 }
@@ -6176,7 +6182,7 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 static void
 nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 				RPRNFAState *state, RPRPatternElement *elem,
-				int64 currentPos, bool initialAdvance)
+				int64 currentPos)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elements = pattern->elements;
@@ -6199,7 +6205,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		state->elemIdx = elem->jump;
 		jumpElem = &elements[state->elemIdx];
 		nfa_route_to_elem(winstate, ctx, state, jumpElem,
-						  currentPos, initialAdvance);
+						  currentPos);
 
 		/*
 		 * Fast-forward fallback for nullable bodies.  E.g. (A?){2,3} when A
@@ -6223,7 +6229,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 				ffState->counts[nextElem->depth]++;
 
 			nfa_route_to_elem(winstate, ctx, ffState, nextElem,
-							  currentPos, initialAdvance);
+							  currentPos);
 		}
 	}
 	else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
@@ -6239,7 +6245,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
 			state->counts[nextElem->depth]++;
 
-		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
+		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
 	}
 	else
 	{
@@ -6277,7 +6283,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 
 			/* Exit first (preferred for reluctant) */
 			nfa_route_to_elem(winstate, ctx, exitState, nextElem,
-							  currentPos, initialAdvance);
+							  currentPos);
 
 			/*
 			 * If exit path reached FIN, shortest match is found. Skip loop to
@@ -6291,16 +6297,16 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 
 			/* Loop second */
 			nfa_route_to_elem(winstate, ctx, state, jumpElem,
-							  currentPos, initialAdvance);
+							  currentPos);
 		}
 		else
 		{
 			/* Loop first (preferred for greedy) */
 			nfa_route_to_elem(winstate, ctx, state, jumpElem,
-							  currentPos, initialAdvance);
+							  currentPos);
 			/* Exit second */
 			nfa_route_to_elem(winstate, ctx, exitState, nextElem,
-							  currentPos, initialAdvance);
+							  currentPos);
 		}
 	}
 }
@@ -6314,7 +6320,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 static void
 nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 				RPRNFAState *state, RPRPatternElement *elem,
-				int64 currentPos, bool initialAdvance)
+				int64 currentPos)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elements = pattern->elements;
@@ -6359,7 +6365,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 
 			/* Exit first (preferred for reluctant) */
 			nfa_route_to_elem(winstate, ctx, cloneState, nextElem,
-							  currentPos, initialAdvance);
+							  currentPos);
 
 			/*
 			 * If exit path reached FIN, the shortest match is found. Skip
@@ -6400,7 +6406,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			}
 
 			nfa_route_to_elem(winstate, ctx, state, nextElem,
-							  currentPos, initialAdvance);
+							  currentPos);
 		}
 	}
 	else if (canLoop)
@@ -6424,7 +6430,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 				state->counts[nextElem->depth]++;
 		}
 
-		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
+		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
 	}
 }
 
@@ -6436,7 +6442,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
  */
 static void
 nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
-				  RPRNFAState *state, int64 currentPos, bool initialAdvance)
+				  RPRNFAState *state, int64 currentPos)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elem;
@@ -6458,28 +6464,25 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
 	switch (elem->varId)
 	{
 		case RPR_VARID_FIN:
-			/* FIN: record match (skip for initial advance) */
-			if (!initialAdvance)
-				nfa_add_matched_state(winstate, ctx, state, currentPos);
-			else
-				nfa_state_free(winstate, state);
+			/* FIN: record match */
+			nfa_add_matched_state(winstate, ctx, state, currentPos);
 			break;
 
 		case RPR_VARID_ALT:
-			nfa_advance_alt(winstate, ctx, state, elem, currentPos, initialAdvance);
+			nfa_advance_alt(winstate, ctx, state, elem, currentPos);
 			break;
 
 		case RPR_VARID_BEGIN:
-			nfa_advance_begin(winstate, ctx, state, elem, currentPos, initialAdvance);
+			nfa_advance_begin(winstate, ctx, state, elem, currentPos);
 			break;
 
 		case RPR_VARID_END:
-			nfa_advance_end(winstate, ctx, state, elem, currentPos, initialAdvance);
+			nfa_advance_end(winstate, ctx, state, elem, currentPos);
 			break;
 
 		default:
 			/* VAR element */
-			nfa_advance_var(winstate, ctx, state, elem, currentPos, initialAdvance);
+			nfa_advance_var(winstate, ctx, state, elem, currentPos);
 			break;
 	}
 }
@@ -6489,13 +6492,12 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
  *
  * Advance phase (divergence): transition from all surviving states.
  * Called after match phase with matched VAR states, or at context creation
- * for initial epsilon expansion (initialAdvance=true skips FIN matches).
+ * for initial epsilon expansion (with currentPos = startPos - 1).
  *
  * Processes states in order, using recursive DFS to maintain lexical order.
  */
 static void
-nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos,
-			bool initialAdvance)
+nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 {
 	RPRNFAState *states = ctx->states;
 	RPRNFAState *state;
@@ -6515,7 +6517,7 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos,
 		states = states->next;
 		state->next = NULL;
 
-		nfa_advance_state(winstate, ctx, state, currentPos, initialAdvance);
+		nfa_advance_state(winstate, ctx, state, currentPos);
 
 		/*
 		 * Early termination: if a FIN was newly reached in this advance,
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index a7c536625f1..2fefb933c71 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1090,9 +1090,9 @@ WINDOW w AS (
 );
  count 
 -------
-     1
-     1
-     1
+     0
+     0
+     0
 (3 rows)
 
 -- Reluctant quantifier: prefer shortest match
@@ -1124,9 +1124,9 @@ WINDOW w AS (
 );
  count 
 -------
-     1
-     1
-     1
+     0
+     0
+     0
 (3 rows)
 
 -- Reluctant quantifier: prefer shortest match
@@ -1192,9 +1192,9 @@ WINDOW w AS (
 );
  count 
 -------
-     1
-     1
-     1
+     0
+     0
+     0
 (3 rows)
 
 -- Reluctant quantifier: prefer shortest match
@@ -1382,9 +1382,9 @@ WINDOW w AS (
 );
  count 
 -------
-     1
-     1
-     1
+     0
+     0
+     0
 (3 rows)
 
 -- Reluctant quantifier: prefer shortest match
@@ -1460,9 +1460,9 @@ WINDOW w AS (
 );
  count 
 -------
-     1
-     1
-     1
+     0
+     0
+     0
 (3 rows)
 
 -- Reluctant quantifier: prefer shortest match
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index ef184b7950b..d5cd80f423e 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3347,7 +3347,7 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a' b')+" c)*
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 7 peak, 178 total, 0 merged
+   NFA States: 9 peak, 178 total, 0 merged
    NFA Contexts: 4 peak, 61 total, 22 pruned
    NFA: 1 matched (len 57/57/57.0), 0 mismatched
    NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
@@ -3384,7 +3384,7 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a (b c)+)*
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 5 peak, 160 total, 0 merged
+   NFA States: 7 peak, 160 total, 0 merged
    NFA Contexts: 4 peak, 61 total, 22 pruned
    NFA: 1 matched (len 57/57/57.0), 0 mismatched
    NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
-- 
2.50.1 (Apple Git-155)

---

From: Henson Choi <[email-masked]>
Date: Sat, 7 Mar 2026 08:18:27 +0900

Hi, Tatsuo

Does "a zero-length match" mean "an empty match"?

Yes, they refer to the same thing.  "Zero-length match" is the more
common term in general regex implementations (PCRE2, Perl, Python,
Java, etc.[1]), but the RPR standard (ISO/IEC 19075-5, Section 4.12.2)
uses "empty match" exclusively.

[1] https://www.regular-expressions.info/zerolength.html

BTW, currently we place all nfa_* functions at the bottom of
> nodeWindowAgg.c.  However nodeWindowAgg.c in master branch places "API
> exposed to window functions" at the bottom of the file. Do you think
> we should follow the way?

Yes, we should follow master's convention.  I see three options:

  (a) Reorder within nodeWindowAgg.c: move the nfa_* functions up and
      keep the "API exposed to window functions" section at the bottom,
      matching master's layout.

  (b) Separate file under src/backend/executor/, keeping it close to
      nodeWindowAgg.c while making the boundary explicit.

  (c) A dedicated src/backend/rpr/ directory modeled on
      src/backend/regex/, giving the NFA engine its own namespace.
      This could also be an opportunity to consolidate the existing
      src/backend/optimizer/plan/rpr.c into the same directory.

For now (a) is the safest change.  Longer term, (b) or (c) would make
more sense -- especially when we extend to MATCH_RECOGNIZE (R010),
where the NFA engine will need to be shared across both code paths.
Either way, the NFA engine can be exposed via a header so that R010
can share it without further restructuring.

Since the NFA algorithm is not familiar territory for most DBMS
developers, it would also be worth preserving the detailed algorithm
description posted earlier in this thread -- either as structured
comments or as a dedicated README alongside the code.

What do you think?  Should we start with (a) now and revisit the
broader restructuring approaches -- (b) or (c) -- later, or would you
prefer to discuss them first?  Either of those would also resolve the
file layout convention issue naturally, since new files would follow
proper conventions from the start.

One more thing: there are no ECPG example programs or regression tests
for RPR yet.  I'd like to propose adding them.  Shall I draft an
initial set, or would you prefer to coordinate with the ECPG
maintainers first?

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Sun, 8 Mar 2026 01:02:41 +0900

Hi Tatsuo,

I found Trino uses "empty match" too [2]. So for SQL users, I guess
> "empty match" is more familiar wording.
>

Agreed.  Since RPR is a feature defined by the SQL standard, using the
standard's own terminology is the right choice: it lets users
cross-reference the specification directly.  That said, "zero-length
match" is widely used in the regex literature (PCRE2, Perl, Python,
Java, etc.), so developers researching the topic should treat the two
terms as synonymous.  In the SQL context, "empty match" is the preferred
term.

> I prefer (a) or (b) for now, at least for the first commit. The reason
> is, current nfa functions take a WindowAggState argument. If we prefer
> (c), I think we need to change some of (or most of) nfa functions so
> that they do not take the WindowAggState argument. What do you think?
>

That is a valid concern.  (c) will be necessary in the long run, but
MATCH_RECOGNIZE (R010) is not yet on the horizon, and restructuring the
NFA layer now -- before that requirement is concrete -- carries a real
risk of introducing defects during the refactoring.  The risks outweigh the
benefits at this stage.

Instead, I'll try (b): a separate file under src/backend/executor/
can retain the WindowAggState argument while making the NFA boundary
explicit, and the change is still reviewable.  If (b) turns out to be
harder than expected or surfaces unexpected issues, I'll fall back to
(a).

If (b) works out, I'll add a structured header comment to the new file
to preserve the algorithm description for future reviewers.

I am not familiar with ECPG. Do you know if ECPG has Window clause
> tests? If ECPG does not have any Window clause tests, is it worth to
> add RPR tests to ECPG?
>

I checked src/interfaces/ecpg/test/ and confirmed there are no Window
clause tests there at all.  If ECPG has no Window clause coverage, I
see no reason to add RPR tests there at this stage.  I think it would be
better to leave ECPG tests out of this patch set for now.

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Sun, 8 Mar 2026 23:24:55 +0900

Hi Tatsuo,

> Question is, should we fix optimize_window_clause as well?

Yes, we should. The comment in optimize_window_clauses explicitly
states "Perform the same duplicate check that is done in
transformWindowFuncCall", so the two checks must remain in sync.
Even if omitting the RPR fields produces no visible bug today (because
the early return guard in nocfbot-0006 already skips RPR windows), the
code would be misleading and fragile: a future change could remove
that guard and silently reintroduce the bug.

Adding the RPR fields explicitly also serves as defensive coding and
makes the intent clear to anyone reading or modifying the code later.

When incorporating nocfbot-0006 as patch 06/12, this fix was included:

    wc->rpSkipTo == existing_wc->rpSkipTo &&
    wc->initial == existing_wc->initial &&
    equal(wc->defineClause, existing_wc->defineClause) &&
    equal(wc->rpPattern, existing_wc->rpPattern)

Patch 06/12 now covers both the frame optimization guard and the
optimize_window_clauses deduplication fix, with EXPLAIN-based regression
tests added to verify the behavior.

> The two window definitions are regarded as identical and produces
> wrong result.

Thank you for finding this. Your patch has been incorporated as patch
09/12 ("Fix window deduplication for RPR windows at parse time"),
modified to add regression test cases covering the scenario. The fix
adds the missing RPR check in transformWindowFuncCall:

    equal(refwin->rpCommonSyntax, windef->rpCommonSyntax)

This prevents incorrect merging of an RPR window with a non-RPR window
that has identical frame options.

The previous submission (nocfbot-0001..0008) has been extended to
12 patches. Changes since the last submission:

  06/12  Disable frame optimization for RPR windows  [updated]
         optimize_window_clauses deduplication fix added, as discussed.

  09/12  Fix window deduplication for RPR windows at parse time  [new]
         Incorporates your fix with test cases added.

  10/12  Walk DEFINE clause in window tree traversal  [new]
         A newly discovered issue: nodeFuncs.c was not visiting the
         DEFINE clause in expression_tree_walker, query_tree_walker,
         and their mutator counterparts. The demonstrated case is SQL
         function inlining: a SQL function with a parameter used in
         DEFINE (e.g., DEFINE A AS v > $1) would fail to substitute
         the actual argument, producing wrong results.

  11/12  Use SQL standard term "empty match" in RPR comments  [new]
         Align NFA comment terminology with the SQL standard: replace
         implementation-specific terms "zero-length match" and
         "zero-consumption" with the standard term "empty match".

  12/12  Extract RPR NFA engine into execRPR.c  [new]
         Following the earlier discussion about file layout, this
         implements option (b): move the NFA engine to a dedicated
         src/backend/executor/execRPR.c, keeping it close to
         nodeWindowAgg.c while making the boundary explicit. Functions
         still take WindowAggState as an argument (no signature change).
         The public interface is declared in execRPR.h using the ExecRPR*
         naming convention, consistent with other executor headers.
         The NFA algorithm guide shared earlier in this thread has also
         been incorporated as structured comments at the top of the file,
         as suggested.

The full updated series (12 patches on top of v44) is attached.

Regards,
Henson
From f4abb464c187ef6d1334d856db881d40069531da Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 15:49:54 +0900
Subject: [PATCH 01/12] Fix elog message to use lowercase per PostgreSQL
 convention

---
 src/backend/executor/nodeWindowAgg.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

 		default:

From 2bc7f29dcfceefc83fd787eb138c96247f565360 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 15:50:48 +0900
Subject: [PATCH 02/12] Fix RPR reluctant quantifier flag lost during VIEW
 serialization

---
 src/backend/optimizer/plan/rpr.c  | 36 +++++++++++++++++--------------
 src/backend/parser/gram.y         | 18 ++++++++++------
 src/backend/utils/adt/ruleutils.c |  2 +-
 src/include/nodes/parsenodes.h    |  4 ++--
 4 files changed, 35 insertions(+), 25 deletions(-)

 

 				prev->min += child->min;

 
 				prev->min += child->min;

 

 

 
 		/*
 
 	n->min = min;
 	n->max = max;

From 350f3234f318059826a2b2bd96e91e4d7fa22253 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 17:06:59 +0900
Subject: [PATCH 03/12] Expand RPR test coverage and improve test comments

---
 src/test/regress/expected/rpr.out         | 2920 +++------------------
 src/test/regress/expected/rpr_base.out    |  354 ++-
 src/test/regress/expected/rpr_explain.out |  177 +-
 src/test/regress/expected/rpr_nfa.out     |  679 ++++-
 src/test/regress/sql/rpr.sql              | 1703 ++----------
 src/test/regress/sql/rpr_base.sql         |  183 +-
 src/test/regress/sql/rpr_explain.sql      |   69 +-
 src/test/regress/sql/rpr_nfa.sql          |  512 +++-
 8 files changed, 2415 insertions(+), 4182 deletions(-)

+--

 

+--

 

---

 

 
 DROP VIEW rpr_serial_v8;
-DROP TABLE rpr_serial;

 

 

 DROP TABLE rpr_plan;
 
 DROP VIEW rpr_v;
 
 DROP VIEW rpr_v;
 
 DROP VIEW rpr_v;
 
 DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
+DROP VIEW rpr_v;

 

 

 

 
 SELECT * FROM stock;
 
+--

 
--- match everything
 
+-- match everything

+--

 

 

 

 

 
 
 DROP VIEW rpr_serial_v8;
 
-DROP TABLE rpr_serial;

 

 

 
 -- Semantic errors

 

 

 
--- RPR with GROUP BY
 
 SELECT category,
        COUNT(*) as group_cnt,
 
--- RPR with HAVING
 
 SELECT category,
        COUNT(*) as group_cnt,
 
 DROP TABLE rpr_plan;
-

 

 

 

 

 

 

From f9c80e37bbc493737ed04d237abdd4115d5a412e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 19:32:24 +0900
Subject: [PATCH 04/12] Keep RPR test objects for pg_upgrade/pg_dump testing

---
 src/test/regress/expected/rpr_base.out    |  18 +-
 src/test/regress/expected/rpr_explain.out | 504 +++++++++-------------
 src/test/regress/sql/rpr_base.sql         |  29 +-
 src/test/regress/sql/rpr_explain.sql      | 484 +++++++++------------
 4 files changed, 410 insertions(+), 625 deletions(-)

 

 

 

 

 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 -- Very long matches
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 -- Single row
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 -- Multiple alternations
 
-DROP VIEW rpr_v;
 -- Optional elements
 
-DROP VIEW rpr_v;
 -- Bounded quantifiers
 
-DROP VIEW rpr_v;
 -- Star quantifier
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 -- Exact count {n}
 
-DROP VIEW rpr_v;
 -- Range {n,m}
 
-DROP VIEW rpr_v;
 -- At least {n,}
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 -- Alternation at start
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 -- Alternation at end
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 -- Nested groups
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 -- Using PREV function
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
 
-DROP VIEW rpr_v;
--- Cleanup
-DROP TABLE nfa_test;
-DROP TABLE nfa_complex;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
 -- Very long matches

 

 

 

 -- XML format output

 

 -- Empty result set
 
 -- Single row

 
 
 -- Nested groups
 
 -- Multiple alternations
 
 -- Optional elements
 
 -- Bounded quantifiers
 
 -- Star quantifier

 

 

 
 
 -- With INITIAL keyword

 
 -- Plus quantifier

 
 -- Exact count {n}
 
 -- Range {n,m}
 
 -- At least {n,}

 

 
 -- Simple alternation

 
 
 -- Alternation at start

 
 -- Alternation at end

 
 
 -- Simple group

 -- Nested groups

 

 
 -- count(*) with pattern

 

 
 -- Using PREV function

 

 
--- Cleanup
-DROP TABLE nfa_test;
-DROP TABLE nfa_complex;
-- 
2.50.1 (Apple Git-155)

From 81a9b83b9d61704e85d97bc79b10ac6a00d73def Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 4 Mar 2026 15:53:33 +0900
Subject: [PATCH 05/12] Disable run condition pushdown for RPR windows

---
 src/backend/optimizer/path/allpaths.c  | 11 +++++++++++
 src/test/regress/expected/rpr_base.out |  4 +++-
 2 files changed, 14 insertions(+), 1 deletion(-)

+	/*
 
 -- Nested subqueries
 SELECT *
-- 
2.50.1 (Apple Git-155)

From 458224afa9df8438413b2f37f87175581f82aeab Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 4 Mar 2026 15:59:45 +0900
Subject: [PATCH 06/12] Disable frame optimization for RPR windows

---
 src/backend/optimizer/plan/planner.c      |  14 ++-
 src/test/regress/expected/rpr_explain.out | 145 ++++++++++++++++++++++
 src/test/regress/sql/rpr_explain.sql      |  98 +++++++++++++++
 3 files changed, 256 insertions(+), 1 deletion(-)

index 04faf919033..842c2aa2b94 100644
 
+		/*
+		 * If a DEFINE clause exists, do not let support functions replace the
+		 * frame with a non-RPR-compatible one.  RPR windows require ROWS
+		 * BETWEEN CURRENT ROW AND ...
+		 */
+		if (wc->defineClause != NIL)
+			continue;
+
 		foreach(lc2, wflists->windowFuncs[wc->winref])
 		{
 			SupportRequestOptimizeWindowClause req;
@@ -5990,7 +5998,11 @@ optimize_window_clauses(PlannerInfo *root, WindowFuncLists *wflists)
 					equal(wc->orderClause, existing_wc->orderClause) &&
 					wc->frameOptions == existing_wc->frameOptions &&
 					equal(wc->startOffset, existing_wc->startOffset) &&
-					equal(wc->endOffset, existing_wc->endOffset))
+					equal(wc->endOffset, existing_wc->endOffset) &&
+					wc->rpSkipTo == existing_wc->rpSkipTo &&
+					wc->initial == existing_wc->initial &&
+					equal(wc->defineClause, existing_wc->defineClause) &&
+					equal(wc->rpPattern, existing_wc->rpPattern))
 				{
 					ListCell   *lc4;
 
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 3c70a12874a..e07ad57f300 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3821,3 +3821,148 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
+--
+-- Planner optimization: optimize_window_clauses must not alter RPR frame
+--
+-- optimize_window_clauses() replaces frame options via prosupport functions.
+-- Affected functions: row_number, rank, dense_rank, percent_rank, cume_dist,
+-- ntile.  All would change the frame to ROWS UNBOUNDED PRECEDING, breaking
+-- RPR's required ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING.
+-- Test with row_number() as representative case.
+--
+-- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
+CREATE VIEW rpr_ev87 AS
+SELECT row_number() OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ORDER BY v
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+);
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Subquery Scan on rpr_ev87
+   ->  WindowAgg
+         Window: w AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING)
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(6 rows)
+
+-- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+CREATE VIEW rpr_ev88 AS
+SELECT row_number() OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ORDER BY v
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        B AS v > PREV(v)
+);
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Subquery Scan on rpr_ev88
+   ->  WindowAgg
+         Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(7 rows)
+
+--
+-- Planner optimization: non-RPR and RPR windows that share the same base frame
+-- after frame optimization are kept as separate WindowAgg nodes.
+--
+CREATE VIEW rpr_ev89 AS
+SELECT
+    row_number() OVER w_normal AS rn_normal,
+    row_number() OVER w_rpr AS rn_rpr
+FROM generate_series(1, 5) AS s(v)
+WINDOW
+    w_normal AS (ORDER BY v RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),
+    w_rpr AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS v > 1
+    );
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev89;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Subquery Scan on rpr_ev89
+   ->  WindowAgg
+         Window: w_rpr AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a+"
+         ->  WindowAgg
+               Window: w_normal AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING)
+               ->  Sort
+                     Sort Key: s.v
+                     ->  Function Scan on generate_series s
+(9 rows)
+
+--
+-- Planner optimization: find_window_run_conditions must not push down
+-- RPR window function results as Run Conditions.
+--
+-- 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:
+--   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.
+--
+-- Without RPR: count(*) > 0 is pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM generate_series(1, 10) AS s(v)
+    WINDOW w AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    )
+) t WHERE cnt > 0;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Subquery Scan on t
+   ->  WindowAgg
+         Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Run Condition: (count(*) OVER w > 0)
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(7 rows)
+
+-- With RPR: count(*) > 0 must not be pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM generate_series(1, 10) AS s(v)
+    WINDOW w AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP PAST LAST ROW
+        PATTERN (A B+)
+        DEFINE
+            B AS v > PREV(v)
+    )
+) t WHERE cnt > 0;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Subquery Scan on t
+   Filter: (t.cnt > 0)
+   ->  WindowAgg
+         Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(8 rows)
+
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 8e22382a68e..93e06b0cbdf 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2196,3 +2196,101 @@ WINDOW w AS (
         E AS v % 100 = 5
 );');
 
+--
+-- Planner optimization: non-RPR and RPR windows that share the same base frame
+-- after frame optimization are kept as separate WindowAgg nodes.
+CREATE VIEW rpr_ev89 AS
+SELECT
+    row_number() OVER w_normal AS rn_normal,
+    row_number() OVER w_rpr AS rn_rpr
+FROM generate_series(1, 5) AS s(v)
+WINDOW
+    w_normal AS (ORDER BY v RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),
+    w_rpr AS (
+        DEFINE A AS v > 1
+    );
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev89;

From ba063cdab2a824cb1f0312bc608b8896f27344bf Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 3 Mar 2026 22:17:29 +0900
Subject: [PATCH 07/12] Add stock scenario tests for RPR pattern matching

---
 src/test/regress/data/stock.data  | 1632 +++++++++++++++++++++++++++++
 src/test/regress/expected/rpr.out |  517 ++++++++-
 src/test/regress/sql/rpr.sql      |  256 ++++-
 3 files changed, 2403 insertions(+), 2 deletions(-)
 create mode 100644 src/test/regress/data/stock.data

 

From 5fc0fe9c5a36fa7f7dd84f21349a056f0f5bc15a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 4 Mar 2026 14:08:47 +0900
Subject: [PATCH 08/12] Fix zero-min reluctant quantifier to produce
 zero-length match

---
 src/backend/executor/nodeWindowAgg.c      | 104 +++++++++++-----------
 src/test/regress/expected/rpr_base.out    |  30 +++----
 src/test/regress/expected/rpr_explain.out |   4 +-
 3 files changed, 70 insertions(+), 68 deletions(-)

 /*

 
 	return ctx;
 }
 
 		if (skipState != NULL)

 

 		/*

 		if (skipState != NULL)
 		{
 
 		/*

 
 
 			/*
 
 			/* Loop second */

 			/*

 
 
 		case RPR_VARID_ALT:
 
 		case RPR_VARID_BEGIN:
 
 		case RPR_VARID_END:
 
 		default:
 			/* VAR element */

 		/*

 

index e07ad57f300..bd345906133 100644

From d4c87cb2d5b7f35223dc3bfa9d71809c9fcb135e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 8 Mar 2026 18:30:21 +0900
Subject: [PATCH 09/12] Fix window deduplication for RPR windows at parse time

---
 src/backend/parser/parse_agg.c         |  3 ++-
 src/test/regress/expected/rpr_base.out | 26 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 20 ++++++++++++++++++++
 3 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 81e1d70ffc1..5e942075444 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -1110,7 +1110,8 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 				equal(refwin->orderClause, windef->orderClause) &&
 				refwin->frameOptions == windef->frameOptions &&
 				equal(refwin->startOffset, windef->startOffset) &&
-				equal(refwin->endOffset, windef->endOffset))
+				equal(refwin->endOffset, windef->endOffset) &&
+				equal(refwin->rpCommonSyntax, windef->rpCommonSyntax))
 			{
 				/* found a duplicate window specification */
 				wfunc->winref = winref;
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 2fefb933c71..09d079b05c6 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -14,6 +14,7 @@
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
 --   Error Cases Tests
+--   Window Deduplication Tests
 --
 -- Planner Layer:
 --   Pattern Optimization Tests
@@ -2968,6 +2969,31 @@ ORDER BY id;
 (3 rows)
 
 DROP TABLE rpr_null;
+-- ============================================================
+-- Window Deduplication Tests
+-- ============================================================
+-- non-RPR and RPR windows with identical base frame are kept separate.
+SELECT id, val,
+    first_value(id) OVER (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    ) AS fv_normal,
+    first_value(id) OVER w1 AS fv_rpr
+FROM (VALUES (1, 10), (2, 20), (3, 30), (4, 40)) AS t(id, val)
+WINDOW w1 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 10
+);
+ id | val | fv_normal | fv_rpr 
+----+-----+-----------+--------
+  1 |  10 |         1 |       
+  2 |  20 |         2 |      2
+  3 |  30 |         3 |       
+  4 |  40 |         4 |       
+(4 rows)
+
 -- ============================================================
 -- Pattern Optimization Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 453860c1499..95cfb4b86fe 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -14,6 +14,7 @@
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
 --   Error Cases Tests
+--   Window Deduplication Tests
 --
 -- Planner Layer:
 --   Pattern Optimization Tests
@@ -2080,6 +2081,25 @@ ORDER BY id;
 
 DROP TABLE rpr_null;
 
+-- ============================================================
+-- Window Deduplication Tests
+-- ============================================================
+
+-- non-RPR and RPR windows with identical base frame are kept separate.
+SELECT id, val,
+    first_value(id) OVER (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    ) AS fv_normal,
+    first_value(id) OVER w1 AS fv_rpr
+FROM (VALUES (1, 10), (2, 20), (3, 30), (4, 40)) AS t(id, val)
+WINDOW w1 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 10
+);
+
 -- ============================================================
 -- Pattern Optimization Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)

From e3d0ecd07299c07cca83e1d1a75e72af4f789c0f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 8 Mar 2026 18:30:29 +0900
Subject: [PATCH 10/12] Walk DEFINE clause in window tree traversal

---
 src/backend/nodes/nodeFuncs.c          |  6 ++++++
 src/test/regress/expected/rpr_base.out | 29 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 24 +++++++++++++++++++++
 3 files changed, 59 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 199ed27995f..ddf6812d7b0 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2411,6 +2411,8 @@ expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(wc->endOffset))
 					return true;
+				if (WALK(wc->defineClause))
+					return true;
 			}
 			break;
 		case T_CTECycleClause:
@@ -2763,6 +2765,8 @@ query_tree_walker_impl(Query *query,
 				return true;
 			if (WALK(wc->endOffset))
 				return true;
+			if (WALK(wc->defineClause))
+				return true;
 		}
 	}
 
@@ -3499,6 +3503,7 @@ expression_tree_mutator_impl(Node *node,
 				MUTATE(newnode->orderClause, wc->orderClause, List *);
 				MUTATE(newnode->startOffset, wc->startOffset, Node *);
 				MUTATE(newnode->endOffset, wc->endOffset, Node *);
+				MUTATE(newnode->defineClause, wc->defineClause, List *);
 				return (Node *) newnode;
 			}
 			break;
@@ -3831,6 +3836,7 @@ query_tree_mutator_impl(Query *query,
 			FLATCOPY(newnode, wc, WindowClause);
 			MUTATE(newnode->startOffset, wc->startOffset, Node *);
 			MUTATE(newnode->endOffset, wc->endOffset, Node *);
+			MUTATE(newnode->defineClause, wc->defineClause, List *);
 
 			resultlist = lappend(resultlist, (Node *) newnode);
 		}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 09d079b05c6..ffd0f8ed9f1 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -28,6 +28,7 @@
 --   Complex Expression Tests
 --   Set Operations Tests
 --   Sorting and Grouping Tests
+--   SQL Function Inlining Tests
 --   Stress Tests
 --   Error Limit Tests
 --
@@ -5453,6 +5454,34 @@ LIMIT 3 OFFSET 1;
 (3 rows)
 
 DROP TABLE rpr_sort;
+-- SQL function inlining: $1 in DEFINE must be substituted by
+-- substitute_actual_parameters_in_from via query_tree_mutator.
+CREATE TABLE rpr_srf_t (v int);
+INSERT INTO rpr_srf_t SELECT generate_series(1, 5);
+CREATE FUNCTION rpr_srf_f(threshold int)
+RETURNS TABLE (v int, cnt bigint)
+LANGUAGE sql STABLE AS $$
+    SELECT v::int, count(*) OVER w
+    FROM rpr_srf_t
+    WINDOW w AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS v > $1
+    )
+$$;
+SELECT v, cnt FROM rpr_srf_f(3) ORDER BY v;
+ v | cnt 
+---+-----
+ 1 |   0
+ 2 |   0
+ 3 |   0
+ 4 |   2
+ 5 |   0
+(5 rows)
+
+DROP TABLE rpr_srf_t;
+DROP FUNCTION rpr_srf_f(int);
 DROP TABLE rpr_planner;
 -- ============================================================
 -- Stress Tests
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 95cfb4b86fe..3d6ca2bc2a2 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -28,6 +28,7 @@
 --   Complex Expression Tests
 --   Set Operations Tests
 --   Sorting and Grouping Tests
+--   SQL Function Inlining Tests
 --   Stress Tests
 --   Error Limit Tests
 --
@@ -3478,6 +3479,29 @@ LIMIT 3 OFFSET 1;
 
 DROP TABLE rpr_sort;
 
+-- SQL function inlining: $1 in DEFINE must be substituted by
+-- substitute_actual_parameters_in_from via query_tree_mutator.
+CREATE TABLE rpr_srf_t (v int);
+INSERT INTO rpr_srf_t SELECT generate_series(1, 5);
+
+CREATE FUNCTION rpr_srf_f(threshold int)
+RETURNS TABLE (v int, cnt bigint)
+LANGUAGE sql STABLE AS $$
+    SELECT v::int, count(*) OVER w
+    FROM rpr_srf_t
+    WINDOW w AS (
+        ORDER BY v
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS v > $1
+    )
+$$;
+
+SELECT v, cnt FROM rpr_srf_f(3) ORDER BY v;
+
+DROP TABLE rpr_srf_t;
+DROP FUNCTION rpr_srf_f(int);
+
 DROP TABLE rpr_planner;

From 352b918acb2dc10bd55eaf0471f1f5c60b8a1791 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 8 Mar 2026 19:17:31 +0900
Subject: [PATCH 11/12] Use SQL standard term "empty match" in RPR comments

---
 src/backend/executor/nodeWindowAgg.c  | 4 ++--
 src/test/regress/expected/rpr_nfa.out | 4 ++--
 src/test/regress/sql/rpr_nfa.sql      | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 3075c51789c..76c1a6bebef 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -5008,12 +5008,12 @@ register_result:
  *   handles nested groups like "((A|B)+)+" correctly - exiting the inner
  *   group counts as one iteration of the outer group.
  *
- * - Zero-length match handling: The initial advance uses currentPos =
+ * - Empty match handling: The initial advance uses currentPos =
  *   startPos - 1 (before any row is consumed). If FIN is reached via
  *   epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow,
  *   resulting in UNMATCHED. For reluctant min=0 patterns (A*?, A??),
  *   the skip path reaches FIN first and early termination prunes enter
- *   paths, yielding an immediate zero-length (unmatched) result. For
+ *   paths, yielding an immediate empty (unmatched) result. For
  *   greedy patterns (A*), the enter path adds VAR states first, then
  *   the skip FIN is recorded but VAR states survive for later matching.
  *
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index c8464e8fcf4..7b5a17fb671 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -4239,8 +4239,8 @@ WINDOW w AS (
 (6 rows)
 
 -- (A? B?){2,3}: pure empty body (nothing matches)
--- XXX: All NULL: same issue as test_728_min2 (initialAdvance blocks FIN
--- for zero-length matches at context start)
+-- XXX: All NULL: same issue as test_728_min2 (empty match at context
+-- start yields UNMATCHED via startPos-1 initial advance)
 WITH test_728_multi_empty AS (
     SELECT * FROM (VALUES
         (1, ARRAY['C']),
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index c0f4dfa1248..5edcb3357e6 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -3197,8 +3197,8 @@ WINDOW w AS (
 );
 
 -- (A? B?){2,3}: pure empty body (nothing matches)
--- XXX: All NULL: same issue as test_728_min2 (initialAdvance blocks FIN
--- for zero-length matches at context start)
+-- XXX: All NULL: same issue as test_728_min2 (empty match at context
+-- start yields UNMATCHED via startPos-1 initial advance)
 WITH test_728_multi_empty AS (
     SELECT * FROM (VALUES
         (1, ARRAY['C']),
-- 
2.50.1 (Apple Git-155)

From f4c54f983b502626fe15e7ed91535f8d9e5756b2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 8 Mar 2026 22:30:20 +0900
Subject: [PATCH 12/12] Extract RPR NFA engine into execRPR.c

---
 src/backend/executor/Makefile        |    1 +
 src/backend/executor/execRPR.c       | 3037 ++++++++++++++++++++++++++
 src/backend/executor/meson.build     |    1 +
 src/backend/executor/nodeWindowAgg.c | 2921 ++++++-------------------
 src/backend/optimizer/plan/rpr.c     |    2 +-
 src/include/executor/execRPR.h       |   40 +
 6 files changed, 3755 insertions(+), 2247 deletions(-)
 create mode 100644 src/backend/executor/execRPR.c
 create mode 100644 src/include/executor/execRPR.h

diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index 11118d0ce02..eeed9a904e5 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -26,6 +26,7 @@ OBJS = \
 	execPartition.o \
 	execProcnode.o \
 	execReplication.o \
+	execRPR.o \
 	execSRF.o \
 	execScan.o \
 	execTuples.o \
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
new file mode 100644
index 00000000000..0d5ba7516e9
--- /dev/null
+++ b/src/backend/executor/execRPR.c
@@ -0,0 +1,3037 @@
+/*-------------------------------------------------------------------------
+ *
+ * execRPR.c
+ *	  NFA-based Row Pattern Recognition engine for window functions.
+ *
+ * This file implements the NFA execution engine for the ROWS BETWEEN
+ * PATTERN clause (SQL Standard Feature R020: Row Pattern Recognition in
+ * Window Functions).
+ *
+ * The engine executes the compiled RPRPattern structure directly, avoiding
+ * regex compilation overhead.  It is called by nodeWindowAgg.c and exposes
+ * the interface declared in executor/execRPR.h.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/execRPR.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "executor/execRPR.h"
+#include "executor/executor.h"
+#include "optimizer/rpr.h"
+#include "utils/memutils.h"
+
+/*
+ * ============================================================================
+ *   PostgreSQL Row Pattern Recognition: Flat-Array Stream NFA Guide
+ * ============================================================================
+ *
+ *   Target audience: Developers with a basic understanding of the PostgreSQL
+ *                    executor and planner architecture
+ *
+ *   Scope: The entire process from PATTERN/DEFINE clause parsing to NFA
+ *          runtime execution
+ *
+ *   Related code:
+ *     - src/backend/parser/parse_rpr.c          (parser phase)
+ *     - src/backend/optimizer/plan/rpr.c        (optimizer phase)
+ *     - src/backend/executor/nodeWindowAgg.c    (executor phase, window agg)
+ *     - src/backend/executor/execRPR.c          (executor phase, NFA engine)
+ *     - src/include/executor/execRPR.h          (NFA public API)
+ *     - src/include/nodes/plannodes.h           (plan node definitions)
+ *     - src/include/nodes/execnodes.h           (execution state definitions)
+ *     - src/include/optimizer/rpr.h             (types and constants)
+ *
+ * ============================================================================
+ *
+ * What is a Flat-Array Stream NFA?
+ *
+ *   The NFA in this implementation is not a traditional state-transition graph
+ *   but a flat array of fixed-size 16-byte elements. At runtime, it processes
+ *   the row stream in a forward-only manner, expanding epsilon transitions
+ *   eagerly without backtracking.
+ *
+ *   - Flat-Array: Pattern compiled into a flat array,
+ *                 not a graph (Chapter IV)
+ *   - Stream:     Rows consumed sequentially in one direction,
+ *                 never revisited (Chapter XII)
+ *   - NFA:        Nondeterministic execution where multiple states
+ *                 coexist within a single context (Chapter VI)
+ *
+ * Chapter I  Row Pattern Recognition Overview
+ * ============================================================================
+ *
+ * Row Pattern Recognition (hereafter RPR) is a feature introduced in SQL:2016
+ * that matches regex-based patterns against ordered row sets.
+ *
+ * The SQL standard defines two forms:
+ *
+ *   Feature R010: MATCH_RECOGNIZE (FROM clause)
+ *     - Dedicated table operator
+ *     - Provides dedicated functions such as MATCH_NUMBER(), CLASSIFIER()
+ *     - Supports ONE ROW PER MATCH / ALL ROWS PER MATCH
+ *
+ *   Feature R020: RPR in a window (WINDOW clause)
+ *     - Integrated into the existing window function framework
+ *     - Supports ALL ROWS PER MATCH only
+ *     - No MATCH_NUMBER()
+ *
+ * This implementation targets Feature R020.
+ *
+ * The basic syntax is as follows:
+ *
+ *   SELECT ...
+ *   OVER (
+ *     PARTITION BY ...
+ *     ORDER BY ...
+ *     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ *     [INITIAL | SEEK]   -- SEEK is defined in the standard but not implemented
+ *     AFTER MATCH SKIP TO NEXT ROW | SKIP PAST LAST ROW
+ *     PATTERN ( <regex> )
+ *     DEFINE <variable> AS <condition>, ...
+ *   )
+ *
+ * The PATTERN clause is a regular expression over row pattern variables.
+ * The DEFINE clause specifies boolean conditions that determine whether each
+ * variable evaluates to true for the current row.
+ *
+ * Example:
+ *
+ *   PATTERN (A+ B)
+ *   DEFINE A AS price > PREV(price),
+ *          B AS price < PREV(price)
+ *
+ * This pattern matches "a span where prices rise consecutively then drop."
+ *
+ * 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                 |
+ *   +------------------------------------------------------------+
+ *
+ * Each phase uses independent data structures, and the interfaces between
+ * phases are well-defined:
+ *
+ *   Parser -> Planner:    WindowClause.rpPattern (RPRPatternNode tree)
+ *                         WindowClause.defineClause (TargetEntry list)
+ *
+ *   Planner -> Executor:  WindowAgg.rpPattern (RPRPattern struct)
+ *                         WindowAgg.defineClause (TargetEntry list)
+ *
+ * Chapter III  Parsing Phase
+ * ============================================================================
+ *
+ * III-1. Entry Point
+ *
+ *   transformWindowDefinitions() (parse_clause.c)
+ *     +-- transformRPR() (parse_rpr.c)
+ *
+ * transformRPR() is invoked when RPCommonSyntax is present and performs the
+ * following:
+ *
+ *   (1) Frame option validation
+ *       - Only ROWS is allowed (RANGE, GROUPS are not)
+ *       - The start boundary must be CURRENT ROW
+ *       - EXCLUDE option is not allowed
+ *
+ *   (2) Transcription to WindowClause
+ *       - Copies rpPattern, rpSkipTo, initial fields
+ *
+ *   (3) DEFINE clause transformation (transformDefineClause)
+ *
+ * III-2. PATTERN AST
+ *
+ * The parser transforms the PATTERN clause into an RPRPatternNode tree.
+ * Each node has one of the following four types:
+ *
+ *   RPR_PATTERN_VAR    Variable reference. Name stored in varName field.
+ *   RPR_PATTERN_SEQ    Concatenation. Children node list in children.
+ *   RPR_PATTERN_ALT    Alternation. Branch node list in children.
+ *   RPR_PATTERN_GROUP  Group (parentheses). Body node list in children.
+ *
+ * All nodes have min/max fields to express quantifiers:
+ *
+ *   A       -> VAR(A, min=1, max=1)
+ *   A+      -> VAR(A, min=1, max=INF)
+ *   A*      -> VAR(A, min=0, max=INF)
+ *   A?      -> VAR(A, min=0, max=1)
+ *   A{3,5}  -> VAR(A, min=3, max=5)
+ *
+ * If the reluctant field is true, the quantifier is reluctant (non-greedy).
+ * (RPRPatternNode.reluctant is bool; reluctant_location is the separate
+ * ParseLoc field holding the '?' token position, or -1 if absent.)
+ *
+ * Example: PATTERN ((A+ B) | C*)
+ *
+ *   ALT
+ *   +-- SEQ
+ *   |   +-- VAR(A, 1, INF)
+ *   |   +-- VAR(B, 1, 1)
+ *   +-- VAR(C, 0, INF)
+ *
+ * III-3. DEFINE Clause Transformation
+ *
+ * transformDefineClause() processes each DEFINE variable as follows:
+ *
+ *   (1) Checks for duplicate variable names
+ *   (2) Transforms the expression into a standard SQL expression
+ *   (3) Coerces to Boolean type (coerce_to_boolean)
+ *   (4) Wraps in a TargetEntry with the variable name set in resname
+ *
+ * Variables that are used in PATTERN but not defined in DEFINE are implicitly
+ * evaluated as TRUE (matching all rows).
+ *
+ * Chapter IV  Compilation Phase
+ * ============================================================================
+ *
+ * IV-1. Entry Point
+ *
+ *   create_windowagg_plan() (createplan.c)
+ *     +-- collectPatternVariables()   Collect variable names
+ *     +-- filterDefineClause()        Remove unused DEFINE entries
+ *     +-- buildRPRPattern()           NFA compilation (6 phases)
+ *
+ * IV-2. The 6 Phases of buildRPRPattern()
+ *
+ *   Phase 1: AST optimization (optimizeRPRPattern)
+ *   Phase 2: Statistics collection (scanRPRPattern)
+ *   Phase 3: Memory allocation (allocateRPRPattern)
+ *   Phase 4: NFA element fill (fillRPRPattern)
+ *   Phase 5: Finalization (finalizeRPRPattern)
+ *   Phase 6: Absorbability analysis (computeAbsorbability)
+ *
+ * IV-3. Phase 1: AST Optimization
+ *
+ * After copying the parser-generated AST, the following optimizations are
+ * applied:
+ *
+ *   (a) SEQ flattening: Unwrap nested SEQ nodes
+ *       SEQ(A, SEQ(B, C)) -> SEQ(A, B, C)
+ *
+ *   (b) Consecutive variable merging: Merge consecutive occurrences of the
+ *       same variable into a single quantifier
+ *       A A -> A{2}
+ *       A{2,3} A{1,2} -> A{3,5}
+ *
+ *   (c) Consecutive group merging: Merge repeated identical groups
+ *       (A B)+ (A B)+ -> (A B){2,INF}
+ *
+ *   (d) Consecutive ALT merging: Merge repeated identical ALT nodes
+ *       (A | B) (A | B) (A | B) -> (A | B){3}
+ *
+ *   (e) Prefix/suffix absorption: Absorb identical sequences before/after
+ *       a group
+ *       A B (A B)+ -> (A B){2,INF}
+ *
+ *   (f) ALT flattening and deduplication
+ *       (A | (B | C)) -> (A | B | C)
+ *       (A | B | A) -> (A | B)
+ *
+ *   (g) Quantifier multiplication: Collapse nested quantifiers when safe
+ *       (A+)+ -> A+
+ *       (A{2,3}){5} -> A{10,15}
+ *
+ *   (h) Single-child unwrap
+ *       SEQ(A) -> A,  (A){1,1} -> A
+ *
+ * IV-4. Phase 4: NFA Element Array Generation
+ *
+ * Transforms the optimized AST into a flat array of RPRPatternElement.
+ * This is the core data structure used for NFA simulation at runtime.
+ *
+ * RPRPatternElement struct (16 bytes):
+ *
+ *   Field      Size     Description
+ *   ---------------------------------------------------------
+ *   varId      1B      Variable ID (0-251) or control code (252-255)
+ *   depth      1B      Group nesting depth
+ *   flags      1B      Bit flags (see below)
+ *   reserved   1B      Padding
+ *   min        4B      Quantifier lower bound
+ *   max        4B      Quantifier upper bound
+ *   next       2B      Next element index (sequential flow)
+ *   jump       2B      Branch target index (for ALT/GROUP)
+ *
+ * Control codes:
+ *
+ *   RPR_VARID_BEGIN (252)  Group start marker
+ *   RPR_VARID_END   (253)  Group end marker
+ *   RPR_VARID_ALT   (254)  Alternation start marker
+ *   RPR_VARID_FIN   (255)  Pattern completion marker
+ *
+ * Element flags (1 byte, bitmask):
+ *
+ *   0x01  RPR_ELEM_RELUCTANT          (VAR, BEGIN, END)
+ *         Non-greedy quantifier.  Prefers shorter match: try exit-loop
+ *         first, then repeat.  Set on VAR for simple (A+?),
+ *         on BEGIN+END for group ((...)+?).
+ *
+ *   0x02  RPR_ELEM_EMPTY_LOOP         (END)
+ *         Group body can produce empty match (all children nullable).
+ *         Creates a fast-forward exit clone alongside the normal
+ *         loop-back so cycle detection doesn't kill legitimate
+ *         matches. (IV-4b)
+ *
+ *   0x04  RPR_ELEM_ABSORBABLE_BRANCH  (VAR, BEGIN, END, ALT)
+ *         Element lies within an absorbable region.  Used at runtime
+ *         to track whether the current NFA state is in an absorbable
+ *         context.
+ *
+ *   0x08  RPR_ELEM_ABSORBABLE         (VAR, END)
+ *         Absorption judgment 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
+ *
+ *   Accessor macros:
+ *     RPRElemIsReluctant(e)        (e)->flags & 0x01
+ *     RPRElemCanEmptyLoop(e)       (e)->flags & 0x02
+ *     RPRElemIsAbsorbableBranch(e) (e)->flags & 0x04
+ *     RPRElemIsAbsorbable(e)       (e)->flags & 0x08
+ *
+ * Example: PATTERN (A+ B | C)
+ *
+ *   AST: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
+ *
+ *   Compilation result:
+ *
+ *   idx  varId  depth  min  max  next  jump  Description
+ *   ------------------------------------------------------------
+ *    0   ALT    0      1    1    1     -1    Alternation start
+ *    1   A(0)   1      1    INF  2     3     Branch 1: A+
+ *    2   B(1)   1      1    1    4     -1    Branch 1: B -> FIN
+ *    3   C(2)   1      1    1    4     -1    Branch 2: C -> FIN
+ *    4   FIN    0      1    1    -1    -1    Pattern completion
+ *
+ *   - idx 0: ALT marker. next(=1) is the start of the first branch
+ *   - idx 1: Variable A. next(=2) is B, jump(=3) is the start of the second
+ *            branch
+ *   - idx 2: Variable B. next(=4) is FIN
+ *   - idx 3: Variable C. next(=4) is FIN
+ *   - idx 4: FIN marker. Match completion signal
+ *
+ * Roles of next and jump:
+ *
+ *   - next: The next element to move to "after consuming" the current element.
+ *           For VAR, the next position after a successful match.
+ *           For BEGIN/END, the next position inside/outside the group.
+ *
+ *   - jump: The element to "skip to."
+ *           In ALT, a jump from one branch to the next branch.
+ *           In BEGIN, a skip path to END+1 (for groups with min=0).
+ *           In END, a loop-back to the start of the group body.
+ *
+ * Example: PATTERN ((A B)+)
+ *
+ *   idx  varId    depth  min  max  next  jump  Description
+ *   --------------------------------------------------------------
+ *    0   BEGIN    0      1    INF  1     4     Group start
+ *    1   A(0)     1      1    1    2     -1    A
+ *    2   B(1)     1      1    1    3     -1    B
+ *    3   END      0      1    INF  4     1     Group end
+ *    4   FIN      0      1    1    -1    -1    Pattern completion
+ *
+ *   - idx 0: BEGIN. next(=1) enters the group body.
+ *            jump(=4) skips to after END = FIN (used when min=0).
+ *   - idx 3: END. next(=4) exits the group.
+ *            jump(=1) loops back to the start of the group body.
+ *
+ * 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:
+ *
+ *   Greedy (default):  try loop-back first, then exit  (prefer longer match)
+ *   Reluctant:         try exit first, then loop-back   (prefer shorter match)
+ *
+ * The flag is set on all elements that carry the quantifier:
+ *
+ *   Simple VAR (A+?):     RPR_ELEM_RELUCTANT on the VAR element
+ *   Group ((...)+?):      RPR_ELEM_RELUCTANT on BEGIN and END elements
+ *
+ * At runtime (nfa_advance), the flag controls DFS exploration order:
+ *
+ *   VAR with quantifier:
+ *     Greedy:    primary path = next (continue), clone = jump (skip)
+ *     Reluctant: primary path = jump (skip), clone = next (continue)
+ *
+ *   END element:
+ *     Greedy:    primary path = jump (loop-back), clone = next (exit)
+ *     Reluctant: primary path = next (exit), clone = jump (loop-back)
+ *
+ *   BEGIN with min=0:
+ *     Greedy:    primary path = next (enter group), clone = jump (skip)
+ *     Reluctant: primary path = jump (skip), clone = next (enter group)
+ *
+ * The absorption optimization requires greedy quantifiers. Reluctant
+ * quantifiers are excluded from absorbability analysis (see IV-5).
+ *
+ * IV-4b. Empty Loop Flag (RPR_ELEM_EMPTY_LOOP)
+ *
+ * The empty-loop flag is set during Phase 4 (fillRPRPatternGroup) on the END
+ * element when the group body is nullable -- i.e., every path through the
+ * body can match zero rows (all children are nullable).
+ *
+ * Example patterns that trigger this flag:
+ *
+ *   (A?)*    A is nullable (min=0), so group body is nullable -> END gets flag
+ *   (A? B?)+ Both children nullable -> body nullable -> END gets flag
+ *   (A | B*) B* is nullable, making the ALT nullable -> END gets flag
+ *
+ * The flag works in conjunction with the empty match cycle detection
+ * (elemIdx visited bitmap). Without this flag, cycle detection alone would
+ * cause legitimate matches to fail.
+ *
+ * Problem example: (A*){2,3}
+ *   - Iteration 1: A* consumes all available rows -> count=1, END reached
+ *   - Loop-back for iteration 2: A* matches zero rows -> END reached again
+ *   - Cycle detection sees the same elemIdx on the same row -> state killed
+ *   - count never reaches min(2) -> match fails (incorrect)
+ *
+ * With the RPR_ELEM_EMPTY_LOOP flag, nfa_advance_end creates two paths:
+ * the normal loop-back (which cycle detection will eventually kill) and
+ * a fast-forward exit clone that bypasses the loop entirely.
+ * (See IX-4(c) for detailed runtime behavior.)
+ *     - Empty match is impossible since body is not nullable
+ *
+ * IV-5. Absorbability Analysis (RPR_ELEM_ABSORBABLE)
+ *
+ * Context absorption is an optimization technique that reduces O(n^2) to O(n).
+ * (Runtime behavior is described in Chapter VIII.)
+ *
+ * This phase determines whether the pattern has a structure suitable for the
+ * absorption optimization and sets flags on the relevant elements:
+ *
+ *   RPR_ELEM_ABSORBABLE         Absorption comparison point
+ *   RPR_ELEM_ABSORBABLE_BRANCH  Element within an absorbable region
+ *
+ * Eligibility conditions:
+ *
+ *   (1) SKIP PAST LAST ROW (not NEXT ROW)
+ *   (2) Frame end is UNBOUNDED FOLLOWING
+ *
+ * Structural conditions (isUnboundedStart + computeAbsorbabilityRecursive):
+ *
+ *   Case 1: Simple VAR+ (e.g., A+)
+ *           -> ABSORBABLE | ABSORBABLE_BRANCH set on the VAR
+ *   Case 2: GROUP+ whose body consists only of {1,1} VARs (e.g., (A B)+)
+ *           -> ABSORBABLE_BRANCH on children,
+ *             ABSORBABLE | ABSORBABLE_BRANCH on END
+ *   Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
+ *           -> Recurses from BEGIN into the body, applying Case 1.
+ *             ABSORBABLE | ABSORBABLE_BRANCH set on A.
+ *             B and END get no flags -> absorption stops once past A.
+ *
+ * Absorbability is determined per-element, not per-pattern.
+ * Absorption comparison is performed only when a state resides at an
+ * element with the RPR_ELEM_ABSORBABLE flag. Once a state leaves the
+ * flagged region, absorption is permanently disabled for that state.
+ *
+ * Through this mechanism, the runtime guarantees monotonicity:
+ * "a context that started earlier always subsumes a context that
+ * started later."
+ *
+ * Chapter V  NFA Runtime Data Structures
+ * ============================================================================
+ *
+ * V-1. RPRNFAState -- NFA State
+ *
+ * A single NFA state represents "how far the pattern has progressed."
+ *
+ *   Field         Description
+ *   -----------------------------------------------------------
+ *   elemIdx       Index of the current pattern element
+ *   counts[]      Repetition count per group depth
+ *   isAbsorbable  Whether the state is in an absorbable region
+ *   next          Next state in the linked list
+ *
+ * The size of the counts array is rpPattern->maxDepth (= maximum group
+ * nesting depth + 1), allocated as a flexible array member at the end of
+ * the struct.
+ *
+ * Example: In PATTERN ((A B)+ C), a state waiting for B in the 3rd iteration
+ *
+ *   Element array: [0:BEGIN(d0) 1:A(d1) 2:B(d1) 3:END(d0) 4:C(d0) 5:FIN]
+ *
+ *   elemIdx = 2 (B, depth 1)
+ *   counts[0] = 2 (depth 0: depth of END. Group completed 2 iterations)
+ *   counts[1] = 1 (depth 1: depth of B. A matched in current iteration)
+ *
+ *   Counts are indexed by depth, not by elemIdx.
+ *   counts[0] is incremented when passing through END(depth 0),
+ *   and the group repetition count is preserved even when
+ *   the state is at B(depth 1).
+ *
+ * Definition of two states being "equal":
+ *
+ *   Two states are equal if they have the same elemIdx and the same counts
+ *   up to the depth of that element.
+ *   nfa_states_equal() compares counts[0..elem->depth] using memcmp.
+ *   Only counts at or below the depth of the current element are meaningful.
+ *
+ * V-2. RPRNFAContext -- Matching Context
+ *
+ * A single context represents "a matching attempt started from a specific
+ * start row."
+ *
+ *   Field                 Description
+ *   ---------------------------------------------------------------------
+ *   states                Linked list of active NFA states
+ *   matchStartRow         Row number where matching started
+ *   matchEndRow           Row number where matching completed
+ *                         (-1 if incomplete)
+ *   lastProcessedRow      Last row processed
+ *   matchedState          State that reached FIN (for greedy fallback)
+ *   hasAbsorbableState    Whether this context can absorb other contexts
+ *   allStatesAbsorbable   Whether this context can be absorbed
+ *   next, prev            Doubly-linked list
+ *
+ * Since the NFA is nondeterministic, multiple states can coexist
+ * simultaneously within a single context.
+ *
+ * Example: In PATTERN (A | B) C, if the first row matches both A and B,
+ * two states coexist within the context:
+ *
+ *   State 1: elemIdx=3 (waiting for C, via branch A)
+ *   State 2: elemIdx=3 (waiting for C, via branch B)
+ *
+ * In this case, since the (elemIdx, counts) of the two states are equal,
+ * nfa_add_state_unique() retains only State 1 (branch A), which was
+ * added first.
+ * Because DFS processes the first branch of ALT first, the state via A
+ * is registered first, and the state via B is discarded as a duplicate.
+ * This is the preferment guarantee.
+ *
+ * V-3. RPR Fields of WindowAggState
+ *
+ *   nfaContext / nfaContextTail   Doubly-linked list of active contexts
+ *   nfaContextFree                Reuse pool for contexts
+ *   nfaStateFree                  Reuse pool for states
+ *   nfaVarMatched                 Per-row cache: varMatched[varId]
+ *   nfaVisitedElems               Bitmap for cycle detection
+ *   nfaStateSize                  Precomputed size of RPRNFAState
+ *
+ * Memory management:
+ *
+ *   States and contexts are managed through their own free lists.
+ *   Instead of palloc, they are obtained from the reuse pool, and
+ *   returned to the pool upon deallocation.
+ *   This reduces the overhead of frequent allocation/deallocation.
+ *
+ * Chapter VI  NFA Execution: 3-Phase Model
+ * ============================================================================
+ *
+ * VI-1. Entry Point and Overall Flow
+ *
+ * When the window function processes each row, row_is_in_reduced_frame()
+ * is called. This function determines whether the current row belongs to
+ * a matched frame, and if necessary, calls update_reduced_frame() to
+ * drive the NFA.
+ *
+ * Flow of update_reduced_frame():
+ *
+ *   (1) Find or create a context for the target row
+ *   (2) Enter the row processing loop
+ *   (3) After the loop ends, record the result in reduced_frame_map
+ *
+ * Pseudocode of the row processing loop:
+ *
+ *   targetCtx = ExecRPRGetHeadContext(pos)
+ *   if targetCtx == NULL:
+ *       targetCtx = ExecRPRStartContext(pos)
+ *
+ *   for currentPos = startPos; targetCtx->states != NULL; currentPos++:
+ *       if not nfa_evaluate_row(currentPos):  -- row does not exist
+ *           ExecRPRFinalizeAllContexts()      -- finalize all contexts
+ *           ExecRPRCleanupDeadContexts()      -- clean up after finalization
+ *           break
+ *
+ *       ExecRPRProcessRow(currentPos)         -- 3-phase processing
+ *       ExecRPRStartContext(currentPos + 1)   -- pre-create next start point
+ *       ExecRPRCleanupDeadContexts()          -- remove dead contexts
+ *
+ * Key point: Processing a single row may require processing multiple rows
+ * ahead. Due to the nature of window functions, determining the frame for
+ * row N requires looking at rows beyond N.
+ *
+ * VI-2. Context Creation: ExecRPRStartContext()
+ *
+ * Creates a new context and performs the initial advance.
+ *
+ *   (1) Allocate context via nfa_context_alloc()
+ *   (2) Set matchStartRow = pos
+ *   (3) Create initial state: elemIdx=0 (first pattern element),
+ *       counts=all zero
+ *   (4) Call nfa_advance(initialAdvance=true)
+ *
+ * The initial advance expands epsilon transitions at the beginning of
+ * the pattern. For example, the initial advance for PATTERN ((A | B) C):
+ *
+ *   Start: elemIdx=0 (ALT)
+ *     -> Expand ALT branches
+ *       -> elemIdx=1 (A) -- VAR, so add state; stop here
+ *       -> elemIdx=2 (B) -- VAR, so add state; stop here
+ *
+ *   Result: Two states in the context {waiting for A, waiting for B}
+ *
+ * During the initial advance, reaching FIN is not recorded as a match.
+ * This is to prevent empty matches.
+ *
+ * VI-3. Row Evaluation: nfa_evaluate_row()
+ *
+ * Evaluates all variable conditions in the DEFINE clause at once for
+ * the current row.
+ *
+ *   for each defineClause[i]:
+ *       result = ExecEvalExpr(defineClause[i])
+ *       varMatched[i] = (not null and true)
+ *
+ * To support row navigation operators such as PREV() and NEXT(),
+ * the previous row, current row, and next row are set in separate slots:
+ *
+ *   ecxt_scantuple  = previous row (for PREV reference)
+ *   ecxt_outertuple = current row  (default reference)
+ *   ecxt_innertuple = next row     (for NEXT reference)
+ *
+ * The varMatched array is referenced later in Phase 1 (Match).
+ *
+ * VI-4. ExecRPRProcessRow(): 3-Phase Processing
+ *
+ * NFA processing for a single row is divided into three phases:
+ *
+ *   +--------------------------------------------+
+ *   |  Phase 1: MATCH (convergence)              |
+ *   |  Compare the current row against each VAR  |
+ *   |  state. Remove states that fail to match.  |
+ *   |                                            |
+ *   |  Phase 2: ABSORB (absorption)              |
+ *   |  Merge duplicate contexts to prevent       |
+ *   |  state explosion.                          |
+ *   |                                            |
+ *   |  Phase 3: ADVANCE (expansion)              |
+ *   |  Expand epsilon transitions to prepare     |
+ *   |  for the next row.                         |
+ *   +--------------------------------------------+
+ *
+ * This ordering is important:
+ *
+ *   - Match executes first to "consume the current row."
+ *   - Absorb executes immediately after Match, when states have been updated.
+ *   - Advance executes last to prepare "states waiting for the next row."
+ *
+ * Chapter VII  Phase 1: Match
+ * ============================================================================
+ *
+ * nfa_match() iterates through each state in the context:
+ *
+ *   (1) Check whether the state's elemIdx is a VAR element
+ *   (2) Compare against the current row using nfa_eval_var_match()
+ *   (3) Match success: increment repetition count, retain state
+ *   (4) Match failure: remove state
+ *
+ * Match determination (nfa_eval_var_match):
+ *
+ *   If varId is within the range of defineVariableList:
+ *       Use the value of varMatched[varId]
+ *
+ *   If varId exceeds the range (variable not defined in DEFINE):
+ *       Unconditionally true (matches all rows)
+ *
+ * Immediate advance for simple VARs:
+ *
+ *   For a VAR with min=1, max=1 where the next element is END,
+ *   the Match phase processes through END immediately.
+ *   This is necessary for accurate state comparison in Phase 2 (Absorb).
+ *
+ *   Example: In PATTERN ((A B)+), when A matches, it immediately advances
+ *   to B, and when B matches, it immediately advances through END to
+ *   complete the group count. This enables absorption comparison with
+ *   other contexts.
+ *
+ * Chapter VIII  Phase 2: Absorb (Context Absorption)
+ * ============================================================================
+ *
+ * VIII-1. Problem
+ *
+ * In the current implementation, a new context is started for each row
+ * processed.
+ * Applying PATTERN (A+) to 10 rows produces 10 contexts,
+ * each of which tracks state independently.
+ *
+ * If there are N rows, the total number of states becomes O(N^2):
+ *
+ *   Context 1 (started at row 1): can match A up to N times
+ *   Context 2 (started at row 2): can match A up to N-1 times
+ *   ...
+ *   Context N (started at row N): can match A 1 time
+ *
+ * VIII-2. Solution: Context Absorption
+ *
+ * Key observation: a context started earlier contains
+ * all matches of a later-started context (monotonicity principle).
+ *
+ * If Context 1 started at row 1 and matched A 5 times,
+ * the state where Context 2 (started at row 2) matched A 4 times
+ * is already contained within Context 1.
+ *
+ * Therefore Context 2 can be "absorbed" into Context 1.
+ *
+ * VIII-3. Absorption Conditions
+ *
+ *   (1) The pattern is marked as isAbsorbable (see IV-5)
+ *   (2) allStatesAbsorbable of the target context is true
+ *   (3) An earlier context "covers" all states of the target
+ *
+ * Cover condition (nfa_states_covered):
+ *
+ *   A state with the same elemIdx exists in the earlier context,
+ *   and the count at that depth is greater than or equal -- then it is covered.
+ *
+ * VIII-4. Dual-Flag Design
+ *
+ * Two boolean flags make the absorption decision efficient:
+ *
+ *   hasAbsorbableState (monotonic: only true->false transition possible)
+ *     "Does this context have the ability to absorb other contexts?"
+ *     true if at least one absorbable state exists.
+ *     Transitions to false when states are removed leaving no absorbable
+ *     states.
+ *     Once false, it never becomes true again.
+ *
+ *   allStatesAbsorbable (dynamic: can fluctuate)
+ *     "Can this context be absorbed?"
+ *     true if all states are in an absorbable region.
+ *     Becomes false when a non-absorbable state is added; reverts to true
+ *     when it is removed.
+ *
+ * VIII-5. Absorption Order
+ *
+ * nfa_absorb_contexts() traverses from tail (newest) to head (oldest).
+ *
+ *   for ctx = tail to head:
+ *       if ctx.allStatesAbsorbable:
+ *           for older = ctx.prev to head:
+ *               if older.hasAbsorbableState:
+ *                   if nfa_states_covered(older, ctx):
+ *                       free(ctx)  -- absorbed
+ *                       break
+ *
+ * Since inspection starts from the newest context, the most recently started
+ * (= having the shortest match) context is absorbed first.
+ *
+ * Chapter IX  Phase 3: Advance (Epsilon Transition Expansion)
+ * ============================================================================
+ *
+ * IX-1. Overview
+ *
+ * nfa_advance() expands epsilon transitions from each state after Match,
+ * generating "new states waiting for the next row."
+ *
+ * An epsilon transition is a transition that moves without consuming a row:
+ *
+ *   - ALT: branch to each alternative
+ *   - BEGIN: enter group (or skip if min=0)
+ *   - END: loop-back within group (or exit when condition is met)
+ *   - FIN: record match completion
+ *   - VAR loop/exit: repeat/exit according to the quantifier
+ *
+ * Expansion stops upon reaching a VAR element, and the state is added.
+ * This is because VAR is the element that "will consume the next row."
+ *
+ * IX-2. Processing Order: DFS and Preferment
+ *
+ * advance processes states in lexicographic order,
+ * performing Depth-First Search (DFS) on each state.
+ *
+ * This DFS order is what guarantees the SQL standard's "preferment":
+ *
+ *   The branch that appears first in the PATTERN text takes precedence.
+ *
+ * Example: PATTERN (A | B) C
+ *
+ *   The first branch A of the ALT takes precedence over the second branch B.
+ *   When both A and B can match, the match via A is selected.
+ *
+ * nfa_add_state_unique() prevents duplicate addition of the same state,
+ * so the state added first (= from the preferred branch) is retained.
+ *
+ * IX-3. Routing Function: nfa_route_to_elem()
+ *
+ * All inter-element transitions in the advance phase go through
+ * nfa_route_to_elem().
+ * This function branches its behavior based on the type of the next element:
+ *
+ *   If the next element is VAR:
+ *     (1) Add the state to the context (nfa_add_state_unique)
+ *     (2) If the VAR has min=0, also add a skip path (recurse via next)
+ *     -> Expansion stops here (VAR is the element that "will consume the next
+ *        row")
+ *
+ *   If the next element is non-VAR (ALT, BEGIN, END, FIN):
+ *     -> Recursively call nfa_advance_state() to continue expansion
+ *
+ * With this structure, advance recursively follows epsilon transitions
+ * until reaching a VAR, consistently stopping only at VAR elements.
+ *
+ * IX-4. Per-Element advance Behavior
+ *
+ * (a) ALT (nfa_advance_alt)
+ *
+ *   Upon encountering an ALT element, all branches are expanded in order.
+ *   The first element of each branch is connected via a jump pointer.
+ *
+ *   idx=0 (ALT) -> branch 1 start (next) -> branch 2 start (jump) -> ...
+ *
+ *   nfa_advance_state() is recursively called for each branch.
+ *
+ * (b) BEGIN (nfa_advance_begin)
+ *
+ *   Handles group entry.
+ *   jump points to the element after END (= first element outside the group).
+ *
+ *   Greedy (default):
+ *     (1) Enter the group body (move via next, reset the count at that depth)
+ *     (2) If min=0, also add a group skip path (move via jump)
+ *
+ *   Reluctant:
+ *     Order reversed -- skip path first, group entry second.
+ *     If the skip path reaches FIN, the group entry path is not generated
+ *     (shortest match preferred).
+ *
+ * (c) END (nfa_advance_end)
+ *
+ *   Handles group termination. This is the core of the repetition logic.
+ *
+ *   Let count be the count at the current depth:
+ *
+ *   count < min:
+ *     Loop-back (move via jump, repeat the group body)
+ *
+ *     If the RPR_ELEM_EMPTY_LOOP flag is set:
+ *       In addition to loop-back, also add a fast-forward exit path.
+ *       This is because the body may produce an empty match, causing count
+ *       to never reach min. fast-forward resets counts[depth] to 0
+ *       and exits via next (treating the remaining required iterations
+ *       as empty matches).
+ *
+ *   min <= count < max:
+ *     Greedy: loop-back first, exit second
+ *     Reluctant: exit first, loop-back second
+ *                If the exit path reaches FIN, loop-back is omitted.
+ *
+ *   count >= max:
+ *     Unconditional exit (move via next)
+ *
+ *   On exit: reset counts[depth] = 0, and if the next element is an outer END,
+ *   increment the count at the outer depth.
+ *
+ * (d) VAR (nfa_advance_var)
+ *
+ *   Handles repeat/exit for a VAR element with a quantifier.
+ *
+ *   Let count be the count at the current depth:
+ *
+ *   count < min:
+ *     Unconditional loop (stay at the same elemIdx, wait for the next row)
+ *
+ *   min <= count < max:
+ *     Greedy: loop first, exit (next) second
+ *     Reluctant: exit first, loop second
+ *                If the exit path reaches FIN, loop is omitted.
+ *
+ *   count >= max:
+ *     Unconditional exit (move via next)
+ *
+ *   On exit: reset counts[depth] = 0.
+ *
+ * (e) FIN
+ *
+ *   Match success. The current state is moved to matchedState for recording,
+ *   and matchEndRow is set to the current row.
+ *
+ *   Upon reaching FIN, all remaining unprocessed states are removed
+ *   (early termination). By DFS order, the path that reached FIN first
+ *   has the highest preferment, so the rest are inferior paths.
+ *   This is the core mechanism that guarantees preferment.
+ *
+ *   In SKIP PAST LAST ROW mode, upon reaching FIN, subsequent contexts
+ *   that started within the match range are immediately pruned.
+ *
+ * IX-5. State Deduplication: nfa_add_state_unique()
+ *
+ * When adding a new state to a context, it is compared against existing
+ * states;
+ * if an identical state already exists, it is not added.
+ *
+ * Comparison criteria: elemIdx + counts[0..elem->depth] (see V-1)
+ *
+ * This deduplication is the core mechanism that suppresses NFA state
+ * explosion.
+ * Because DFS order causes preferred-branch states to be added first,
+ * identical states from lower-priority branches are automatically discarded.
+ *
+ * IX-6. Cycle Detection: nfaVisitedElems
+ *
+ * When a group body can produce an empty match,
+ * looping back from END may cause an infinite loop.
+ *
+ * Example: PATTERN ((A?)*)
+ *
+ *   A? has min=0, so it can pass through without matching.
+ *   If the outer group repeats: BEGIN -> A? skip -> END -> BEGIN -> ...
+ *
+ * To prevent this:
+ *
+ *   (1) At compile time: set the RPR_ELEM_EMPTY_LOOP flag on the END
+ *       of groups whose body is nullable.
+ *       The runtime effect of this flag is described in IX-4(c):
+ *       when count < min, a fast-forward exit path is added,
+ *       resolving the deadlock where count cannot increase due to empty
+ *       matches.
+ *
+ *   (2) At runtime: initialize the nfaVisitedElems bitmap immediately before
+ *       DFS expansion of each state within advance (once per state).
+ *       During DFS, set the corresponding elemIdx bit when visiting each
+ *       element.
+ *       If a previously visited elemIdx is revisited, that path is terminated.
+ *
+ *   Note: the bitmap tracks only elemIdx and does not consider counts.
+ *   Therefore, legitimate revisits to the same elemIdx but with different
+ *   counts may also be blocked.  This only occurs when the group body is
+ *   nullable (all paths can match empty), causing END -> loop-back ->
+ *   skip -> END within a single DFS.  In such cases the END element has
+ *   the RPR_ELEM_EMPTY_LOOP flag, so the fast-forward exit (IX-4(c))
+ *   provides an alternative path that bypasses the cycle.
+ *
+ * Chapter X  Match Result Processing
+ * ============================================================================
+ *
+ * X-1. Reduced Frame Map
+ *
+ * RPR match results are recorded in a byte array called reduced_frame_map.
+ * One byte is allocated per row, and the value is one of the following:
+ *
+ *   RF_NOT_DETERMINED (0)  Not yet processed
+ *   RF_FRAME_HEAD     (1)  Start row of the match
+ *   RF_SKIPPED        (2)  Interior row of the match (skipped in frame)
+ *   RF_UNMATCHED      (3)  Match failure
+ *
+ * The window function references this map to determine frame inclusion for
+ * each row.
+ *
+ * X-2. AFTER MATCH SKIP
+ *
+ * Determines the starting point for the next match attempt after a successful
+ * match:
+ *
+ *   SKIP TO NEXT ROW:
+ *     New match attempt begins from the row after the match start row.
+ *     Overlapping matches are possible.
+ *
+ *   SKIP PAST LAST ROW:
+ *     New match attempt begins from the row after the match end row.
+ *     Only non-overlapping matches are possible.
+ *
+ * X-3. INITIAL vs SEEK
+ *
+ *   Standard definition (section 6.12):
+ *   INITIAL: "is used to look for a match whose first row is R."
+ *   SEEK:    "is used to permit a search for the first match anywhere
+ *            from R through the end of the full window frame."
+ *   In either case, if there is no match, the reduced window frame is empty.
+ *   The default is INITIAL.
+ *
+ *   Current implementation:
+ *   SEEK is not supported (the parser raises an error).
+ *   Only INITIAL is supported, searching only for matches starting at each
+ *   row position pos.
+ *
+ * Chapter XI  Worked Example: Full Execution Trace
+ * ============================================================================
+ *
+ * XI-1. Query
+ *
+ *   SELECT company, tdate, price,
+ *          first_value(price) OVER w AS start_price,
+ *          last_value(price) OVER w AS end_price
+ *   FROM stock
+ *   WINDOW w AS (
+ *     PARTITION BY company
+ *     ORDER BY tdate
+ *     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ *     AFTER MATCH SKIP PAST LAST ROW
+ *     PATTERN (A+ B)
+ *     DEFINE A AS price > PREV(price),
+ *            B AS price < PREV(price)
+ *   );
+ *
+ * XI-2. Data
+ *
+ *   Row#    tdate       price
+ *   --------------------------
+ *   0       2024-01-01  100
+ *   1       2024-01-02  110
+ *   2       2024-01-03  120
+ *   3       2024-01-04  115
+ *   4       2024-01-05  130
+ *
+ * XI-3. Compilation Result
+ *
+ *   PATTERN (A+ B) -> unchanged after optimization
+ *
+ *   idx  varId  depth  min  max  next  jump
+ *   -----------------------------------------
+ *    0   A(0)   0      1    INF  1     -1     A+
+ *    1   B(1)   0      1    1    2     -1     B
+ *    2   FIN    0      1    1    -1    -1
+ *
+ *   DEFINE: A -> "price > PREV(price)", B -> "price < PREV(price)"
+ *   isAbsorbable = true (A+ is a simple unbounded VAR)
+ *
+ * XI-4. Execution Trace
+ *
+ * --- Row 0 (price=100) ---
+ *
+ *   update_reduced_frame(0) called.
+ *
+ *   Context C0 created (matchStartRow=0).
+ *   Initial advance: elemIdx=0(A) -> VAR, so state is added.
+ *   C0.states = [{elemIdx=0, counts=[0]}]
+ *
+ *   nfa_evaluate_row(0):
+ *     A: price(100) > PREV(price) -> no PREV -> false
+ *     B: price(100) < PREV(price) -> no PREV -> false
+ *     varMatched = [false, false]
+ *
+ *   ExecRPRProcessRow(0):
+ *     Phase 1 (Match): A(0) state vs varMatched[0]=false -> state removed
+ *     C0.states = [] (empty)
+ *
+ *     Phase 2 (Absorb): skipped (no states)
+ *     Phase 3 (Advance): skipped (no states)
+ *
+ *   C0.states is empty, so the loop terminates.
+ *   matchEndRow < matchStartRow -> RF_UNMATCHED.
+ *   register_reduced_frame_map(0, RF_UNMATCHED).
+ *
+ * --- Row 1 (price=110) ---
+ *
+ *   update_reduced_frame(1) called.
+ *
+ *   Context C1 created (matchStartRow=1).
+ *   Initial advance: C1.states = [{elemIdx=0, counts=[0]}]
+ *
+ *   nfa_evaluate_row(1):
+ *     A: 110 > PREV(100) -> true
+ *     B: 110 < PREV(100) -> false
+ *     varMatched = [true, false]
+ *
+ *   ExecRPRProcessRow(1):
+ *     Phase 1 (Match): A(0) match succeeds -> counts[0]++ -> counts=[1]
+ *     C1.states = [{elemIdx=0, counts=[1]}]
+ *
+ *     Phase 3 (Advance):
+ *       State {elemIdx=0, counts=[1]}: A+ (min=1, count=1, max=INF)
+ *         count >= min, so:
+ *         Greedy -> loop first: keep {elemIdx=0, counts=[1]}
+ *                   exit: reset counts[0]=0, next(=1) -> {elemIdx=1,
+ *                         counts=[0]}
+ *     C1.states = [{elemIdx=0, counts=[1]}, {elemIdx=1, counts=[0]}]
+ *
+ * --- Row 2 (price=120) ---
+ *
+ *   Context C2 created (matchStartRow=2).
+ *   Initial advance: C2.states = [{elemIdx=0, counts=[0]}]
+ *
+ *   nfa_evaluate_row(2):
+ *     A: 120 > PREV(110) -> true
+ *     B: 120 < PREV(110) -> false
+ *     varMatched = [true, false]
+ *
+ *   C1 ExecRPRProcessRow(2):
+ *     Phase 1 (Match):
+ *       {elemIdx=0, counts=[1]}: A matches -> counts=[2]
+ *       {elemIdx=1, counts=[0]}: B does not match -> removed
+ *     C1.states = [{elemIdx=0, counts=[2]}]
+ *
+ *   C2 ExecRPRProcessRow(2):
+ *     Phase 1 (Match):
+ *       {elemIdx=0, counts=[0]}: A matches -> counts=[1]
+ *     C2.states = [{elemIdx=0, counts=[1]}]
+ *
+ *     Phase 2 (Absorb):
+ *       Does C1 (started earlier) cover C2?
+ *         C1: {elemIdx=0, counts=[2]}, C2: {elemIdx=0, counts=[1]}
+ *         Same elemIdx, C1.counts >= C2.counts -> covered
+ *       C2 absorbed. -> removed.
+ *
+ *     Phase 3 (Advance):
+ *       {elemIdx=0, counts=[2]}: Greedy -> loop + exit
+ *         Loop: {elemIdx=0, counts=[2]}
+ *         Exit: reset counts[0]=0, next(=1) -> {elemIdx=1, counts=[0]}
+ *     C1.states = [{elemIdx=0, counts=[2]}, {elemIdx=1, counts=[0]}]
+ *
+ *   Context C3 created (matchStartRow=3).
+ *
+ * --- Row 3 (price=115) ---
+ *
+ *   nfa_evaluate_row(3):
+ *     A: 115 > PREV(120) -> false
+ *     B: 115 < PREV(120) -> true
+ *     varMatched = [false, true]
+ *
+ *   ExecRPRProcessRow(3):
+ *     Phase 1 (Match):
+ *       {elemIdx=0, counts=[2]}: A does not match -> removed
+ *       {elemIdx=1, counts=[0]}: B matches -> counts=[1]
+ *     C1.states = [{elemIdx=1, counts=[1]}]
+ *
+ *     Phase 3 (Advance):
+ *       {elemIdx=1, counts=[1]}: B (min=1, max=1)
+ *         count(1) >= max(1) -> unconditional exit
+ *         Reset counts[0]=0, next = 2 (FIN)
+ *       FIN reached -> matchEndRow = 3, matchedState recorded.
+ *       Early termination: no remaining states, so completed immediately.
+ *     C1.states = [] (empty after reaching FIN)
+ *
+ *   C1.states is empty and matchEndRow=3 >= matchStartRow=1 -> match succeeds.
+ *
+ *   register_reduced_frame_map(1, RF_FRAME_HEAD)
+ *   register_reduced_frame_map(2, RF_SKIPPED)
+ *   register_reduced_frame_map(3, RF_SKIPPED)
+ *
+ * --- Row 4 (price=130) ---
+ *
+ *   update_reduced_frame(4) called.
+ *   C3 was already created but matchStartRow=3, so it is not applicable.
+ *   New context C4 created (matchStartRow=4).
+ *
+ *   nfa_evaluate_row(4):
+ *     A: 130 > PREV(115) -> true
+ *     B: 130 < PREV(115) -> false
+ *
+ *   ... No subsequent rows, so ExecRPRFinalizeAllContexts() is called.
+ *   Match incomplete -> RF_UNMATCHED.
+ *
+ * XI-5. Final Result
+ *
+ *   Row 0: RF_UNMATCHED  -> frame = the row itself
+ *   Row 1: RF_FRAME_HEAD -> frame = rows 1 through 3
+ *   Row 2: RF_SKIPPED    -> inside row 1's match
+ *   Row 3: RF_SKIPPED    -> inside row 1's match
+ *   Row 4: RF_UNMATCHED  -> frame = the row itself
+ *
+ * Chapter XII  Summary of Key Design Decisions
+ * ============================================================================
+ *
+ * XII-1. Flat Array vs Tree-Based NFA
+ *
+ *   Choice: Flat array (RPRPatternElement[])
+ *
+ *   Rationale:
+ *   - Cache-friendly: 16-byte fixed size, contiguous memory
+ *   - Index-based references: 2-byte indices instead of pointers
+ *   - Easy to serialize: can use memcpy when passing to plan nodes
+ *
+ * XII-2. Forward-only Execution vs Backtracking
+ *
+ *   Choice: Forward-only (state set tracking)
+ *
+ *   Rationale:
+ *   - Backtracking takes exponential time in the worst case
+ *   - NFA simulation guarantees polynomial time
+ *   - DFS order naturally guarantees preferment.
+ *     Greedy/reluctant per quantifier requires only reversing the DFS order
+ *   - Window functions receive sorted rows sequentially.
+ *     Forward-only fits directly into this pipeline,
+ *     whereas backtracking requires re-fetching previous rows
+ *   - DEFINE conditions are SQL expressions (PREV, RUNNING aggregates, etc.)
+ *     with high re-evaluation cost. Forward-only requires only one evaluation
+ *     per row
+ *
+ * XII-3. Per-Context Management
+ *
+ *   Choice: Independent context per start row
+ *
+ *   Rationale:
+ *   - Supports overlapping matches under SKIP TO NEXT ROW
+ *   - Determines the frame for each row independently
+ *   - Absorption optimization can eliminate redundant contexts in O(n)
+ *
+ * XII-4. Memory Pool Management
+ *
+ *   Choice: Custom free list
+ *
+ *   Rationale:
+ *   - NFA states are created and destroyed in large numbers per row
+ *   - Avoids palloc/pfree overhead
+ *   - State size is variable (counts[] array), but within a single query
+ *     maxDepth is fixed, so all states have the same size
+ *
+ * XII-5. Execution Optimization Summary
+ *
+ *   The following optimizations make the NFA simulation practical.
+ *
+ *   -- Compile-time --
+ *
+ *   (1) AST Optimization (IV-3)
+ *
+ *     Simplifies the AST 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.
+ *
+ *     Significance: Reducing the element count directly shrinks the state
+ *     space, decreasing the cost of all subsequent runtime phases (match,
+ *     absorb, advance).
+ *
+ *   -- Runtime: advance phase --
+ *
+ *   (2) Group Skip (IX-4(b))
+ *
+ *     At the BEGIN of a group with min=0, uses jump to skip the entire
+ *     group. Moves directly to the first element outside the group without
+ *     exploring the group body. Greedy enters then skips; Reluctant skips
+ *     then enters.
+ *
+ *     Significance: For optional groups (min=0), immediately generates
+ *     a skip path without exploring the body, avoiding unnecessary DFS
+ *     expansion.
+ *
+ *   (3) State Deduplication (IX-5)
+ *
+ *     During advance, DFS may generate states with the same (elemIdx,
+ *     counts) combination through multiple paths. Additionally, unlike
+ *     VAR repetition, group repetition cannot perform absorption
+ *     comparison using VAR states, so inline advance is performed from
+ *     after Phase 1 match through to 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.
+ *
+ *     Significance: Prevents exponential growth of the state count in
+ *     ALT branches and quantifier expansion. Since DFS order causes the
+ *     preferred branch's state to be registered first, identical states
+ *     from lower-priority branches are automatically discarded, thereby
+ *     also guaranteeing preferment.
+ *
+ *   (4) Cycle Detection and Fast-Forward (IX-6, IX-4(c))
+ *
+ *     When a nullable group body (e.g., A?) repeats empty matches,
+ *     the END -> BEGIN loop-back can continue indefinitely.
+ *
+ *     Two mechanisms resolve this:
+ *     - A visited bitmap (nfaVisitedElems) blocks revisitation of the
+ *       same element, preventing infinite loops (safety)
+ *     - At an END with the RPR_ELEM_EMPTY_LOOP flag set, when
+ *       count < min, the remaining required iterations are treated as
+ *       empty matches and a fast-forward exit path out of the group is
+ *       added (correctness)
+ *
+ *     Significance: Cycle detection guarantees termination, and
+ *     fast-forward guarantees that the min condition is satisfied.
+ *     Without these, patterns containing nullable groups would fall
+ *     into infinite loops or fail to match.
+ *
+ *   (5) Match Pruning (IX-4(e))
+ *
+ *     When a state reaches FIN during advance, all remaining unprocessed
+ *     states of that context are removed. Because of DFS order, the path
+ *     that reaches FIN first has the highest preferment, so the remaining
+ *     paths are inferior.
+ *
+ *     Significance: Once the best match is determined, exploration of
+ *     inferior paths is immediately terminated. This mechanism achieves
+ *     both preferment guarantees and performance optimization.
+ *
+ *   -- Runtime: inter-context --
+ *
+ *   (6) Early Termination (SKIP PAST LAST ROW)
+ *
+ *     In SKIP PAST LAST ROW mode, when a match is found, subsequent
+ *     contexts whose start rows fall within the match range are pruned
+ *     immediately without further processing.
+ *     In SKIP TO NEXT ROW mode, overlapping contexts are preserved
+ *     because each row requires its own independent match.
+ *
+ *     Significance: Prunes subsequent contexts whose start rows overlap
+ *     with a prior match range, avoiding unnecessary processing.
+ *
+ *   (7) Context Absorption (Chapter VIII)
+ *
+ *     If an independent context is created for each row, O(n^2) states
+ *     accumulate. By exploiting the monotonicity that an earlier-started
+ *     context subsumes the states of a later-started context, redundant
+ *     contexts are eliminated early.
+ *
+ *     Absorbability is determined per-element; comparison is performed
+ *     only at elements with the RPR_ELEM_ABSORBABLE flag (see IV-5).
+ *
+ *     Significance: Keeps the number of active contexts at a constant
+ *     level, achieving O(n^2) -> O(n) time complexity. Without this,
+ *     performance degrades sharply on long partitions.
+ *
+ * Appendix A. Key Function Index
+ * ============================================================================
+ *
+ *   Function                      File                  Role
+ *   --------------------------------------------------------------------------
+ *   transformRPR                  parse_rpr.c           Parser entry point
+ *   transformDefineClause         parse_rpr.c           DEFINE transformation
+ *   collectPatternVariables       rpr.c                 Variable collection
+ *   filterDefineClause            rpr.c                 DEFINE filtering
+ *   buildRPRPattern               rpr.c                 NFA compilation main
+ *   optimizeRPRPattern            rpr.c                 AST optimization
+ *   fillRPRPattern                rpr.c                 NFA element generation
+ *   finalizeRPRPattern            rpr.c                 Finalization
+ *   computeAbsorbability          rpr.c                 Absorption analysis
+ *   update_reduced_frame          nodeWindowAgg.c       Execution main loop
+ *   nfa_evaluate_row              nodeWindowAgg.c       DEFINE evaluation
+ *   ExecRPRStartContext           execRPR.c             Context creation
+ *   ExecRPRProcessRow             execRPR.c             3-phase processing
+ *   nfa_match                     execRPR.c             Phase 1
+ *   nfa_absorb_contexts           execRPR.c             Phase 2
+ *   nfa_advance                   execRPR.c             Phase 3
+ *   nfa_advance_state             execRPR.c             Per-state branching
+ *   nfa_route_to_elem             execRPR.c             Element routing
+ *   nfa_advance_alt               execRPR.c             ALT handling
+ *   nfa_advance_begin             execRPR.c             BEGIN handling
+ *   nfa_advance_end               execRPR.c             END handling
+ *   nfa_advance_var               execRPR.c             VAR handling
+ *   nfa_add_state_unique          execRPR.c             Deduplication
+ *   nfa_states_covered            execRPR.c             Absorption check
+ *
+ * Appendix B. Data Structure Relationship Diagram
+ * ============================================================================
+ *
+ *   Parser Layer
+ *   --------
+ *   RPCommonSyntax
+ *     |--- rpSkipTo: RPSkipTo
+ *     |--- initial: bool
+ *     +--- rpPattern: RPRPatternNode* (tree)
+ *          |--- nodeType: VAR | SEQ | ALT | GROUP
+ *          |--- min, max: quantifier
+ *          |--- varName: variable name (VAR only)
+ *          +--- children: List* (SEQ/ALT/GROUP only)
+ *
+ *   Planner Layer
+ *   ----------
+ *   WindowAgg (plan node)
+ *     |--- rpSkipTo: RPSkipTo
+ *     |--- defineClause: List<TargetEntry>
+ *     +--- rpPattern: RPRPattern*
+ *          |--- numVars: int
+ *          |--- varNames: char**
+ *          |--- maxDepth: RPRDepth
+ *          |--- isAbsorbable: bool
+ *          |--- numElements: int
+ *          +--- elements: RPRPatternElement[]  (flat array)
+ *               |--- varId      (1B)
+ *               |--- depth      (1B)
+ *               |--- flags      (1B)
+ *               |--- reserved   (1B)
+ *               |--- min, max   (4B + 4B)
+ *               +--- next, jump (2B + 2B)
+ *
+ *   Executor Layer
+ *   ----------
+ *   WindowAggState
+ *     |--- rpSkipTo: RPSkipTo (AFTER MATCH SKIP mode)
+ *     |--- rpPattern: RPRPattern* (copied from plan)
+ *     |--- defineVariableList: List<String> (variable names, DEFINE order)
+ *     |--- defineClauseList: List<ExprState>
+ *     |--- nfaVarMatched: bool[] (per-row cache)
+ *     |--- nfaVisitedElems: bitmapword* (cycle detection)
+ *     |--- nfaStateSize: Size (pre-calculated RPRNFAState allocation size)
+ *     |--- nfaContext <-> nfaContextTail (doubly-linked list)
+ *     |   +--- RPRNFAContext
+ *     |       |--- states: RPRNFAState* (linked list)
+ *     |       |   |--- elemIdx
+ *     |       |   |--- counts[]
+ *     |       |   +--- isAbsorbable
+ *     |       |--- matchStartRow, matchEndRow
+ *     |       |--- lastProcessedRow
+ *     |       |--- matchedState (cloned on FIN arrival)
+ *     |       |--- hasAbsorbableState
+ *     |       +--- allStatesAbsorbable
+ *     |--- nfaContextFree (recycling pool)
+ *     +--- nfaStateFree (recycling pool)
+ *
+ * Appendix C. NFA Element Array Examples
+ * ============================================================================
+ *
+ * C-1. PATTERN (A B C)
+ *
+ *   idx  varId  depth  min  max  next  jump
+ *   ------------------------------------------
+ *    0   A      0      1    1    1     -1
+ *    1   B      0      1    1    2     -1
+ *    2   C      0      1    1    3     -1
+ *    3   FIN    0      1    1    -1    -1
+ *
+ * C-2. PATTERN (A+ B*)
+ *
+ *   idx  varId  depth  min  max  next  jump  flags
+ *   ------------------------------------------------------------------------
+ *    0   A      0      1    INF  1     -1    ABSORBABLE | ABSORBABLE_BRANCH
+ *    1   B      0      0    INF  2     -1
+ *    2   FIN    0      1    1    -1    -1
+ *
+ *   Only A+ is the absorption point (Case 1). Once past A,
+ *   absorption is permanently disabled for that state.
+ *
+ * C-3. PATTERN (A | B | C)
+ *
+ *   idx  varId  depth  min  max  next  jump
+ *   ----------------------------------------
+ *    0   ALT    0      1    1    1     -1    alternation start
+ *    1   A      1      1    1    4     2     branch 1 -> FIN, jump -> branch 2
+ *    2   B      1      1    1    4     3     branch 2 -> FIN, jump -> branch 3
+ *    3   C      1      1    1    4     -1    branch 3 -> FIN
+ *    4   FIN    0      1    1    -1    -1
+ *
+ * C-4. PATTERN ((A B)+ C)
+ *
+ *   idx  varId    depth  min  max  next  jump  flags
+ *   --------------------------------------------------------------------------
+ *    0   BEGIN    0      1    INF  1     4     ABSORBABLE_BRANCH
+ *    1   A        1      1    1    2     -1    ABSORBABLE_BRANCH
+ *    2   B        1      1    1    3     -1    ABSORBABLE_BRANCH
+ *    3   END      0      1    INF  4     1     ABSORBABLE | ABSORBABLE_BRANCH
+ *    4   C        0      1    1    5     -1
+ *    5   FIN      0      1    1    -1    -1
+ *
+ *   Case 2: GROUP+ with {1,1} body VARs. A, B are branches;
+ *   END is the absorption point. Compare with C-6 (Case 3).
+ *
+ * C-5. PATTERN ((A | B)+? C)
+ *
+ *   idx  varId    depth  min  max   next  jump  flags
+ *   -------------------------------------------------------------------
+ *    0   BEGIN    0      1    INF   1     5     RELUCTANT, group start
+ *    1   ALT      1      1    1     2     -1    alternation start
+ *    2   A        2      1    1     4     3     branch 1
+ *    3   B        2      1    1     4     -1    branch 2
+ *    4   END      0      1    INF   5     1     RELUCTANT, group end
+ *    5   C        0      1    1     6     -1
+ *    6   FIN      0      1    1     -1    -1
+ *
+ * C-6. PATTERN ((A+ B)+ C)  -- Absorbability flag example
+ *
+ *   idx  varId    depth  min  max   next  jump  flags
+ *   ---------------------------------------------------------------------------
+ *    0   BEGIN    0      1    INF   1     4     ABSORBABLE_BRANCH, group start
+ *    1   A        1      1    INF   2     -1    ABSORBABLE | ABSORBABLE_BRANCH
+ *    2   B        1      1    1     3     -1
+ *    3   END      0      1    INF   4     1     group end
+ *    4   C        0      1    1     5     -1
+ *    5   FIN      0      1    1     -1    -1
+ *
+ *   Recurses from BEGIN into the body -> A matches Case 1 (simple VAR+).
+ *   A gets ABSORBABLE | ABSORBABLE_BRANCH, BEGIN gets ABSORBABLE_BRANCH.
+ *   B and END get no flags -> absorption stops once the state advances to B.
+ *   (See IV-5 Case 3)
+ *
+ * C-7. PATTERN ((A+ B | C*)+ D)  -- Per-branch absorption in ALT
+ *
+ *   idx  varId    depth  min  max   next  jump  flags
+ *   ---------------------------------------------------------------------------
+ *    0   BEGIN    0      1    INF   1     6     ABSORBABLE_BRANCH
+ *    1   ALT      1      1    1     2     -1    ABSORBABLE_BRANCH
+ *    2   A        2      1    INF   3     4     ABSORBABLE | ABSORBABLE_BRANCH
+ *    3   B        2      1    1     5     -1
+ *    4   C        2      0    INF   5     -1    ABSORBABLE | ABSORBABLE_BRANCH
+ *    5   END      0      1    INF   6     1     EMPTY_LOOP
+ *    6   D        0      1    1     7     -1
+ *    7   FIN      0      1    1     -1    -1
+ *
+ *   ALT branches are checked independently for absorbability.
+ *   Branch 1: A+ matches Case 1 -> A gets ABSORBABLE. B has no flag.
+ *   Branch 2: C* matches Case 1 -> C gets ABSORBABLE.
+ *   Both A and C get ABSORBABLE_BRANCH as part of their respective branch
+ *   paths.
+ *   END has EMPTY_LOOP: branch 2 (C*) is nullable, making the group body
+ *   nullable.
+ *   BEGIN and ALT get ABSORBABLE_BRANCH (on the path to absorbable elements).
+ *
+ * ============================================================================
+ *   End of document
+ * ============================================================================
+ */
+
+/* Bitmap macros for NFA cycle detection (cf. bitmapset.c, tidbitmap.c) */
+#define WORDNUM(x)	((x) / BITS_PER_BITMAPWORD)
+#define BITNUM(x)	((x) % BITS_PER_BITMAPWORD)
+
+/* Forward declarations - NFA state management */
+static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
+static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
+static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
+static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
+									 int32 *counts, bool sourceAbsorbable);
+static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
+							 RPRNFAState *s2);
+static bool nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
+								 RPRNFAState *state);
+static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
+								  RPRNFAState *state, int64 matchEndRow);
+
+/* Forward declarations - NFA context management (internal) */
+static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate);
+static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
+
+/* Forward declarations - NFA statistics */
+static void nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen);
+static void nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen);
+static void nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen);
+
+/* Forward declarations - NFA absorption */
+static void nfa_update_absorption_flags(RPRNFAContext *ctx);
+static bool nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older,
+							   RPRNFAContext *newer);
+static void nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx);
+static void nfa_absorb_contexts(WindowAggState *winstate);
+
+/* Forward declarations - NFA match and advance */
+static bool nfa_eval_var_match(WindowAggState *winstate,
+							   RPRPatternElement *elem, bool *varMatched);
+static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
+					  bool *varMatched);
+static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, int64 currentPos);
+static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, RPRPatternElement *nextElem,
+							  int64 currentPos);
+static void nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
+							RPRNFAState *state, RPRPatternElement *elem,
+							int64 currentPos);
+static void nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, RPRPatternElement *elem,
+							  int64 currentPos);
+static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
+							RPRNFAState *state, RPRPatternElement *elem,
+							int64 currentPos);
+static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
+							RPRNFAState *state, RPRPatternElement *elem,
+							int64 currentPos);
+static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
+						int64 currentPos);
+
+/*
+ * NFA-based pattern matching implementation
+ *
+ * These functions implement direct NFA execution using the compiled
+ * RPRPattern structure, avoiding regex compilation overhead.
+ *
+ * Execution Flow: match -> absorb -> advance
+ * -----------------------------------------
+ * The NFA execution follows a three-phase cycle for each row:
+ *
+ * 1. MATCH (convergence): Evaluate all waiting states against current row.
+ *    States on VAR elements are checked against their defining conditions.
+ *    Failed matches are removed, successful ones may transition forward.
+ *    This is a "convergence" phase - the number of states tends to decrease.
+ *
+ * 2. ABSORB: After matching, check if any context can absorb another.
+ *    Context absorption is an optimization that merges equivalent contexts.
+ *    A context can only be absorbed if ALL its states are absorbable.
+ *
+ * 3. ADVANCE (divergence): Expand states through epsilon transitions.
+ *    States advance through ALT (alternation), END (group end), and
+ *    optional elements until reaching VAR or FIN elements where they wait.
+ *    This is a "divergence" phase - ALT creates multiple branch states.
+ *
+ * Key Design Decisions:
+ * ---------------------
+ * - VAR->END transition in match phase: When a simple VAR (max=1) matches
+ *   and the next element is END, we transition immediately in the match
+ *   phase rather than waiting for advance. This is necessary for correct
+ *   absorption: states must be at END to be marked absorbable before the
+ *   absorption check occurs.
+ *
+ * - Optional VAR skip paths: When advance lands on a VAR with min=0,
+ *   we create both a waiting state AND a skip state (like ALT branches).
+ *   This ensures patterns like "A B? C" work correctly - we need a state
+ *   waiting for B AND a state that has already skipped to C.
+ *
+ * - END->END count increment: When transitioning from one END to another
+ *   END within advance, we must increment the outer END's count. This
+ *   handles nested groups like "((A|B)+)+" correctly - exiting the inner
+ *   group counts as one iteration of the outer group.
+ *
+ * - Empty match handling: The initial advance uses currentPos =
+ *   startPos - 1 (before any row is consumed). If FIN is reached via
+ *   epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow,
+ *   resulting in UNMATCHED. For reluctant min=0 patterns (A*?, A??),
+ *   the skip path reaches FIN first and early termination prunes enter
+ *   paths, yielding an immediate empty (unmatched) result. For
+ *   greedy patterns (A*), the enter path adds VAR states first, then
+ *   the skip FIN is recorded but VAR states survive for later matching.
+ *
+ * Context Absorption Runtime:
+ * ---------------------------
+ * Absorption uses flags computed at planning time (in rpr.c) and two
+ * context-level flags maintained at runtime:
+ *
+ * State-level:
+ *   state.isAbsorbable: true if state is in the absorbable region.
+ *     - Set at creation: elem->flags & RPR_ELEM_ABSORBABLE_BRANCH
+ *     - At transition: prevAbsorbable && (newElem->flags & ABSORBABLE_BRANCH)
+ *     - Monotonic: once false, stays false forever
+ *
+ * Context-level:
+ *   ctx.hasAbsorbableState: can this context absorb others?
+ *     - True if at least one state has isAbsorbable=true
+ *     - Monotonic: true->false only (optimization: skip recalc when false)
+ *
+ *   ctx.allStatesAbsorbable: can this context be absorbed?
+ *     - True if ALL states have isAbsorbable=true
+ *     - Dynamic: can change false->true (when non-absorbable states die)
+ *
+ * Absorption Algorithm:
+ *   For each pair (older Ctx1, newer Ctx2):
+ *   1. Pre-check: Ctx1.hasAbsorbableState && Ctx2.allStatesAbsorbable
+ *      -> If false, skip (fast filter)
+ *   2. Coverage check: For each Ctx2 state with isAbsorbable=true,
+ *      find Ctx1 state with same elemIdx and count >= Ctx2.count
+ *   3. If all Ctx2 absorbable states are covered, absorb Ctx2
+ *
+ * Example: Pattern A+ B
+ *   Row 1: Ctx1 at A (count=1)
+ *   Row 2: Ctx1 at A (count=2), Ctx2 at A (count=1)
+ *   -> Both at same elemIdx (A), Ctx1.count >= Ctx2.count
+ *   -> Ctx2 absorbed
+ *
+ * The asymmetric design (Ctx1 needs hasAbsorbable, Ctx2 needs allAbsorbable)
+ * allows absorption even when Ctx1 has extra non-absorbable states.
+ */
+
+/*
+ * nfa_state_alloc
+ *
+ * Allocate an NFA state, reusing from freeList if available.
+ * freeList is stored in WindowAggState for reuse across match attempts.
+ * Uses flexible array member for counts[].
+ */
+static RPRNFAState *
+nfa_state_alloc(WindowAggState *winstate)
+{
+	RPRNFAState *state;
+
+	/* Try to reuse from free list first */
+	if (winstate->nfaStateFree != NULL)
+	{
+		state = winstate->nfaStateFree;
+		winstate->nfaStateFree = state->next;
+	}
+	else
+	{
+		/* Allocate in partition context for proper lifetime */
+		state = MemoryContextAlloc(winstate->partcontext, winstate->nfaStateSize);
+	}
+
+	/* Initialize entire state to zero */
+	memset(state, 0, winstate->nfaStateSize);
+
+	/* Update statistics */
+	winstate->nfaStatesActive++;
+	winstate->nfaStatesTotalCreated++;
+	if (winstate->nfaStatesActive > winstate->nfaStatesMax)
+		winstate->nfaStatesMax = winstate->nfaStatesActive;
+
+	return state;
+}
+
+/*
+ * nfa_state_free
+ *
+ * Return a state to the free list for later reuse.
+ */
+static void
+nfa_state_free(WindowAggState *winstate, RPRNFAState *state)
+{
+	winstate->nfaStatesActive--;
+	state->next = winstate->nfaStateFree;
+	winstate->nfaStateFree = state;
+}
+
+/*
+ * nfa_state_free_list
+ *
+ * Return all states in a list to the free list.
+ */
+static void
+nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list)
+{
+	RPRNFAState *next;
+
+	for (; list != NULL; list = next)
+	{
+		next = list->next;
+		nfa_state_free(winstate, list);
+	}
+}
+
+/*
+ * nfa_state_create
+ *
+ * Create a new state with given elemIdx and counts.
+ * isAbsorbable is computed immediately: inherited AND new element's flag.
+ * Monotonic property: once false, stays false through all transitions.
+ *
+ * Caller is responsible for linking the returned state.
+ */
+static RPRNFAState *
+nfa_state_create(WindowAggState *winstate, int16 elemIdx,
+				 int32 *counts, bool sourceAbsorbable)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	int			maxDepth = pattern->maxDepth;
+	RPRNFAState *state = nfa_state_alloc(winstate);
+	RPRPatternElement *elem = &pattern->elements[elemIdx];
+
+	state->elemIdx = elemIdx;
+	if (counts != NULL && maxDepth > 0)
+		memcpy(state->counts, counts, sizeof(int32) * maxDepth);
+
+	/*
+	 * Compute isAbsorbable immediately at transition time. isAbsorbable =
+	 * sourceAbsorbable && (elem->flags & ABSORBABLE_BRANCH) Monotonic: once
+	 * false, stays false (can't re-enter absorbable region).
+	 */
+	state->isAbsorbable = sourceAbsorbable && RPRElemIsAbsorbableBranch(elem);
+
+	return state;
+}
+
+/*
+ * nfa_states_equal
+ *
+ * Check if two states are equivalent (same elemIdx and counts).
+ */
+static bool
+nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elem;
+	int			compareDepth;
+
+	if (s1->elemIdx != s2->elemIdx)
+		return false;
+
+	/* Compare counts up to current element's depth */
+	elem = &pattern->elements[s1->elemIdx];
+	compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
+
+	if (memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
+		return false;
+
+	return true;
+}
+
+/*
+ * nfa_add_state_unique
+ *
+ * Add a state to ctx->states at the END, only if no duplicate exists.
+ * Returns true if state was added, false if duplicate found (state is freed).
+ * Earlier states have better lexical order (DFS traversal order), so existing wins.
+ */
+static bool
+nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *state)
+{
+	RPRNFAState *s;
+	RPRNFAState *tail = NULL;
+
+	/* Check for duplicate and find tail */
+	for (s = ctx->states; s != NULL; s = s->next)
+	{
+		if (nfa_states_equal(winstate, s, state))
+		{
+			/*
+			 * Duplicate found - existing has better lexical order, discard
+			 * new
+			 */
+			nfa_state_free(winstate, state);
+			winstate->nfaStatesMerged++;
+			return false;
+		}
+		tail = s;
+	}
+
+	/* No duplicate, add at end */
+	state->next = NULL;
+	if (tail == NULL)
+		ctx->states = state;
+	else
+		tail->next = state;
+
+	return true;
+}
+
+/*
+ * nfa_add_matched_state
+ *
+ * Record a state that reached FIN, replacing any previous match.
+ *
+ * For SKIP PAST LAST ROW, also prune subsequent contexts whose start row
+ * falls within the match range, as they cannot produce output rows.
+ */
+static void
+nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
+					  RPRNFAState *state, int64 matchEndRow)
+{
+	if (ctx->matchedState != NULL)
+		nfa_state_free(winstate, ctx->matchedState);
+
+	ctx->matchedState = state;
+	state->next = NULL;
+	ctx->matchEndRow = matchEndRow;
+
+	/* Prune contexts that started within this match's range */
+	if (winstate->rpSkipTo == ST_PAST_LAST_ROW)
+	{
+		RPRNFAContext *nextCtx;
+		int64		skippedLen;
+
+		while (ctx->next != NULL &&
+			   ctx->next->matchStartRow <= matchEndRow)
+		{
+			nextCtx = ctx->next;
+			ctx->next = ctx->next->next;
+
+			Assert(nextCtx->lastProcessedRow >= nextCtx->matchStartRow);
+			skippedLen = nextCtx->lastProcessedRow - nextCtx->matchStartRow + 1;
+			nfa_record_context_skipped(winstate, skippedLen);
+
+			ExecRPRFreeContext(winstate, nextCtx);
+		}
+		if (ctx->next == NULL)
+			winstate->nfaContextTail = ctx;
+	}
+}
+
+/*
+ * nfa_context_alloc
+ *
+ * Allocate an NFA context, reusing from free list if available.
+ */
+static RPRNFAContext *
+nfa_context_alloc(WindowAggState *winstate)
+{
+	RPRNFAContext *ctx;
+
+	if (winstate->nfaContextFree != NULL)
+	{
+		ctx = winstate->nfaContextFree;
+		winstate->nfaContextFree = ctx->next;
+	}
+	else
+	{
+		/* Allocate in partition context for proper lifetime */
+		ctx = MemoryContextAlloc(winstate->partcontext, sizeof(RPRNFAContext));
+	}
+
+	ctx->next = NULL;
+	ctx->prev = NULL;
+	ctx->states = NULL;
+	ctx->matchStartRow = -1;
+	ctx->matchEndRow = -1;
+	ctx->lastProcessedRow = -1;
+	ctx->matchedState = NULL;
+
+	/* Initialize two-flag absorption design based on pattern */
+	ctx->hasAbsorbableState = winstate->rpPattern->isAbsorbable;
+	ctx->allStatesAbsorbable = winstate->rpPattern->isAbsorbable;
+
+	/* Update statistics */
+	winstate->nfaContextsActive++;
+	winstate->nfaContextsTotalCreated++;
+	if (winstate->nfaContextsActive > winstate->nfaContextsMax)
+		winstate->nfaContextsMax = winstate->nfaContextsActive;
+
+	return ctx;
+}
+
+/*
+ * nfa_unlink_context
+ *
+ * Remove a context from the doubly-linked active context list.
+ * Updates head (nfaContext) and tail (nfaContextTail) as needed.
+ */
+static void
+nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx)
+{
+	if (ctx->prev != NULL)
+		ctx->prev->next = ctx->next;
+	else
+		winstate->nfaContext = ctx->next;	/* was head */
+
+	if (ctx->next != NULL)
+		ctx->next->prev = ctx->prev;
+	else
+		winstate->nfaContextTail = ctx->prev;	/* was tail */
+
+	ctx->next = NULL;
+	ctx->prev = NULL;
+}
+
+/*
+ * nfa_update_length_stats
+ *
+ * Helper function to update min/max/total length statistics.
+ * Called when tracking match/mismatch/absorbed/skipped lengths.
+ */
+static void
+nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen)
+{
+	if (count == 1)
+	{
+		stats->min = newLen;
+		stats->max = newLen;
+	}
+	else
+	{
+		if (newLen < stats->min)
+			stats->min = newLen;
+		if (newLen > stats->max)
+			stats->max = newLen;
+	}
+	stats->total += newLen;
+}
+
+/*
+ * nfa_record_context_skipped
+ *
+ * Record a skipped context in statistics.
+ */
+static void
+nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen)
+{
+	winstate->nfaContextsSkipped++;
+	nfa_update_length_stats(winstate->nfaContextsSkipped,
+							&winstate->nfaSkippedLen,
+							skippedLen);
+}
+
+/*
+ * nfa_record_context_absorbed
+ *
+ * Record an absorbed context in statistics.
+ */
+static void
+nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen)
+{
+	winstate->nfaContextsAbsorbed++;
+	nfa_update_length_stats(winstate->nfaContextsAbsorbed,
+							&winstate->nfaAbsorbedLen,
+							absorbedLen);
+}
+
+/*
+ * nfa_update_absorption_flags
+ *
+ * Update context's absorption flags after state changes.
+ *
+ * Two flags control absorption behavior:
+ *   hasAbsorbableState: true if context has at least one absorbable state.
+ *     This flag is monotonic (true -> false only). Once all absorbable states
+ *     die, no new absorbable states can be created through transitions.
+ *   allStatesAbsorbable: true if ALL states in context are absorbable.
+ *     This flag is dynamic and can change false -> true when non-absorbable
+ *     states die off.
+ *
+ * Optimization: Once hasAbsorbableState becomes false, both flags remain false
+ * permanently, so we skip recalculation.
+ */
+static void
+nfa_update_absorption_flags(RPRNFAContext *ctx)
+{
+	RPRNFAState *state;
+	bool		hasAbsorbable = false;
+	bool		allAbsorbable = true;
+
+	/*
+	 * Optimization: Once hasAbsorbableState becomes false, it stays false. No
+	 * need to recalculate - both flags remain false permanently.
+	 */
+	if (!ctx->hasAbsorbableState)
+	{
+		ctx->allStatesAbsorbable = false;
+		return;
+	}
+
+	/* No states means no absorbable states */
+	if (ctx->states == NULL)
+	{
+		ctx->hasAbsorbableState = false;
+		ctx->allStatesAbsorbable = false;
+		return;
+	}
+
+	/*
+	 * 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
+	 * point.
+	 */
+	for (state = ctx->states; state != NULL; state = state->next)
+	{
+		if (state->isAbsorbable)
+			hasAbsorbable = true;
+		else
+			allAbsorbable = false;
+	}
+
+	ctx->hasAbsorbableState = hasAbsorbable;
+	ctx->allStatesAbsorbable = allAbsorbable;
+}
+
+/*
+ * nfa_states_covered
+ *
+ * Check if all states in newer context are "covered" by older context.
+ *
+ * A newer state is covered when older context has an absorbable state at the
+ * same pattern element (elemIdx) with count >= newer's count at that depth.
+ * The covering state must be absorbable because only absorbable states can
+ * guarantee to produce superset matches.
+ *
+ * If all newer states are covered, newer context's eventual matches will be
+ * a subset of older context's matches, making newer redundant.
+ */
+static bool
+nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *newer)
+{
+	RPRNFAState *newerState;
+
+	for (newerState = newer->states; newerState != NULL; newerState = newerState->next)
+	{
+		RPRNFAState *olderState;
+		RPRPatternElement *elem;
+		int			depth;
+		bool		found = false;
+
+		/* All states are absorbable (caller checks allStatesAbsorbable) */
+		elem = &pattern->elements[newerState->elemIdx];
+		depth = elem->depth;
+
+		for (olderState = older->states; olderState != NULL; olderState = olderState->next)
+		{
+			/* Covering state must also be absorbable */
+			if (olderState->isAbsorbable &&
+				olderState->elemIdx == newerState->elemIdx &&
+				olderState->counts[depth] >= newerState->counts[depth])
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * nfa_try_absorb_context
+ *
+ * Try to absorb ctx (newer) into an older in-progress context.
+ * Returns true if ctx was absorbed and freed.
+ *
+ * Absorption requires three conditions:
+ *   1. ctx must have all states absorbable (allStatesAbsorbable).
+ *      If ctx has any non-absorbable state, it may produce unique matches.
+ *   2. older must have at least one absorbable state (hasAbsorbableState).
+ *      Without absorbable states, older cannot cover newer's states.
+ *   3. All ctx states must be covered by older's absorbable states.
+ *      This ensures older will produce all matches that ctx would produce.
+ *
+ * Context list is ordered by creation time (oldest first via prev chain).
+ * Each row creates at most one context, so earlier contexts have smaller
+ * matchStartRow values.
+ */
+static void
+nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRNFAContext *older;
+
+	/* Early exit: ctx must have all states absorbable */
+	if (!ctx->allStatesAbsorbable)
+		return;
+
+	for (older = ctx->prev; older != NULL; older = older->prev)
+	{
+		/*
+		 * By invariant: ctx->prev chain is in creation order (oldest first),
+		 * and each row creates at most one context. So all contexts in this
+		 * chain have matchStartRow < ctx->matchStartRow.
+		 */
+
+		/* Older must also be in-progress */
+		if (older->states == NULL)
+			continue;
+
+		/* Older must have at least one absorbable state */
+		if (!older->hasAbsorbableState)
+			continue;
+
+		/* Check if all newer states are covered by older */
+		if (nfa_states_covered(pattern, older, ctx))
+		{
+			int64		absorbedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
+
+			ExecRPRFreeContext(winstate, ctx);
+			nfa_record_context_absorbed(winstate, absorbedLen);
+			return;
+		}
+	}
+}
+
+/*
+ * nfa_absorb_contexts
+ *
+ * Absorb redundant contexts to reduce memory usage and computation.
+ *
+ * For patterns like A+, newer contexts starting later will produce subset
+ * matches of older contexts with higher counts. By absorbing these redundant
+ * contexts early, we avoid duplicate work.
+ *
+ * Iterates from tail (newest) toward head (oldest) via prev chain.
+ * Only in-progress contexts (states != NULL) are candidates for absorption;
+ * completed contexts represent valid match results.
+ */
+static void
+nfa_absorb_contexts(WindowAggState *winstate)
+{
+	RPRNFAContext *ctx;
+	RPRNFAContext *nextCtx;
+
+	for (ctx = winstate->nfaContextTail; ctx != NULL; ctx = nextCtx)
+	{
+		nextCtx = ctx->prev;
+
+		/*
+		 * Only absorb in-progress contexts; completed contexts are valid
+		 * results
+		 */
+		if (ctx->states != NULL)
+			nfa_try_absorb_context(winstate, ctx);
+	}
+}
+
+/*
+ * nfa_eval_var_match
+ *
+ * Evaluate if a VAR element matches the current row.
+ * Undefined variables (varId >= defineVariableList length) default to TRUE.
+ */
+static bool
+nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
+				   bool *varMatched)
+{
+	/* This function should only be called for VAR elements */
+	Assert(RPRElemIsVar(elem));
+
+	if (varMatched == NULL)
+		return false;
+	if (elem->varId >= list_length(winstate->defineVariableList))
+		return true;
+	return varMatched[elem->varId];
+}
+
+/*
+ * nfa_match
+ *
+ * Match phase (convergence): evaluate VAR elements against current row.
+ * Only updates counts and removes dead states. Minimal transitions.
+ *
+ * For VAR elements:
+ *   - matched: count++, keep state (unless count > max)
+ *   - not matched: remove state (exit alternatives already exist from
+ *     previous advance when count >= min was satisfied)
+ *
+ * For simple VARs (min=max=1) followed by END:
+ *   - Advance to END and update group count before absorb phase
+ *   - This ensures absorption can compare states by group completion
+ *
+ * Non-VAR elements (ALT, END, FIN) are kept as-is for advance phase.
+ */
+static void
+nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+	RPRNFAState **prevPtr = &ctx->states;
+	RPRNFAState *state;
+	RPRNFAState *nextState;
+
+	/*
+	 * Evaluate VAR elements against current row. For simple VARs with END
+	 * next, advance to END and update group count inline so absorb phase can
+	 * compare states properly.
+	 */
+	for (state = ctx->states; state != NULL; state = nextState)
+	{
+		RPRPatternElement *elem = &elements[state->elemIdx];
+
+		nextState = state->next;
+
+		if (RPRElemIsVar(elem))
+		{
+			bool		matched;
+			int			depth = elem->depth;
+			int32		count = state->counts[depth];
+
+			matched = nfa_eval_var_match(winstate, elem, varMatched);
+
+			if (matched)
+			{
+				/* Increment count */
+				if (count < RPR_COUNT_MAX)
+					count++;
+
+				/* Max constraint should not be exceeded */
+				Assert(elem->max == RPR_QUANTITY_INF || count <= elem->max);
+
+				state->counts[depth] = count;
+
+				/*
+				 * For simple VAR (min=max=1) with END next, advance to END
+				 * and update group count inline. This keeps state in place,
+				 * preserving lexical order.
+				 */
+				if (elem->min == 1 && elem->max == 1 &&
+					RPRElemIsEnd(&elements[elem->next]))
+				{
+					RPRPatternElement *endElem = &elements[elem->next];
+					int			endDepth = endElem->depth;
+					int32		endCount = state->counts[endDepth];
+
+					Assert(count == 1);
+
+					/* Increment group count with overflow protection */
+					if (endCount < RPR_COUNT_MAX)
+						endCount++;
+
+					/*
+					 * END's max can never be exceeded here because
+					 * nfa_advance_end only loops when count < max, so
+					 * endCount entering inline advance is at most max-1, and
+					 * incrementing yields at most max.
+					 */
+					Assert(endElem->max == RPR_QUANTITY_INF ||
+						   endCount <= endElem->max);
+
+					state->elemIdx = elem->next;
+					state->counts[endDepth] = endCount;
+				}
+				/* else: stay at VAR for advance phase */
+			}
+			else
+			{
+				/*
+				 * Not matched - remove state. Exit alternatives were already
+				 * created by advance phase when count >= min was satisfied.
+				 */
+				*prevPtr = nextState;
+				nfa_state_free(winstate, state);
+				continue;
+			}
+		}
+		/* Non-VAR elements: keep as-is for advance phase */
+
+		prevPtr = &state->next;
+	}
+}
+
+/*
+ * nfa_route_to_elem
+ *
+ * Route state to next element. If VAR, add to ctx->states and process
+ * skip path if optional. Otherwise, continue epsilon expansion via recursion.
+ */
+static void
+nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
+				  RPRNFAState *state, RPRPatternElement *nextElem,
+				  int64 currentPos)
+{
+	if (RPRElemIsVar(nextElem))
+	{
+		RPRNFAState *skipState = NULL;
+
+		/* Create skip state before add_unique, which may free state */
+		if (RPRElemCanSkip(nextElem))
+			skipState = nfa_state_create(winstate, nextElem->next,
+										 state->counts, state->isAbsorbable);
+
+		nfa_add_state_unique(winstate, ctx, state);
+
+		if (skipState != NULL)
+			nfa_advance_state(winstate, ctx, skipState, currentPos);
+	}
+	else
+	{
+		nfa_advance_state(winstate, ctx, state, currentPos);
+	}
+}
+
+/*
+ * nfa_advance_alt
+ *
+ * Handle ALT element: expand all branches in lexical order via DFS.
+ */
+static void
+nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
+				RPRNFAState *state, RPRPatternElement *elem,
+				int64 currentPos)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+	RPRElemIdx	altIdx = elem->next;
+
+	while (altIdx >= 0 && altIdx < pattern->numElements)
+	{
+		RPRPatternElement *altElem = &elements[altIdx];
+		RPRNFAState *newState;
+
+		/* Stop if element is outside ALT scope (not a branch) */
+		if (altElem->depth <= elem->depth)
+			break;
+
+		/* Create independent state for each branch */
+		newState = nfa_state_create(winstate, altIdx,
+									state->counts, state->isAbsorbable);
+
+		/* Recursively process this branch before next */
+		nfa_advance_state(winstate, ctx, newState, currentPos);
+		altIdx = altElem->jump;
+	}
+
+	nfa_state_free(winstate, state);
+}
+
+/*
+ * nfa_advance_begin
+ *
+ * Handle BEGIN element: group entry logic.
+ * BEGIN is only visited at initial group entry (count is always 0).
+ * If min=0, creates a skip path past the group.
+ * Loop-back from END goes directly to first child, bypassing BEGIN.
+ */
+static void
+nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
+				  RPRNFAState *state, RPRPatternElement *elem,
+				  int64 currentPos)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+	RPRNFAState *skipState = NULL;
+
+	state->counts[elem->depth] = 0;
+
+	/* Optional group: create skip path (but don't route yet) */
+	if (elem->min == 0)
+	{
+		skipState = nfa_state_create(winstate, elem->jump,
+									 state->counts, state->isAbsorbable);
+	}
+
+	if (skipState != NULL && RPRElemIsReluctant(elem))
+	{
+		RPRNFAState *savedMatch = ctx->matchedState;
+
+		/* Reluctant: skip first (prefer fewer iterations), enter second */
+		nfa_route_to_elem(winstate, ctx, skipState,
+						  &elements[elem->jump], currentPos);
+
+		/*
+		 * If skip path reached FIN, shortest match is found. Skip group entry
+		 * to prevent longer matches.
+		 */
+		if (ctx->matchedState != savedMatch)
+		{
+			nfa_state_free(winstate, state);
+			return;
+		}
+
+		state->elemIdx = elem->next;
+		nfa_route_to_elem(winstate, ctx, state,
+						  &elements[state->elemIdx], currentPos);
+	}
+	else
+	{
+		/* Greedy: enter first, skip second */
+		state->elemIdx = elem->next;
+		nfa_route_to_elem(winstate, ctx, state,
+						  &elements[state->elemIdx], currentPos);
+
+		if (skipState != NULL)
+		{
+			nfa_route_to_elem(winstate, ctx, skipState,
+							  &elements[elem->jump], currentPos);
+		}
+	}
+}
+
+/*
+ * nfa_advance_end
+ *
+ * Handle END element: group repetition logic.
+ * Decides whether to loop back or exit based on count vs min/max.
+ */
+static void
+nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
+				RPRNFAState *state, RPRPatternElement *elem,
+				int64 currentPos)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+	int			depth = elem->depth;
+	int32		count = state->counts[depth];
+
+	if (count < elem->min)
+	{
+		RPRPatternElement *jumpElem;
+		RPRNFAState *ffState = NULL;
+
+		/* Snapshot state for ff path before modifying for loop-back */
+		if (RPRElemCanEmptyLoop(elem))
+			ffState = nfa_state_create(winstate, state->elemIdx,
+									   state->counts, state->isAbsorbable);
+
+		/* Loop back for real matches (primary path) */
+		for (int d = depth + 1; d < pattern->maxDepth; d++)
+			state->counts[d] = 0;
+		state->elemIdx = elem->jump;
+		jumpElem = &elements[state->elemIdx];
+		nfa_route_to_elem(winstate, ctx, state, jumpElem,
+						  currentPos);
+
+		/*
+		 * Fast-forward fallback for nullable bodies.  E.g. (A?){2,3} when A
+		 * doesn't match: the loop-back produces empty iterations that cycle
+		 * detection would kill.  Instead, exit directly treating all
+		 * remaining required iterations as empty.  Route to elem->next (not
+		 * nfa_advance_end) to avoid creating competing greedy/reluctant loop
+		 * states.
+		 */
+		if (ffState != NULL)
+		{
+			RPRPatternElement *nextElem;
+
+			ffState->counts[depth] = 0;
+			ffState->elemIdx = elem->next;
+			nextElem = &elements[ffState->elemIdx];
+
+			/* END->END: increment outer END's count */
+			if (RPRElemIsEnd(nextElem) &&
+				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
+				ffState->counts[nextElem->depth]++;
+
+			nfa_route_to_elem(winstate, ctx, ffState, nextElem,
+							  currentPos);
+		}
+	}
+	else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
+	{
+		/* Must exit: reached max iterations. */
+		RPRPatternElement *nextElem;
+
+		state->counts[depth] = 0;
+		state->elemIdx = elem->next;
+		nextElem = &elements[state->elemIdx];
+
+		/* END->END: increment outer END's count */
+		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
+			state->counts[nextElem->depth]++;
+
+		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
+	}
+	else
+	{
+		/*
+		 * Between min and max (with at least one iteration) - can exit or
+		 * loop. Greedy: loop first (prefer more iterations). Reluctant: exit
+		 * first (prefer fewer iterations).
+		 */
+		RPRNFAState *exitState;
+		RPRPatternElement *jumpElem;
+		RPRPatternElement *nextElem;
+
+		/*
+		 * Create exit state first (need original counts before modifying
+		 * state)
+		 */
+		exitState = nfa_state_create(winstate, elem->next,
+									 state->counts, state->isAbsorbable);
+		exitState->counts[depth] = 0;
+		nextElem = &elements[exitState->elemIdx];
+
+		/* END->END: increment outer END's count */
+		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
+			exitState->counts[nextElem->depth]++;
+
+		/* Prepare loop state */
+		for (int d = depth + 1; d < pattern->maxDepth; d++)
+			state->counts[d] = 0;
+		state->elemIdx = elem->jump;
+		jumpElem = &elements[state->elemIdx];
+
+		if (RPRElemIsReluctant(elem))
+		{
+			RPRNFAState *savedMatch = ctx->matchedState;
+
+			/* Exit first (preferred for reluctant) */
+			nfa_route_to_elem(winstate, ctx, exitState, nextElem,
+							  currentPos);
+
+			/*
+			 * If exit path reached FIN, shortest match is found. Skip loop to
+			 * prevent longer matches from replacing it.
+			 */
+			if (ctx->matchedState != savedMatch)
+			{
+				nfa_state_free(winstate, state);
+				return;
+			}
+
+			/* Loop second */
+			nfa_route_to_elem(winstate, ctx, state, jumpElem,
+							  currentPos);
+		}
+		else
+		{
+			/* Loop first (preferred for greedy) */
+			nfa_route_to_elem(winstate, ctx, state, jumpElem,
+							  currentPos);
+			/* Exit second */
+			nfa_route_to_elem(winstate, ctx, exitState, nextElem,
+							  currentPos);
+		}
+	}
+}
+
+/*
+ * nfa_advance_var
+ *
+ * Handle VAR element: loop/exit transitions.
+ * After match phase, all VAR states have matched - decide next action.
+ */
+static void
+nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
+				RPRNFAState *state, RPRPatternElement *elem,
+				int64 currentPos)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+	int			depth = elem->depth;
+	int32		count = state->counts[depth];
+	bool		canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max);
+	bool		canExit = (count >= elem->min);
+
+	/* After a successful match, count >= 1, so at least one must be true */
+	Assert(canLoop || canExit);
+
+	if (canLoop && canExit)
+	{
+		/*
+		 * Both loop and exit possible. Greedy: loop first (prefer longer
+		 * match). Reluctant: exit first (prefer shorter match).
+		 */
+		RPRNFAState *cloneState;
+		RPRPatternElement *nextElem;
+		bool		reluctant = RPRElemIsReluctant(elem);
+
+		/*
+		 * Clone state for the second-priority path. For greedy, clone is the
+		 * loop state; for reluctant, clone is the exit state.
+		 */
+		if (reluctant)
+		{
+			RPRNFAState *savedMatch = ctx->matchedState;
+
+			/* Clone for exit, original stays for loop */
+			cloneState = nfa_state_create(winstate, elem->next,
+										  state->counts, state->isAbsorbable);
+			cloneState->counts[depth] = 0;
+			nextElem = &elements[cloneState->elemIdx];
+
+			/* When exiting directly to an outer END, increment its count */
+			if (RPRElemIsEnd(nextElem))
+			{
+				if (cloneState->counts[nextElem->depth] < RPR_COUNT_MAX)
+					cloneState->counts[nextElem->depth]++;
+			}
+
+			/* Exit first (preferred for reluctant) */
+			nfa_route_to_elem(winstate, ctx, cloneState, nextElem,
+							  currentPos);
+
+			/*
+			 * If exit path reached FIN, the shortest match is found. Skip
+			 * loop state to prevent longer matches from replacing it.
+			 */
+			if (ctx->matchedState != savedMatch)
+			{
+				nfa_state_free(winstate, state);
+				return;
+			}
+
+			/* Loop second */
+			nfa_add_state_unique(winstate, ctx, state);
+		}
+		else
+		{
+			/* Clone for loop, original used for exit */
+			cloneState = nfa_state_create(winstate, state->elemIdx,
+										  state->counts, state->isAbsorbable);
+
+			/* Loop first (preferred for greedy) */
+			nfa_add_state_unique(winstate, ctx, cloneState);
+
+			/* Exit second */
+			state->counts[depth] = 0;
+			state->elemIdx = elem->next;
+			nextElem = &elements[state->elemIdx];
+
+			/*
+			 * When exiting directly to an outer END, increment its iteration
+			 * count.  Simple VARs (min=max=1) handle this via inline advance
+			 * in nfa_match, but quantified VARs bypass that path.
+			 */
+			if (RPRElemIsEnd(nextElem))
+			{
+				if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+					state->counts[nextElem->depth]++;
+			}
+
+			nfa_route_to_elem(winstate, ctx, state, nextElem,
+							  currentPos);
+		}
+	}
+	else if (canLoop)
+	{
+		/* Loop only: keep state as-is */
+		nfa_add_state_unique(winstate, ctx, state);
+	}
+	else if (canExit)
+	{
+		/* Exit only: advance to next element */
+		RPRPatternElement *nextElem;
+
+		state->counts[depth] = 0;
+		state->elemIdx = elem->next;
+		nextElem = &elements[state->elemIdx];
+
+		/* See comment above: increment outer END count for quantified VARs */
+		if (RPRElemIsEnd(nextElem))
+		{
+			if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+				state->counts[nextElem->depth]++;
+		}
+
+		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
+	}
+}
+
+/*
+ * nfa_advance_state
+ *
+ * Recursively process a single state through epsilon transitions.
+ * DFS traversal ensures states are added to ctx->states in lexical order.
+ */
+static void
+nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+				  RPRNFAState *state, int64 currentPos)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elem;
+
+	Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements);
+
+	/* Cycle detection: if this elemIdx was already visited in this DFS, bail */
+	if (winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] &
+		((bitmapword) 1 << BITNUM(state->elemIdx)))
+	{
+		nfa_state_free(winstate, state);
+		return;
+	}
+	winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
+		((bitmapword) 1 << BITNUM(state->elemIdx));
+
+	elem = &pattern->elements[state->elemIdx];
+
+	switch (elem->varId)
+	{
+		case RPR_VARID_FIN:
+			/* FIN: record match */
+			nfa_add_matched_state(winstate, ctx, state, currentPos);
+			break;
+
+		case RPR_VARID_ALT:
+			nfa_advance_alt(winstate, ctx, state, elem, currentPos);
+			break;
+
+		case RPR_VARID_BEGIN:
+			nfa_advance_begin(winstate, ctx, state, elem, currentPos);
+			break;
+
+		case RPR_VARID_END:
+			nfa_advance_end(winstate, ctx, state, elem, currentPos);
+			break;
+
+		default:
+			/* VAR element */
+			nfa_advance_var(winstate, ctx, state, elem, currentPos);
+			break;
+	}
+}
+
+/*
+ * nfa_advance
+ *
+ * Advance phase (divergence): transition from all surviving states.
+ * Called after match phase with matched VAR states, or at context creation
+ * for initial epsilon expansion (with currentPos = startPos - 1).
+ *
+ * Processes states in order, using recursive DFS to maintain lexical order.
+ */
+static void
+nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
+{
+	RPRNFAState *states = ctx->states;
+	RPRNFAState *state;
+
+	ctx->states = NULL;			/* Will rebuild */
+
+	/* Process each state in lexical order (DFS order from previous advance) */
+	while (states != NULL)
+	{
+		RPRNFAState *savedMatchedState = ctx->matchedState;
+
+		/* Clear visited bitmap before each state's DFS expansion */
+		memset(winstate->nfaVisitedElems, 0,
+			   sizeof(bitmapword) * winstate->nfaVisitedNWords);
+
+		state = states;
+		states = states->next;
+		state->next = NULL;
+
+		nfa_advance_state(winstate, ctx, state, currentPos);
+
+		/*
+		 * Early termination: if a FIN was newly reached in this advance,
+		 * remaining old states have worse lexical order and can be pruned.
+		 * Only check for new FIN arrivals (not ones from previous rows).
+		 */
+		if (ctx->matchedState != savedMatchedState && states != NULL)
+		{
+			nfa_state_free_list(winstate, states);
+			break;
+		}
+	}
+}
+
+
+/***********************************************************************
+ * API exposed to nodeWindowAgg.c
+ ***********************************************************************/
+
+/*
+ * ExecRPRStartContext
+ *
+ * Start a new match context at given position.
+ * Initializes context, state absorption flags, and performs initial advance
+ * to expand epsilon transitions (ALT branches, optional elements).
+ * Adds context to the tail of winstate->nfaContext list.
+ */
+RPRNFAContext *
+ExecRPRStartContext(WindowAggState *winstate, int64 startPos)
+{
+	RPRNFAContext *ctx;
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elem;
+
+	ctx = nfa_context_alloc(winstate);
+	ctx->matchStartRow = startPos;
+	ctx->states = nfa_state_alloc(winstate);	/* initial state at elem 0 */
+
+	elem = &pattern->elements[0];
+
+	if (RPRElemIsAbsorbableBranch(elem))
+	{
+		ctx->states->isAbsorbable = true;
+	}
+	else
+	{
+		ctx->hasAbsorbableState = false;
+		ctx->allStatesAbsorbable = false;
+		ctx->states->isAbsorbable = false;
+	}
+
+	/* Add to tail of active context list (doubly-linked, oldest-first) */
+	ctx->prev = winstate->nfaContextTail;
+	ctx->next = NULL;
+	if (winstate->nfaContextTail != NULL)
+		winstate->nfaContextTail->next = ctx;
+	else
+		winstate->nfaContext = ctx; /* first context becomes head */
+	winstate->nfaContextTail = ctx;
+
+	/*
+	 * Initial advance (divergence): expand ALT branches and create exit
+	 * states for VAR elements with min=0. This prepares the context for the
+	 * first row's match phase.
+	 *
+	 * Use startPos - 1 as currentPos since no row has been consumed yet. If
+	 * FIN is reached via epsilon transitions, matchEndRow = startPos - 1
+	 * which is less than matchStartRow, resulting in UNMATCHED treatment.
+	 */
+	nfa_advance(winstate, ctx, startPos - 1);
+
+	return ctx;
+}
+
+/*
+ * ExecRPRGetHeadContext
+ *
+ * Return the head context if its start position matches pos.
+ * Returns NULL if no context exists or head doesn't match pos.
+ */
+RPRNFAContext *
+ExecRPRGetHeadContext(WindowAggState *winstate, int64 pos)
+{
+	RPRNFAContext *ctx = winstate->nfaContext;
+
+	/*
+	 * Contexts are sorted by matchStartRow ascending.  If the head context
+	 * doesn't match pos, no context exists for this position.
+	 */
+	if (ctx == NULL || ctx->matchStartRow != pos)
+		return NULL;
+
+	return ctx;
+}
+
+/*
+ * ExecRPRFreeContext
+ *
+ * Unlink context from active list and return it to free list.
+ * Also frees any states in the context.
+ */
+void
+ExecRPRFreeContext(WindowAggState *winstate, RPRNFAContext *ctx)
+{
+	/* Unlink from active list first */
+	nfa_unlink_context(winstate, ctx);
+
+	/* Update statistics */
+	winstate->nfaContextsActive--;
+
+	if (ctx->states != NULL)
+		nfa_state_free_list(winstate, ctx->states);
+	if (ctx->matchedState != NULL)
+		nfa_state_free(winstate, ctx->matchedState);
+
+	ctx->states = NULL;
+	ctx->matchedState = NULL;
+	ctx->next = winstate->nfaContextFree;
+	winstate->nfaContextFree = ctx;
+}
+
+/*
+ * ExecRPRRecordContextSuccess
+ *
+ * Record a successful context in statistics.
+ */
+void
+ExecRPRRecordContextSuccess(WindowAggState *winstate, int64 matchLen)
+{
+	winstate->nfaMatchesSucceeded++;
+	nfa_update_length_stats(winstate->nfaMatchesSucceeded,
+							&winstate->nfaMatchLen,
+							matchLen);
+}
+
+/*
+ * ExecRPRRecordContextFailure
+ *
+ * Record a failed context in statistics.
+ * If failedLen == 1, count as pruned (failed on first row).
+ * If failedLen > 1, count as mismatched and update length stats.
+ */
+void
+ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
+{
+	if (failedLen == 1)
+	{
+		winstate->nfaContextsPruned++;
+	}
+	else
+	{
+		winstate->nfaMatchesFailed++;
+		nfa_update_length_stats(winstate->nfaMatchesFailed,
+								&winstate->nfaFailLen,
+								failedLen);
+	}
+}
+
+/*
+ * ExecRPRProcessRow
+ *
+ * Process all contexts for one row:
+ *   1. Match all contexts (convergence) - evaluate VARs, prune dead states
+ *   2. Absorb redundant contexts - ideal timing after convergence
+ *   3. Advance all contexts (divergence) - create new states for next row
+ */
+void
+ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
+				  bool hasLimitedFrame, int64 frameOffset)
+{
+	RPRNFAContext *ctx;
+	bool	   *varMatched = winstate->nfaVarMatched;
+
+	/*
+	 * Phase 1: Match all contexts (convergence).  Evaluate VAR elements,
+	 * update counts, remove dead states.
+	 */
+	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+	{
+		if (ctx->states == NULL)
+			continue;
+
+		/* Check frame boundary - finalize if exceeded */
+		if (hasLimitedFrame)
+		{
+			int64		ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
+
+			if (currentPos >= ctxFrameEnd)
+			{
+				/* Frame boundary exceeded: force mismatch */
+				nfa_match(winstate, ctx, NULL);
+				continue;
+			}
+		}
+
+		nfa_match(winstate, ctx, varMatched);
+		ctx->lastProcessedRow = currentPos;
+	}
+
+	/*
+	 * Phase 2: Absorb redundant contexts.  After match phase, states have
+	 * converged - ideal for absorption.  First update absorption flags that
+	 * may have changed due to state removal.
+	 */
+	if (winstate->rpPattern->isAbsorbable)
+	{
+		for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+			nfa_update_absorption_flags(ctx);
+
+		nfa_absorb_contexts(winstate);
+	}
+
+	/*
+	 * Phase 3: Advance all contexts (divergence).  Create new states
+	 * (loop/exit) from surviving matched states.
+	 */
+	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+	{
+		if (ctx->states == NULL)
+			continue;
+
+		/*
+		 * Phase 1 already handled frame boundary exceeded contexts by forcing
+		 * mismatch (nfa_match with NULL), which removes all states (all
+		 * states are at VAR positions after advance). So any surviving
+		 * context here must be within its frame boundary.
+		 */
+		Assert(!hasLimitedFrame ||
+			   currentPos < ctx->matchStartRow + frameOffset + 1);
+
+		nfa_advance(winstate, ctx, currentPos);
+	}
+}
+
+/*
+ * ExecRPRCleanupDeadContexts
+ *
+ * Remove contexts that have failed (no active states and no match).
+ * These are contexts that failed during normal processing and should be
+ * counted as pruned (if length 1) or mismatched (if length > 1).
+ */
+void
+ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
+{
+	RPRNFAContext *ctx;
+	RPRNFAContext *next;
+
+	for (ctx = winstate->nfaContext; ctx != NULL; ctx = next)
+	{
+		next = ctx->next;
+
+		/* Skip the target context and contexts still processing */
+		if (ctx == excludeCtx || ctx->states != NULL)
+			continue;
+
+		/* Skip successfully matched contexts (will be handled by SKIP logic) */
+		if (ctx->matchEndRow >= ctx->matchStartRow)
+			continue;
+
+		/*
+		 * This is a failed context - count and remove it. Only count if it
+		 * actually processed its start row. Contexts created for
+		 * beyond-partition rows are silently removed.
+		 */
+		if (ctx->lastProcessedRow >= ctx->matchStartRow)
+		{
+			int64		failedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
+
+			ExecRPRRecordContextFailure(winstate, failedLen);
+		}
+		/* else: context was never processed (beyond-partition), just remove */
+
+		ExecRPRFreeContext(winstate, ctx);
+	}
+}
+
+/*
+ * ExecRPRFinalizeAllContexts
+ *
+ * Finalize all active contexts when partition ends.
+ * Match with NULL to force mismatch, then advance to process epsilon transitions.
+ */
+void
+ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos)
+{
+	RPRNFAContext *ctx;
+
+	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+	{
+		if (ctx->states != NULL)
+		{
+			nfa_match(winstate, ctx, NULL);
+			nfa_advance(winstate, ctx, lastPos);
+		}
+	}
+}
diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build
index dc45be0b2ce..0ff4a5b1d83 100644
--- a/src/backend/executor/meson.build
+++ b/src/backend/executor/meson.build
@@ -13,6 +13,7 @@ backend_sources += files(
   'execParallel.c',
   'execPartition.c',
   'execProcnode.c',
+  'execRPR.c',
   'execReplication.c',
   'execSRF.c',
   'execScan.c',
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 76c1a6bebef..0a9ba5bd4e7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -39,6 +39,7 @@
 #include "catalog/pg_collation_d.h"
 #include "catalog/pg_proc.h"
 #include "executor/executor.h"
+#include "executor/execRPR.h"
 #include "executor/nodeWindowAgg.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -243,89 +244,23 @@ static void put_notnull_info(WindowObject winobj,
 							 int64 pos, int argno, bool isnull);
 static void attno_map(Node *node);
 static bool attno_map_walker(Node *node, void *context);
-static int	row_is_in_reduced_frame(WindowObject winobj, int64 pos);
+
 static bool rpr_is_defined(WindowAggState *winstate);
+static int	row_is_in_reduced_frame(WindowObject winobj, int64 pos);
 
 static void create_reduced_frame_map(WindowAggState *winstate);
+static void clear_reduced_frame_map(WindowAggState *winstate);
 static int	get_reduced_frame_map(WindowAggState *winstate, int64 pos);
 static void register_reduced_frame_map(WindowAggState *winstate, int64 pos,
 									   int val);
-static void clear_reduced_frame_map(WindowAggState *winstate);
 static void update_reduced_frame(WindowObject winobj, int64 pos);
 
 static void check_rpr_navigation(Node *node, bool is_prev);
 static bool rpr_navigation_walker(Node *node, void *context);
 
-/* Forward declarations - NFA row processing */
-static void nfa_process_row(WindowAggState *winstate, int64 currentPos,
-							bool hasLimitedFrame, int64 frameOffset);
-
-/* Forward declarations - NFA state management */
-static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
-static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
-static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
-static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
-									 int32 *counts, bool sourceAbsorbable);
-static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
-							 RPRNFAState *s2);
-static bool nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
-								 RPRNFAState *state);
-static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
-								  RPRNFAState *state, int64 matchEndRow);
-
-/* Forward declarations - NFA context management */
-static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate);
-static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
-static void nfa_context_free(WindowAggState *winstate, RPRNFAContext *ctx);
-static RPRNFAContext *nfa_start_context(WindowAggState *winstate, int64 startPos);
-static RPRNFAContext *nfa_get_head_context(WindowAggState *winstate, int64 pos);
-
-/* Forward declarations - NFA statistics */
-static void nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen);
-static void nfa_record_context_success(WindowAggState *winstate, int64 matchLen);
-static void nfa_record_context_failure(WindowAggState *winstate, int64 failedLen);
-static void nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen);
-static void nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen);
-
 /* Forward declarations - NFA row evaluation */
 static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
 
-/* Forward declarations - NFA context lifecycle */
-static void nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx);
-static void nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos);
-
-/* Forward declarations - NFA absorption */
-static void nfa_update_absorption_flags(RPRNFAContext *ctx);
-static bool nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older,
-							   RPRNFAContext *newer);
-static bool nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx);
-static void nfa_absorb_contexts(WindowAggState *winstate);
-
-/* Forward declarations - NFA match and advance */
-static inline bool nfa_eval_var_match(WindowAggState *winstate,
-									  RPRPatternElement *elem, bool *varMatched);
-static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
-					  bool *varMatched);
-static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
-							  RPRNFAState *state, int64 currentPos);
-static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
-							  RPRNFAState *state, RPRPatternElement *nextElem,
-							  int64 currentPos);
-static void nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
-							RPRNFAState *state, RPRPatternElement *elem,
-							int64 currentPos);
-static void nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
-							  RPRNFAState *state, RPRPatternElement *elem,
-							  int64 currentPos);
-static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
-							RPRNFAState *state, RPRPatternElement *elem,
-							int64 currentPos);
-static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
-							RPRNFAState *state, RPRPatternElement *elem,
-							int64 currentPos);
-static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
-						int64 currentPos);
-
 /*
  * Not null info bit array consists of 2-bit items
  */
@@ -343,10 +278,6 @@ static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
 /* calculate shift bits */
 #define	NN_SHIFT(pos)	((pos) % NN_ITEM_PER_VAR) * NN_BITS_PER_MEMBER
 
-/* Bitmap macros for NFA cycle detection (cf. bitmapset.c, tidbitmap.c) */
-#define WORDNUM(x)	((x) / BITS_PER_BITMAPWORD)
-#define BITNUM(x)	((x) % BITS_PER_BITMAPWORD)
-
 /*
  * initialize_windowaggregate
  * parallel to initialize_aggregates in nodeAgg.c
@@ -4002,822 +3933,195 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
 	mbp[bpos] = mb;
 }
 
-/***********************************************************************
- * API exposed to window functions
- ***********************************************************************/
-
+/*
+ * rpr_is_defined
+ * return true if Row pattern recognition is defined.
+ */
+static bool
+rpr_is_defined(WindowAggState *winstate)
+{
+	return winstate->rpPattern != NULL;
+}
 
 /*
- * WinCheckAndInitializeNullTreatment
- *		Check null treatment clause and sets ignore_nulls
+ * -----------------
+ * row_is_in_reduced_frame
+ * Determine whether a row is in the current row's reduced window frame
+ * according to row pattern matching
  *
- * Window functions should call this to check if they are being called with
- * a null treatment clause when they don't allow it, or to set ignore_nulls.
+ * The row must has been already determined that it is in a full window frame
+ * and fetched it into slot.
+ *
+ * Returns:
+ * = 0, RPR is not defined.
+ * >0, if the row is the first in the reduced frame. Return the number of rows
+ * in the reduced frame.
+ * -1, if the row is unmatched row
+ * -2, if the row is in the reduced frame but needed to be skipped because of
+ * AFTER MATCH SKIP PAST LAST ROW
+ * -----------------
  */
-void
-WinCheckAndInitializeNullTreatment(WindowObject winobj,
-								   bool allowNullTreatment,
-								   FunctionCallInfo fcinfo)
+static int
+row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 {
-	Assert(WindowObjectIsValid(winobj));
-	if (winobj->ignore_nulls != NO_NULLTREATMENT && !allowNullTreatment)
+	WindowAggState *winstate = winobj->winstate;
+	int			state;
+	int			rtn;
+
+	if (!rpr_is_defined(winstate))
 	{
-		const char *funcname = get_func_name(fcinfo->flinfo->fn_oid);
+		/*
+		 * RPR is not defined. Assume that we are always in the the reduced
+		 * window frame.
+		 */
+		rtn = 0;
+#ifdef RPR_DEBUG
+		printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
+			   rtn, pos);
+#endif
+		return rtn;
+	}
 
-		if (!funcname)
-			elog(ERROR, "could not get function name");
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("function %s does not allow RESPECT/IGNORE NULLS",
-						funcname)));
+	state = get_reduced_frame_map(winstate, pos);
+
+	if (state == RF_NOT_DETERMINED)
+	{
+		update_frameheadpos(winstate);
+		update_reduced_frame(winobj, pos);
 	}
-	else if (winobj->ignore_nulls == PARSER_IGNORE_NULLS)
-		winobj->ignore_nulls = IGNORE_NULLS;
+
+	state = get_reduced_frame_map(winstate, pos);
+
+	switch (state)
+	{
+			int64		i;
+			int			num_reduced_rows;
+
+		case RF_FRAME_HEAD:
+			num_reduced_rows = 1;
+			for (i = pos + 1;
+				 get_reduced_frame_map(winstate, i) == RF_SKIPPED; i++)
+				num_reduced_rows++;
+			rtn = num_reduced_rows;
+			break;
+
+		case RF_SKIPPED:
+			rtn = -2;
+			break;
+
+		case RF_UNMATCHED:
+			rtn = -1;
+			break;
+
+		default:
+			elog(ERROR, "unrecognized state: %d at: " INT64_FORMAT,
+				 state, pos);
+			break;
+	}
+
+#ifdef RPR_DEBUG
+	printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
+		   rtn, pos);
+#endif
+	return rtn;
 }
 
+#define REDUCED_FRAME_MAP_INIT_SIZE	1024L
+
 /*
- * WinGetPartitionLocalMemory
- *		Get working memory that lives till end of partition processing
- *
- * On first call within a given partition, this allocates and zeroes the
- * requested amount of space.  Subsequent calls just return the same chunk.
- *
- * Memory obtained this way is normally used to hold state that should be
- * automatically reset for each new partition.  If a window function wants
- * to hold state across the whole query, fcinfo->fn_extra can be used in the
- * usual way for that.
+ * create_reduced_frame_map
+ * Create reduced frame map
  */
-void *
-WinGetPartitionLocalMemory(WindowObject winobj, Size sz)
+static void
+create_reduced_frame_map(WindowAggState *winstate)
 {
-	Assert(WindowObjectIsValid(winobj));
-	if (winobj->localmem == NULL)
-		winobj->localmem =
-			MemoryContextAllocZero(winobj->winstate->partcontext, sz);
-	return winobj->localmem;
+	winstate->reduced_frame_map =
+		MemoryContextAlloc(winstate->partcontext,
+						   REDUCED_FRAME_MAP_INIT_SIZE);
+	winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE;
+	clear_reduced_frame_map(winstate);
 }
 
 /*
- * WinGetCurrentPosition
- *		Return the current row's position (counting from 0) within the current
- *		partition.
+ * clear_reduced_frame_map
+ * Clear reduced frame map
  */
-int64
-WinGetCurrentPosition(WindowObject winobj)
+static void
+clear_reduced_frame_map(WindowAggState *winstate)
 {
-	Assert(WindowObjectIsValid(winobj));
-	return winobj->winstate->currentpos;
+	Assert(winstate->reduced_frame_map != NULL);
+	MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
+		   winstate->alloc_sz);
 }
 
 /*
- * WinGetPartitionRowCount
- *		Return total number of rows contained in the current partition.
- *
- * Note: this is a relatively expensive operation because it forces the
- * whole partition to be "spooled" into the tuplestore at once.  Once
- * executed, however, additional calls within the same partition are cheap.
+ * get_reduced_frame_map
+ * Get reduced frame map specified by pos
  */
-int64
-WinGetPartitionRowCount(WindowObject winobj)
+static int
+get_reduced_frame_map(WindowAggState *winstate, int64 pos)
 {
-	Assert(WindowObjectIsValid(winobj));
-	spool_tuples(winobj->winstate, -1);
-	return winobj->winstate->spooled_rows;
+	Assert(winstate->reduced_frame_map != NULL);
+	Assert(pos >= 0);
+
+	/*
+	 * If pos is not in the reduced frame map, it means that any info
+	 * regarding the pos has not been registered yet. So we return
+	 * RF_NOT_DETERMINED.
+	 */
+	if (pos >= winstate->alloc_sz)
+		return RF_NOT_DETERMINED;
+
+	return winstate->reduced_frame_map[pos];
 }
 
 /*
- * WinSetMarkPosition
- *		Set the "mark" position for the window object, which is the oldest row
- *		number (counting from 0) it is allowed to fetch during all subsequent
- *		operations within the current partition.
- *
- * Window functions do not have to call this, but are encouraged to move the
- * mark forward when possible to keep the tuplestore size down and prevent
- * having to spill rows to disk.
+ * register_reduced_frame_map
+ * Add/replace reduced frame map member at pos.
+ * If there's no enough space, expand the map.
  */
-void
-WinSetMarkPosition(WindowObject winobj, int64 markpos)
+static void
+register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val)
 {
-	WindowAggState *winstate;
+	int64		realloc_sz;
 
-	Assert(WindowObjectIsValid(winobj));
-	winstate = winobj->winstate;
+	Assert(winstate->reduced_frame_map != NULL);
 
-	if (markpos < winobj->markpos)
-		elog(ERROR, "cannot move WindowObject's mark position backward");
-	tuplestore_select_read_pointer(winstate->buffer, winobj->markptr);
-	if (markpos > winobj->markpos)
-	{
-		tuplestore_skiptuples(winstate->buffer,
-							  markpos - winobj->markpos,
-							  true);
-		winobj->markpos = markpos;
-	}
-	tuplestore_select_read_pointer(winstate->buffer, winobj->readptr);
-	if (markpos > winobj->seekpos)
+	if (pos < 0)
+		elog(ERROR, "wrong pos: " INT64_FORMAT, pos);
+
+	while (pos > winstate->alloc_sz - 1)
 	{
-		tuplestore_skiptuples(winstate->buffer,
-							  markpos - winobj->seekpos,
-							  true);
-		winobj->seekpos = markpos;
+		realloc_sz = winstate->alloc_sz * 2;
+
+		winstate->reduced_frame_map =
+			repalloc(winstate->reduced_frame_map, realloc_sz);
+
+		MemSet(winstate->reduced_frame_map + winstate->alloc_sz,
+			   RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz);
+
+		winstate->alloc_sz = realloc_sz;
 	}
+
+	winstate->reduced_frame_map[pos] = val;
 }
 
 /*
- * WinRowsArePeers
- *		Compare two rows (specified by absolute position in partition) to see
- *		if they are equal according to the ORDER BY clause.
+ * update_reduced_frame
+ *		Update reduced frame info using multi-context NFA pattern matching.
  *
- * NB: this does not consider the window frame mode.
+ * Maintains multiple NFA contexts simultaneously, one for each potential
+ * match start position. This allows sharing row evaluations across contexts,
+ * avoiding redundant DEFINE clause evaluations when rewinding for SKIP TO
+ * NEXT ROW mode.
+ *
+ * Key optimizations:
+ * - Row evaluations (expensive DEFINE clauses) happen only once per row
+ * - All active contexts share the same evaluation results
+ * - Contexts persist across calls, enabling O(n) DEFINE evaluations
  */
-bool
-WinRowsArePeers(WindowObject winobj, int64 pos1, int64 pos2)
-{
-	WindowAggState *winstate;
-	WindowAgg  *node;
-	TupleTableSlot *slot1;
-	TupleTableSlot *slot2;
-	bool		res;
-
-	Assert(WindowObjectIsValid(winobj));
-	winstate = winobj->winstate;
-	node = (WindowAgg *) winstate->ss.ps.plan;
-
-	/* If no ORDER BY, all rows are peers; don't bother to fetch them */
-	if (node->ordNumCols == 0)
-		return true;
-
-	/*
-	 * Note: OK to use temp_slot_2 here because we aren't calling any
-	 * frame-related functions (those tend to clobber temp_slot_2).
-	 */
-	slot1 = winstate->temp_slot_1;
-	slot2 = winstate->temp_slot_2;
-
-	if (!window_gettupleslot(winobj, pos1, slot1))
-		elog(ERROR, "specified position is out of window: " INT64_FORMAT,
-			 pos1);
-	if (!window_gettupleslot(winobj, pos2, slot2))
-		elog(ERROR, "specified position is out of window: " INT64_FORMAT,
-			 pos2);
-
-	res = are_peers(winstate, slot1, slot2);
-
-	ExecClearTuple(slot1);
-	ExecClearTuple(slot2);
-
-	return res;
-}
-
-/*
- * WinGetFuncArgInPartition
- *		Evaluate a window function's argument expression on a specified
- *		row of the partition.  The row is identified in lseek(2) style,
- *		i.e. relative to the current, first, or last row.
- *
- * argno: argument number to evaluate (counted from 0)
- * relpos: signed rowcount offset from the seek position
- * seektype: WINDOW_SEEK_CURRENT, WINDOW_SEEK_HEAD, or WINDOW_SEEK_TAIL
- * set_mark: If the row is found and set_mark is true, the mark is moved to
- *		the row as a side-effect.
- * isnull: output argument, receives isnull status of result
- * isout: output argument, set to indicate whether target row position
- *		is out of partition (can pass NULL if caller doesn't care about this)
- *
- * Specifying a nonexistent row is not an error, it just causes a null result
- * (plus setting *isout true, if isout isn't NULL).
- */
-Datum
-WinGetFuncArgInPartition(WindowObject winobj, int argno,
-						 int relpos, int seektype, bool set_mark,
-						 bool *isnull, bool *isout)
-{
-	WindowAggState *winstate;
-	int64		abs_pos;
-	int64		mark_pos;
-	Datum		datum;
-	bool		null_treatment;
-	int			notnull_offset;
-	int			notnull_relpos;
-	int			forward;
-	bool		myisout;
-
-	Assert(WindowObjectIsValid(winobj));
-	winstate = winobj->winstate;
-
-	null_treatment = (winobj->ignore_nulls == IGNORE_NULLS && relpos != 0);
-
-	switch (seektype)
-	{
-		case WINDOW_SEEK_CURRENT:
-			if (null_treatment)
-				abs_pos = winstate->currentpos;
-			else
-				abs_pos = winstate->currentpos + relpos;
-			break;
-		case WINDOW_SEEK_HEAD:
-			if (null_treatment)
-				abs_pos = 0;
-			else
-				abs_pos = relpos;
-			break;
-		case WINDOW_SEEK_TAIL:
-			spool_tuples(winstate, -1);
-			abs_pos = winstate->spooled_rows - 1 + relpos;
-			break;
-		default:
-			elog(ERROR, "unrecognized window seek type: %d", seektype);
-			abs_pos = 0;		/* keep compiler quiet */
-			break;
-	}
-
-	/* Easy case if IGNORE NULLS is not specified */
-	if (!null_treatment)
-	{
-		/* get tuple and evaluate in partition */
-		datum = gettuple_eval_partition(winobj, argno,
-										abs_pos, isnull, &myisout);
-		if (!myisout && set_mark)
-			WinSetMarkPosition(winobj, abs_pos);
-		if (isout)
-			*isout = myisout;
-		return datum;
-	}
-
-	/* Prepare for loop */
-	notnull_offset = 0;
-	notnull_relpos = abs(relpos);
-	forward = relpos > 0 ? 1 : -1;
-	myisout = false;
-	datum = 0;
-
-	/*
-	 * IGNORE NULLS + WINDOW_SEEK_CURRENT + relpos > 0 case, we would fetch
-	 * beyond the current row + relpos to find out the target row. If we mark
-	 * at abs_pos, next call to WinGetFuncArgInPartition or
-	 * WinGetFuncArgInFrame (in case when a window function have multiple
-	 * args) could fail with "cannot fetch row before WindowObject's mark
-	 * position". So keep the mark position at currentpos.
-	 */
-	if (seektype == WINDOW_SEEK_CURRENT && relpos > 0)
-		mark_pos = winstate->currentpos;
-	else
-	{
-		/*
-		 * For other cases we have no idea what position of row callers would
-		 * fetch next time. Also for relpos < 0 case (we go backward), we
-		 * cannot set mark either. For those cases we always set mark at 0.
-		 */
-		mark_pos = 0;
-	}
-
-	/*
-	 * Get the next nonnull value in the partition, moving forward or backward
-	 * until we find a value or reach the partition's end.  We cache the
-	 * nullness status because we may repeat this process many times.
-	 */
-	do
-	{
-		int			nn_info;	/* NOT NULL status */
-
-		abs_pos += forward;
-		if (abs_pos < 0)		/* clearly out of partition */
-			break;
-
-		/* check NOT NULL cached info */
-		nn_info = get_notnull_info(winobj, abs_pos, argno);
-		if (nn_info == NN_NOTNULL)	/* this row is known to be NOT NULL */
-			notnull_offset++;
-		else if (nn_info == NN_NULL)	/* this row is known to be NULL */
-			continue;			/* keep on moving forward or backward */
-		else					/* need to check NULL or not */
-		{
-			/*
-			 * NOT NULL info does not exist yet.  Get tuple and evaluate func
-			 * arg in partition. We ignore the return value from
-			 * gettuple_eval_partition because we are just interested in
-			 * whether we are inside or outside of partition, NULL or NOT
-			 * NULL.
-			 */
-			(void) gettuple_eval_partition(winobj, argno,
-										   abs_pos, isnull, &myisout);
-			if (myisout)		/* out of partition? */
-				break;
-			if (!*isnull)
-				notnull_offset++;
-			/* record the row status */
-			put_notnull_info(winobj, abs_pos, argno, *isnull);
-		}
-	} while (notnull_offset < notnull_relpos);
-
-	/* get tuple and evaluate func arg in partition */
-	datum = gettuple_eval_partition(winobj, argno,
-									abs_pos, isnull, &myisout);
-	if (!myisout && set_mark)
-		WinSetMarkPosition(winobj, mark_pos);
-	if (isout)
-		*isout = myisout;
-
-	return datum;
-}
-
-/*
- * WinGetFuncArgInFrame
- *		Evaluate a window function's argument expression on a specified
- *		row of the window frame.  The row is identified in lseek(2) style,
- *		i.e. relative to the first or last row of the frame.  (We do not
- *		support WINDOW_SEEK_CURRENT here, because it's not very clear what
- *		that should mean if the current row isn't part of the frame.)
- *
- * argno: argument number to evaluate (counted from 0)
- * relpos: signed rowcount offset from the seek position
- * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
- * set_mark: If the row is found/in frame and set_mark is true, the mark is
- *		moved to the row as a side-effect.
- * isnull: output argument, receives isnull status of result
- * isout: output argument, set to indicate whether target row position
- *		is out of frame (can pass NULL if caller doesn't care about this)
- *
- * Specifying a nonexistent or not-in-frame row is not an error, it just
- * causes a null result (plus setting *isout true, if isout isn't NULL).
- *
- * Note that some exclusion-clause options lead to situations where the
- * rows that are in-frame are not consecutive in the partition.  But we
- * count only in-frame rows when measuring relpos.
- *
- * The set_mark flag is interpreted as meaning that the caller will specify
- * a constant (or, perhaps, monotonically increasing) relpos in successive
- * calls, so that *if there is no exclusion clause* there will be no need
- * to fetch a row before the previously fetched row.  But we do not expect
- * the caller to know how to account for exclusion clauses.  Therefore,
- * if there is an exclusion clause we take responsibility for adjusting the
- * mark request to something that will be safe given the above assumption
- * about relpos.
- */
-Datum
-WinGetFuncArgInFrame(WindowObject winobj, int argno,
-					 int relpos, int seektype, bool set_mark,
-					 bool *isnull, bool *isout)
-{
-	WindowAggState *winstate;
-	ExprContext *econtext;
-	TupleTableSlot *slot;
-
-	Assert(WindowObjectIsValid(winobj));
-	winstate = winobj->winstate;
-	econtext = winstate->ss.ps.ps_ExprContext;
-	slot = winstate->temp_slot_1;
-
-	if (winobj->ignore_nulls == IGNORE_NULLS)
-		return ignorenulls_getfuncarginframe(winobj, argno, relpos, seektype,
-											 set_mark, isnull, isout);
-
-	if (WinGetSlotInFrame(winobj, slot,
-						  relpos, seektype, set_mark,
-						  isnull, isout) == 0)
-	{
-		econtext->ecxt_outertuple = slot;
-		return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
-							econtext, isnull);
-	}
-
-	if (isout)
-		*isout = true;
-	*isnull = true;
-	return (Datum) 0;
-}
-
-/*
- * WinGetSlotInFrame
- * slot: TupleTableSlot to store the result
- * relpos: signed rowcount offset from the seek position
- * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
- * set_mark: If the row is found/in frame and set_mark is true, the mark is
- *		moved to the row as a side-effect.
- * isnull: output argument, receives isnull status of result
- * isout: output argument, set to indicate whether target row position
- *		is out of frame (can pass NULL if caller doesn't care about this)
- *
- * Returns 0 if we successfullt got the slot. false if out of frame.
- * (also isout is set)
- */
-static int
-WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
-				  int relpos, int seektype, bool set_mark,
-				  bool *isnull, bool *isout)
-{
-	WindowAggState *winstate;
-	int64		abs_pos;
-	int64		mark_pos;
-	int			num_reduced_frame;
-
-	Assert(WindowObjectIsValid(winobj));
-	winstate = winobj->winstate;
-
-	switch (seektype)
-	{
-		case WINDOW_SEEK_CURRENT:
-			elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame");
-			abs_pos = mark_pos = 0; /* keep compiler quiet */
-			break;
-		case WINDOW_SEEK_HEAD:
-			/* rejecting relpos < 0 is easy and simplifies code below */
-			if (relpos < 0)
-				goto out_of_frame;
-			update_frameheadpos(winstate);
-			abs_pos = winstate->frameheadpos + relpos;
-			mark_pos = abs_pos;
-
-			/*
-			 * Account for exclusion option if one is active, but advance only
-			 * abs_pos not mark_pos.  This prevents changes of the current
-			 * row's peer group from resulting in trying to fetch a row before
-			 * some previous mark position.
-			 *
-			 * Note that in some corner cases such as current row being
-			 * outside frame, these calculations are theoretically too simple,
-			 * but it doesn't matter because we'll end up deciding the row is
-			 * out of frame.  We do not attempt to avoid fetching rows past
-			 * end of frame; that would happen in some cases anyway.
-			 */
-			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
-			{
-				case 0:
-					/* no adjustment needed */
-					break;
-				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
-					if (abs_pos >= winstate->currentpos &&
-						winstate->currentpos >= winstate->frameheadpos)
-						abs_pos++;
-					break;
-				case FRAMEOPTION_EXCLUDE_GROUP:
-					update_grouptailpos(winstate);
-					if (abs_pos >= winstate->groupheadpos &&
-						winstate->grouptailpos > winstate->frameheadpos)
-					{
-						int64		overlapstart = Max(winstate->groupheadpos,
-													   winstate->frameheadpos);
-
-						abs_pos += winstate->grouptailpos - overlapstart;
-					}
-					break;
-				case FRAMEOPTION_EXCLUDE_TIES:
-					update_grouptailpos(winstate);
-					if (abs_pos >= winstate->groupheadpos &&
-						winstate->grouptailpos > winstate->frameheadpos)
-					{
-						int64		overlapstart = Max(winstate->groupheadpos,
-													   winstate->frameheadpos);
-
-						if (abs_pos == overlapstart)
-							abs_pos = winstate->currentpos;
-						else
-							abs_pos += winstate->grouptailpos - overlapstart - 1;
-					}
-					break;
-				default:
-					elog(ERROR, "unrecognized frame option state: 0x%x",
-						 winstate->frameOptions);
-					break;
-			}
-			num_reduced_frame = row_is_in_reduced_frame(winobj,
-														winstate->frameheadpos);
-			if (num_reduced_frame < 0)
-				goto out_of_frame;
-			else if (num_reduced_frame > 0)
-				if (relpos >= num_reduced_frame)
-					goto out_of_frame;
-			break;
-		case WINDOW_SEEK_TAIL:
-			/* rejecting relpos > 0 is easy and simplifies code below */
-			if (relpos > 0)
-				goto out_of_frame;
-
-			/*
-			 * RPR cares about frame head pos. Need to call
-			 * update_frameheadpos
-			 */
-			update_frameheadpos(winstate);
-
-			update_frametailpos(winstate);
-			abs_pos = winstate->frametailpos - 1 + relpos;
-
-			/*
-			 * Account for exclusion option if one is active.  If there is no
-			 * exclusion, we can safely set the mark at the accessed row.  But
-			 * if there is, we can only mark the frame start, because we can't
-			 * be sure how far back in the frame the exclusion might cause us
-			 * to fetch in future.  Furthermore, we have to actually check
-			 * against frameheadpos here, since it's unsafe to try to fetch a
-			 * row before frame start if the mark might be there already.
-			 */
-			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
-			{
-				case 0:
-					/* no adjustment needed */
-					mark_pos = abs_pos;
-					break;
-				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
-					if (abs_pos <= winstate->currentpos &&
-						winstate->currentpos < winstate->frametailpos)
-						abs_pos--;
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				case FRAMEOPTION_EXCLUDE_GROUP:
-					update_grouptailpos(winstate);
-					if (abs_pos < winstate->grouptailpos &&
-						winstate->groupheadpos < winstate->frametailpos)
-					{
-						int64		overlapend = Min(winstate->grouptailpos,
-													 winstate->frametailpos);
-
-						abs_pos -= overlapend - winstate->groupheadpos;
-					}
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				case FRAMEOPTION_EXCLUDE_TIES:
-					update_grouptailpos(winstate);
-					if (abs_pos < winstate->grouptailpos &&
-						winstate->groupheadpos < winstate->frametailpos)
-					{
-						int64		overlapend = Min(winstate->grouptailpos,
-													 winstate->frametailpos);
-
-						if (abs_pos == overlapend - 1)
-							abs_pos = winstate->currentpos;
-						else
-							abs_pos -= overlapend - 1 - winstate->groupheadpos;
-					}
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				default:
-					elog(ERROR, "unrecognized frame option state: 0x%x",
-						 winstate->frameOptions);
-					mark_pos = 0;	/* keep compiler quiet */
-					break;
-			}
-
-			num_reduced_frame = row_is_in_reduced_frame(winobj,
-														winstate->frameheadpos + relpos);
-			if (num_reduced_frame < 0)
-				goto out_of_frame;
-			else if (num_reduced_frame > 0)
-				abs_pos = winstate->frameheadpos + relpos +
-					num_reduced_frame - 1;
-			break;
-		default:
-			elog(ERROR, "unrecognized window seek type: %d", seektype);
-			abs_pos = mark_pos = 0; /* keep compiler quiet */
-			break;
-	}
-
-	if (!window_gettupleslot(winobj, abs_pos, slot))
-		goto out_of_frame;
-
-	/* The code above does not detect all out-of-frame cases, so check */
-	if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0)
-		goto out_of_frame;
-
-	if (isout)
-		*isout = false;
-	if (set_mark)
-		WinSetMarkPosition(winobj, mark_pos);
-	return 0;
-
-out_of_frame:
-	if (isout)
-		*isout = true;
-	*isnull = true;
-	return -1;
-}
-
-/*
- * WinGetFuncArgCurrent
- *		Evaluate a window function's argument expression on the current row.
- *
- * argno: argument number to evaluate (counted from 0)
- * isnull: output argument, receives isnull status of result
- *
- * Note: this isn't quite equivalent to WinGetFuncArgInPartition or
- * WinGetFuncArgInFrame targeting the current row, because it will succeed
- * even if the WindowObject's mark has been set beyond the current row.
- * This should generally be used for "ordinary" arguments of a window
- * function, such as the offset argument of lead() or lag().
- */
-Datum
-WinGetFuncArgCurrent(WindowObject winobj, int argno, bool *isnull)
-{
-	WindowAggState *winstate;
-	ExprContext *econtext;
-
-	Assert(WindowObjectIsValid(winobj));
-	winstate = winobj->winstate;
-
-	econtext = winstate->ss.ps.ps_ExprContext;
-
-	econtext->ecxt_outertuple = winstate->ss.ss_ScanTupleSlot;
-	return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
-						econtext, isnull);
-}
-
-/*
- * rpr_is_defined
- * return true if Row pattern recognition is defined.
- */
-static bool
-rpr_is_defined(WindowAggState *winstate)
-{
-	return winstate->rpPattern != NULL;
-}
-
-/*
- * -----------------
- * row_is_in_reduced_frame
- * Determine whether a row is in the current row's reduced window frame
- * according to row pattern matching
- *
- * The row must has been already determined that it is in a full window frame
- * and fetched it into slot.
- *
- * Returns:
- * = 0, RPR is not defined.
- * >0, if the row is the first in the reduced frame. Return the number of rows
- * in the reduced frame.
- * -1, if the row is unmatched row
- * -2, if the row is in the reduced frame but needed to be skipped because of
- * AFTER MATCH SKIP PAST LAST ROW
- * -----------------
- */
-static int
-row_is_in_reduced_frame(WindowObject winobj, int64 pos)
-{
-	WindowAggState *winstate = winobj->winstate;
-	int			state;
-	int			rtn;
-
-	if (!rpr_is_defined(winstate))
-	{
-		/*
-		 * RPR is not defined. Assume that we are always in the the reduced
-		 * window frame.
-		 */
-		rtn = 0;
-#ifdef RPR_DEBUG
-		printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
-			   rtn, pos);
-#endif
-		return rtn;
-	}
-
-	state = get_reduced_frame_map(winstate, pos);
-
-	if (state == RF_NOT_DETERMINED)
-	{
-		update_frameheadpos(winstate);
-		update_reduced_frame(winobj, pos);
-	}
-
-	state = get_reduced_frame_map(winstate, pos);
-
-	switch (state)
-	{
-			int64		i;
-			int			num_reduced_rows;
-
-		case RF_FRAME_HEAD:
-			num_reduced_rows = 1;
-			for (i = pos + 1;
-				 get_reduced_frame_map(winstate, i) == RF_SKIPPED; i++)
-				num_reduced_rows++;
-			rtn = num_reduced_rows;
-			break;
-
-		case RF_SKIPPED:
-			rtn = -2;
-			break;
-
-		case RF_UNMATCHED:
-			rtn = -1;
-			break;
-
-		default:
-			elog(ERROR, "unrecognized state: %d at: " INT64_FORMAT,
-				 state, pos);
-			break;
-	}
-
-#ifdef RPR_DEBUG
-	printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
-		   rtn, pos);
-#endif
-	return rtn;
-}
-
-#define REDUCED_FRAME_MAP_INIT_SIZE	1024L
-
-/*
- * create_reduced_frame_map
- * Create reduced frame map
- */
-static void
-create_reduced_frame_map(WindowAggState *winstate)
-{
-	winstate->reduced_frame_map =
-		MemoryContextAlloc(winstate->partcontext,
-						   REDUCED_FRAME_MAP_INIT_SIZE);
-	winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE;
-	clear_reduced_frame_map(winstate);
-}
-
-/*
- * clear_reduced_frame_map
- * Clear reduced frame map
- */
-static void
-clear_reduced_frame_map(WindowAggState *winstate)
-{
-	Assert(winstate->reduced_frame_map != NULL);
-	MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
-		   winstate->alloc_sz);
-}
-
-/*
- * get_reduced_frame_map
- * Get reduced frame map specified by pos
- */
-static int
-get_reduced_frame_map(WindowAggState *winstate, int64 pos)
-{
-	Assert(winstate->reduced_frame_map != NULL);
-	Assert(pos >= 0);
-
-	/*
-	 * If pos is not in the reduced frame map, it means that any info
-	 * regarding the pos has not been registered yet. So we return
-	 * RF_NOT_DETERMINED.
-	 */
-	if (pos >= winstate->alloc_sz)
-		return RF_NOT_DETERMINED;
-
-	return winstate->reduced_frame_map[pos];
-}
-
-/*
- * register_reduced_frame_map
- * Add/replace reduced frame map member at pos.
- * If there's no enough space, expand the map.
- */
-static void
-register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val)
-{
-	int64		realloc_sz;
-
-	Assert(winstate->reduced_frame_map != NULL);
-
-	if (pos < 0)
-		elog(ERROR, "wrong pos: " INT64_FORMAT, pos);
-
-	while (pos > winstate->alloc_sz - 1)
-	{
-		realloc_sz = winstate->alloc_sz * 2;
-
-		winstate->reduced_frame_map =
-			repalloc(winstate->reduced_frame_map, realloc_sz);
-
-		MemSet(winstate->reduced_frame_map + winstate->alloc_sz,
-			   RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz);
-
-		winstate->alloc_sz = realloc_sz;
-	}
-
-	winstate->reduced_frame_map[pos] = val;
-}
-
-/*
- * update_reduced_frame
- *		Update reduced frame info using multi-context NFA pattern matching.
- *
- * Maintains multiple NFA contexts simultaneously, one for each potential
- * match start position. This allows sharing row evaluations across contexts,
- * avoiding redundant DEFINE clause evaluations when rewinding for SKIP TO
- * NEXT ROW mode.
- *
- * Key optimizations:
- * - Row evaluations (expensive DEFINE clauses) happen only once per row
- * - All active contexts share the same evaluation results
- * - Contexts persist across calls, enabling O(n) DEFINE evaluations
- */
-static void
-update_reduced_frame(WindowObject winobj, int64 pos)
+static void
+update_reduced_frame(WindowObject winobj, int64 pos)
 {
 	WindowAggState *winstate = winobj->winstate;
 	RPRNFAContext *targetCtx;
@@ -4853,7 +4157,7 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	/*
 	 * Case 2: Find existing context for this pos, or create new one.
 	 */
-	targetCtx = nfa_get_head_context(winstate, pos);
+	targetCtx = ExecRPRGetHeadContext(winstate, pos);
 	if (targetCtx == NULL)
 	{
 		/*
@@ -4867,7 +4171,7 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 			return;
 		}
 		/* Not yet processed - create new context and start fresh */
-		targetCtx = nfa_start_context(winstate, pos);
+		targetCtx = ExecRPRStartContext(winstate, pos);
 	}
 	else if (targetCtx->states == NULL)
 	{
@@ -4900,14 +4204,9 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		/* No more rows in partition? Finalize all contexts */
 		if (!rowExists)
 		{
-			nfa_finalize_all_contexts(winstate, currentPos - 1);
+			ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
 			/* Clean up dead contexts from finalization */
-			nfa_cleanup_dead_contexts(winstate, targetCtx);
-			/* Absorb contexts at partition boundary */
-			if (winstate->rpPattern->isAbsorbable)
-			{
-				nfa_absorb_contexts(winstate);
-			}
+			ExecRPRCleanupDeadContexts(winstate, targetCtx);
 			break;
 		}
 
@@ -4920,20 +4219,20 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		 *   2. Absorb redundant
 		 *   3. Advance all (divergence)
 		 */
-		nfa_process_row(winstate, currentPos, hasLimitedFrame, frameOffset);
+		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.
 		 */
-		nfa_start_context(winstate, currentPos + 1);
+		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.
 		 */
-		nfa_cleanup_dead_contexts(winstate, targetCtx);
+		ExecRPRCleanupDeadContexts(winstate, targetCtx);
 	}
 
 register_result:
@@ -4947,648 +4246,23 @@ register_result:
 		matchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1;
 
 		register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_UNMATCHED);
-		nfa_record_context_failure(winstate, matchLen);
-		nfa_context_free(winstate, targetCtx);
+		ExecRPRRecordContextFailure(winstate, matchLen);
+		ExecRPRFreeContext(winstate, targetCtx);
 		return;
 	}
 
 	/* Match succeeded - register frame map and record statistics */
 	matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1;
 
-	register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD);
-	for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++)
-	{
-		register_reduced_frame_map(winstate, i, RF_SKIPPED);
-	}
-	nfa_record_context_success(winstate, matchLen);
-
-	/* Remove the matched context */
-	nfa_context_free(winstate, targetCtx);
-}
-
-/*
- * NFA-based pattern matching implementation
- *
- * These functions implement direct NFA execution using the compiled
- * RPRPattern structure, avoiding regex compilation overhead.
- *
- * Execution Flow: match -> absorb -> advance
- * -----------------------------------------
- * The NFA execution follows a three-phase cycle for each row:
- *
- * 1. MATCH (convergence): Evaluate all waiting states against current row.
- *    States on VAR elements are checked against their defining conditions.
- *    Failed matches are removed, successful ones may transition forward.
- *    This is a "convergence" phase - the number of states tends to decrease.
- *
- * 2. ABSORB: After matching, check if any context can absorb another.
- *    Context absorption is an optimization that merges equivalent contexts.
- *    A context can only be absorbed if ALL its states are absorbable.
- *
- * 3. ADVANCE (divergence): Expand states through epsilon transitions.
- *    States advance through ALT (alternation), END (group end), and
- *    optional elements until reaching VAR or FIN elements where they wait.
- *    This is a "divergence" phase - ALT creates multiple branch states.
- *
- * Key Design Decisions:
- * ---------------------
- * - VAR->END transition in match phase: When a simple VAR (max=1) matches
- *   and the next element is END, we transition immediately in the match
- *   phase rather than waiting for advance. This is necessary for correct
- *   absorption: states must be at END to be marked absorbable before the
- *   absorption check occurs.
- *
- * - Optional VAR skip paths: When advance lands on a VAR with min=0,
- *   we create both a waiting state AND a skip state (like ALT branches).
- *   This ensures patterns like "A B? C" work correctly - we need a state
- *   waiting for B AND a state that has already skipped to C.
- *
- * - END->END count increment: When transitioning from one END to another
- *   END within advance, we must increment the outer END's count. This
- *   handles nested groups like "((A|B)+)+" correctly - exiting the inner
- *   group counts as one iteration of the outer group.
- *
- * - Empty match handling: The initial advance uses currentPos =
- *   startPos - 1 (before any row is consumed). If FIN is reached via
- *   epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow,
- *   resulting in UNMATCHED. For reluctant min=0 patterns (A*?, A??),
- *   the skip path reaches FIN first and early termination prunes enter
- *   paths, yielding an immediate empty (unmatched) result. For
- *   greedy patterns (A*), the enter path adds VAR states first, then
- *   the skip FIN is recorded but VAR states survive for later matching.
- *
- * Context Absorption Runtime:
- * ---------------------------
- * Absorption uses flags computed at planning time (in rpr.c) and two
- * context-level flags maintained at runtime:
- *
- * State-level:
- *   state.isAbsorbable: true if state is in the absorbable region.
- *     - Set at creation: elem->flags & RPR_ELEM_ABSORBABLE_BRANCH
- *     - At transition: prevAbsorbable && (newElem->flags & ABSORBABLE_BRANCH)
- *     - Monotonic: once false, stays false forever
- *
- * Context-level:
- *   ctx.hasAbsorbableState: can this context absorb others?
- *     - True if at least one state has isAbsorbable=true
- *     - Monotonic: true->false only (optimization: skip recalc when false)
- *
- *   ctx.allStatesAbsorbable: can this context be absorbed?
- *     - True if ALL states have isAbsorbable=true
- *     - Dynamic: can change false->true (when non-absorbable states die)
- *
- * Absorption Algorithm:
- *   For each pair (older Ctx1, newer Ctx2):
- *   1. Pre-check: Ctx1.hasAbsorbableState && Ctx2.allStatesAbsorbable
- *      -> If false, skip (fast filter)
- *   2. Coverage check: For each Ctx2 state with isAbsorbable=true,
- *      find Ctx1 state with same elemIdx and count >= Ctx2.count
- *   3. If all Ctx2 absorbable states are covered, absorb Ctx2
- *
- * Example: Pattern A+ B
- *   Row 1: Ctx1 at A (count=1)
- *   Row 2: Ctx1 at A (count=2), Ctx2 at A (count=1)
- *   -> Both at same elemIdx (A), Ctx1.count >= Ctx2.count
- *   -> Ctx2 absorbed
- *
- * The asymmetric design (Ctx1 needs hasAbsorbable, Ctx2 needs allAbsorbable)
- * allows absorption even when Ctx1 has extra non-absorbable states.
- */
-
-/*
- * nfa_process_row
- *
- * Process all contexts for one row:
- *   1. Match all contexts (convergence) - evaluate VARs, prune dead states
- *   2. Absorb redundant contexts - ideal timing after convergence
- *   3. Advance all contexts (divergence) - create new states for next row
- */
-static void
-nfa_process_row(WindowAggState *winstate, int64 currentPos,
-				bool hasLimitedFrame, int64 frameOffset)
-{
-	RPRNFAContext *ctx;
-	bool	   *varMatched = winstate->nfaVarMatched;
-
-	/*
-	 * Phase 1: Match all contexts (convergence).  Evaluate VAR elements,
-	 * update counts, remove dead states.
-	 */
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-	{
-		if (ctx->states == NULL)
-			continue;
-
-		/* Check frame boundary - finalize if exceeded */
-		if (hasLimitedFrame)
-		{
-			int64		ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
-
-			if (currentPos >= ctxFrameEnd)
-			{
-				/* Frame boundary exceeded: force mismatch */
-				nfa_match(winstate, ctx, NULL);
-				continue;
-			}
-		}
-
-		nfa_match(winstate, ctx, varMatched);
-		ctx->lastProcessedRow = currentPos;
-	}
-
-	/*
-	 * Phase 2: Absorb redundant contexts.  After match phase, states have
-	 * converged - ideal for absorption.  First update absorption flags that
-	 * may have changed due to state removal.
-	 */
-	if (winstate->rpPattern->isAbsorbable)
-	{
-		for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-			nfa_update_absorption_flags(ctx);
-
-		nfa_absorb_contexts(winstate);
-	}
-
-	/*
-	 * Phase 3: Advance all contexts (divergence).  Create new states
-	 * (loop/exit) from surviving matched states.
-	 */
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-	{
-		if (ctx->states == NULL)
-			continue;
-
-		/*
-		 * Phase 1 already handled frame boundary exceeded contexts by forcing
-		 * mismatch (nfa_match with NULL), which removes all states (all
-		 * states are at VAR positions after advance). So any surviving
-		 * context here must be within its frame boundary.
-		 */
-		Assert(!hasLimitedFrame ||
-			   currentPos < ctx->matchStartRow + frameOffset + 1);
-
-		nfa_advance(winstate, ctx, currentPos);
-	}
-}
-
-/*
- * nfa_state_alloc
- *
- * Allocate an NFA state, reusing from freeList if available.
- * freeList is stored in WindowAggState for reuse across match attempts.
- * Uses flexible array member for counts[].
- */
-static RPRNFAState *
-nfa_state_alloc(WindowAggState *winstate)
-{
-	RPRNFAState *state;
-
-	/* Try to reuse from free list first */
-	if (winstate->nfaStateFree != NULL)
-	{
-		state = winstate->nfaStateFree;
-		winstate->nfaStateFree = state->next;
-	}
-	else
-	{
-		/* Allocate in partition context for proper lifetime */
-		state = MemoryContextAlloc(winstate->partcontext, winstate->nfaStateSize);
-	}
-
-	/* Initialize entire state to zero */
-	memset(state, 0, winstate->nfaStateSize);
-
-	/* Update statistics */
-	winstate->nfaStatesActive++;
-	winstate->nfaStatesTotalCreated++;
-	if (winstate->nfaStatesActive > winstate->nfaStatesMax)
-		winstate->nfaStatesMax = winstate->nfaStatesActive;
-
-	return state;
-}
-
-/*
- * nfa_state_free
- *
- * Return a state to the free list for later reuse.
- */
-static void
-nfa_state_free(WindowAggState *winstate, RPRNFAState *state)
-{
-	winstate->nfaStatesActive--;
-	state->next = winstate->nfaStateFree;
-	winstate->nfaStateFree = state;
-}
-
-/*
- * nfa_state_free_list
- *
- * Return all states in a list to the free list.
- */
-static void
-nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list)
-{
-	RPRNFAState *next;
-
-	for (; list != NULL; list = next)
-	{
-		next = list->next;
-		nfa_state_free(winstate, list);
-	}
-}
-
-/*
- * nfa_state_create
- *
- * Create a new state with given elemIdx and counts.
- * isAbsorbable is computed immediately: inherited AND new element's flag.
- * Monotonic property: once false, stays false through all transitions.
- *
- * Caller is responsible for linking the returned state.
- */
-static RPRNFAState *
-nfa_state_create(WindowAggState *winstate, int16 elemIdx,
-				 int32 *counts, bool sourceAbsorbable)
-{
-	RPRPattern *pattern = winstate->rpPattern;
-	int			maxDepth = pattern->maxDepth;
-	RPRNFAState *state = nfa_state_alloc(winstate);
-	RPRPatternElement *elem = &pattern->elements[elemIdx];
-
-	state->elemIdx = elemIdx;
-	if (counts != NULL && maxDepth > 0)
-		memcpy(state->counts, counts, sizeof(int32) * maxDepth);
-
-	/*
-	 * Compute isAbsorbable immediately at transition time. isAbsorbable =
-	 * sourceAbsorbable && (elem->flags & ABSORBABLE_BRANCH) Monotonic: once
-	 * false, stays false (can't re-enter absorbable region).
-	 */
-	state->isAbsorbable = sourceAbsorbable && RPRElemIsAbsorbableBranch(elem);
-
-	return state;
-}
-
-/*
- * nfa_states_equal
- *
- * Check if two states are equivalent (same elemIdx and counts).
- */
-static bool
-nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
-{
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRPatternElement *elem;
-	int			compareDepth;
-
-	if (s1->elemIdx != s2->elemIdx)
-		return false;
-
-	/* Compare counts up to current element's depth */
-	elem = &pattern->elements[s1->elemIdx];
-	compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
-
-	if (memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
-		return false;
-
-	return true;
-}
-
-/*
- * nfa_add_state_unique
- *
- * Add a state to ctx->states at the END, only if no duplicate exists.
- * Returns true if state was added, false if duplicate found (state is freed).
- * Earlier states have better lexical order (DFS traversal order), so existing wins.
- */
-static bool
-nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *state)
-{
-	RPRNFAState *s;
-	RPRNFAState *tail = NULL;
-
-	/* Check for duplicate and find tail */
-	for (s = ctx->states; s != NULL; s = s->next)
-	{
-		if (nfa_states_equal(winstate, s, state))
-		{
-			/*
-			 * Duplicate found - existing has better lexical order, discard
-			 * new
-			 */
-			nfa_state_free(winstate, state);
-			winstate->nfaStatesMerged++;
-			return false;
-		}
-		tail = s;
-	}
-
-	/* No duplicate, add at end */
-	state->next = NULL;
-	if (tail == NULL)
-		ctx->states = state;
-	else
-		tail->next = state;
-
-	return true;
-}
-
-/*
- * nfa_add_matched_state
- *
- * Record a state that reached FIN, replacing any previous match.
- *
- * For SKIP PAST LAST ROW, also prune subsequent contexts whose start row
- * falls within the match range, as they cannot produce output rows.
- */
-static void
-nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
-					  RPRNFAState *state, int64 matchEndRow)
-{
-	if (ctx->matchedState != NULL)
-		nfa_state_free(winstate, ctx->matchedState);
-
-	ctx->matchedState = state;
-	state->next = NULL;
-	ctx->matchEndRow = matchEndRow;
-
-	/* Prune contexts that started within this match's range */
-	if (winstate->rpSkipTo == ST_PAST_LAST_ROW)
-	{
-		RPRNFAContext *nextCtx;
-		int64		skippedLen;
-
-		while (ctx->next != NULL &&
-			   ctx->next->matchStartRow <= matchEndRow)
-		{
-			nextCtx = ctx->next;
-			ctx->next = ctx->next->next;
-
-			Assert(nextCtx->lastProcessedRow >= nextCtx->matchStartRow);
-			skippedLen = nextCtx->lastProcessedRow - nextCtx->matchStartRow + 1;
-			nfa_record_context_skipped(winstate, skippedLen);
-
-			nfa_context_free(winstate, nextCtx);
-		}
-		if (ctx->next == NULL)
-			winstate->nfaContextTail = ctx;
-	}
-}
-
-/*
- * nfa_context_alloc
- *
- * Allocate an NFA context, reusing from free list if available.
- */
-static RPRNFAContext *
-nfa_context_alloc(WindowAggState *winstate)
-{
-	RPRNFAContext *ctx;
-
-	if (winstate->nfaContextFree != NULL)
-	{
-		ctx = winstate->nfaContextFree;
-		winstate->nfaContextFree = ctx->next;
-	}
-	else
-	{
-		/* Allocate in partition context for proper lifetime */
-		ctx = MemoryContextAlloc(winstate->partcontext, sizeof(RPRNFAContext));
-	}
-
-	ctx->next = NULL;
-	ctx->prev = NULL;
-	ctx->states = NULL;
-	ctx->matchStartRow = -1;
-	ctx->matchEndRow = -1;
-	ctx->lastProcessedRow = -1;
-	ctx->matchedState = NULL;
-
-	/* Initialize two-flag absorption design based on pattern */
-	ctx->hasAbsorbableState = winstate->rpPattern->isAbsorbable;
-	ctx->allStatesAbsorbable = winstate->rpPattern->isAbsorbable;
-
-	/* Update statistics */
-	winstate->nfaContextsActive++;
-	winstate->nfaContextsTotalCreated++;
-	if (winstate->nfaContextsActive > winstate->nfaContextsMax)
-		winstate->nfaContextsMax = winstate->nfaContextsActive;
-
-	return ctx;
-}
-
-/*
- * nfa_unlink_context
- *
- * Remove a context from the doubly-linked active context list.
- * Updates head (nfaContext) and tail (nfaContextTail) as needed.
- */
-static void
-nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx)
-{
-	if (ctx->prev != NULL)
-		ctx->prev->next = ctx->next;
-	else
-		winstate->nfaContext = ctx->next;	/* was head */
-
-	if (ctx->next != NULL)
-		ctx->next->prev = ctx->prev;
-	else
-		winstate->nfaContextTail = ctx->prev;	/* was tail */
-
-	ctx->next = NULL;
-	ctx->prev = NULL;
-}
-
-/*
- * nfa_context_free
- *
- * Unlink context from active list and return it to free list.
- * Also frees any states in the context.
- */
-static void
-nfa_context_free(WindowAggState *winstate, RPRNFAContext *ctx)
-{
-	/* Unlink from active list first */
-	nfa_unlink_context(winstate, ctx);
-
-	/* Update statistics */
-	winstate->nfaContextsActive--;
-
-	if (ctx->states != NULL)
-		nfa_state_free_list(winstate, ctx->states);
-	if (ctx->matchedState != NULL)
-		nfa_state_free(winstate, ctx->matchedState);
-
-	ctx->states = NULL;
-	ctx->matchedState = NULL;
-	ctx->next = winstate->nfaContextFree;
-	winstate->nfaContextFree = ctx;
-}
-
-/*
- * nfa_start_context
- *
- * Start a new match context at given position.
- * Initializes context, state absorption flags, and performs initial advance
- * to expand epsilon transitions (ALT branches, optional elements).
- * Adds context to the tail of winstate->nfaContext list.
- */
-static RPRNFAContext *
-nfa_start_context(WindowAggState *winstate, int64 startPos)
-{
-	RPRNFAContext *ctx;
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRPatternElement *elem;
-
-	ctx = nfa_context_alloc(winstate);
-	ctx->matchStartRow = startPos;
-	ctx->states = nfa_state_alloc(winstate);	/* initial state at elem 0 */
-
-	elem = &pattern->elements[0];
-
-	if (RPRElemIsAbsorbableBranch(elem))
-	{
-		ctx->states->isAbsorbable = true;
-	}
-	else
-	{
-		ctx->hasAbsorbableState = false;
-		ctx->allStatesAbsorbable = false;
-		ctx->states->isAbsorbable = false;
-	}
-
-	/* Add to tail of active context list (doubly-linked, oldest-first) */
-	ctx->prev = winstate->nfaContextTail;
-	ctx->next = NULL;
-	if (winstate->nfaContextTail != NULL)
-		winstate->nfaContextTail->next = ctx;
-	else
-		winstate->nfaContext = ctx; /* first context becomes head */
-	winstate->nfaContextTail = ctx;
-
-	/*
-	 * Initial advance (divergence): expand ALT branches and create exit
-	 * states for VAR elements with min=0. This prepares the context for the
-	 * first row's match phase.
-	 *
-	 * Use startPos - 1 as currentPos since no row has been consumed yet. If
-	 * FIN is reached via epsilon transitions, matchEndRow = startPos - 1
-	 * which is less than matchStartRow, resulting in UNMATCHED treatment.
-	 */
-	nfa_advance(winstate, ctx, startPos - 1);
-
-	return ctx;
-}
-
-/*
- * nfa_get_head_context
- *
- * Return the head context if its start position matches pos.
- * Returns NULL if no context exists or head doesn't match pos.
- */
-static RPRNFAContext *
-nfa_get_head_context(WindowAggState *winstate, int64 pos)
-{
-	RPRNFAContext *ctx = winstate->nfaContext;
-
-	/*
-	 * Contexts are sorted by matchStartRow ascending.  If the head context
-	 * doesn't match pos, no context exists for this position.
-	 */
-	if (ctx == NULL || ctx->matchStartRow != pos)
-		return NULL;
-
-	return ctx;
-}
-
-/*
- * nfa_update_length_stats
- *
- * Helper function to update min/max/total length statistics.
- * Called when tracking match/mismatch/absorbed/skipped lengths.
- */
-static void
-nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen)
-{
-	if (count == 1)
-	{
-		stats->min = newLen;
-		stats->max = newLen;
-	}
-	else
-	{
-		if (newLen < stats->min)
-			stats->min = newLen;
-		if (newLen > stats->max)
-			stats->max = newLen;
-	}
-	stats->total += newLen;
-}
-
-/*
- * nfa_record_context_success
- *
- * Record a successful context in statistics.
- */
-static void
-nfa_record_context_success(WindowAggState *winstate, int64 matchLen)
-{
-	winstate->nfaMatchesSucceeded++;
-	nfa_update_length_stats(winstate->nfaMatchesSucceeded,
-							&winstate->nfaMatchLen,
-							matchLen);
-}
-
-/*
- * nfa_record_context_failure
- *
- * Record a failed context in statistics.
- * If failedLen == 1, count as pruned (failed on first row).
- * If failedLen > 1, count as mismatched and update length stats.
- */
-static void
-nfa_record_context_failure(WindowAggState *winstate, int64 failedLen)
-{
-	if (failedLen == 1)
-	{
-		winstate->nfaContextsPruned++;
-	}
-	else
+	register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD);
+	for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++)
 	{
-		winstate->nfaMatchesFailed++;
-		nfa_update_length_stats(winstate->nfaMatchesFailed,
-								&winstate->nfaFailLen,
-								failedLen);
+		register_reduced_frame_map(winstate, i, RF_SKIPPED);
 	}
-}
-
-/*
- * nfa_record_context_skipped
- *
- * Record a skipped context in statistics.
- */
-static void
-nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen)
-{
-	winstate->nfaContextsSkipped++;
-	nfa_update_length_stats(winstate->nfaContextsSkipped,
-							&winstate->nfaSkippedLen,
-							skippedLen);
-}
+	ExecRPRRecordContextSuccess(winstate, matchLen);
 
-/*
- * nfa_record_context_absorbed
- *
- * Record an absorbed context in statistics.
- */
-static void
-nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen)
-{
-	winstate->nfaContextsAbsorbed++;
-	nfa_update_length_stats(winstate->nfaContextsAbsorbed,
-							&winstate->nfaAbsorbedLen,
-							absorbedLen);
+	/* Remove the matched context */
+	ExecRPRFreeContext(winstate, targetCtx);
 }
 
 /*
@@ -5612,7 +4286,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	/*
 	 * Set up slots for current, previous, and next rows. We don't call
 	 * get_slots() here to avoid recursion through row_is_in_frame ->
-	 * update_reduced_frame -> nfa_process_row.
+	 * update_reduced_frame -> ExecRPRProcessRow.
 	 */
 
 	/* Current row -> ecxt_outertuple */
@@ -5659,875 +4333,630 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	return true;				/* Row exists */
 }
 
-/*
- * nfa_cleanup_dead_contexts
- *
- * Remove contexts that have failed (no active states and no match).
- * These are contexts that failed during normal processing and should be
- * counted as pruned (if length 1) or mismatched (if length > 1).
- */
-static void
-nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
-{
-	RPRNFAContext *ctx;
-	RPRNFAContext *next;
-
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = next)
-	{
-		next = ctx->next;
-
-		/* Skip the target context and contexts still processing */
-		if (ctx == excludeCtx || ctx->states != NULL)
-			continue;
-
-		/* Skip successfully matched contexts (will be handled by SKIP logic) */
-		if (ctx->matchEndRow >= ctx->matchStartRow)
-			continue;
-
-		/*
-		 * This is a failed context - count and remove it. Only count if it
-		 * actually processed its start row. Contexts created for
-		 * beyond-partition rows are silently removed.
-		 */
-		if (ctx->lastProcessedRow >= ctx->matchStartRow)
-		{
-			int64		failedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
-
-			nfa_record_context_failure(winstate, failedLen);
-		}
-		/* else: context was never processed (beyond-partition), just remove */
-
-		nfa_context_free(winstate, ctx);
-	}
-}
 
-/*
- * nfa_finalize_all_contexts
- *
- * Finalize all active contexts when partition ends.
- * Match with NULL to force mismatch, then advance to process epsilon transitions.
- */
-static void
-nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos)
-{
-	RPRNFAContext *ctx;
+/***********************************************************************
+ * API exposed to window functions
+ ***********************************************************************/
 
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-	{
-		if (ctx->states != NULL)
-		{
-			nfa_match(winstate, ctx, NULL);
-			nfa_advance(winstate, ctx, lastPos);
-		}
-	}
-}
 
 /*
- * nfa_update_absorption_flags
- *
- * Update context's absorption flags after state changes.
- *
- * Two flags control absorption behavior:
- *   hasAbsorbableState: true if context has at least one absorbable state.
- *     This flag is monotonic (true -> false only). Once all absorbable states
- *     die, no new absorbable states can be created through transitions.
- *   allStatesAbsorbable: true if ALL states in context are absorbable.
- *     This flag is dynamic and can change false -> true when non-absorbable
- *     states die off.
+ * WinCheckAndInitializeNullTreatment
+ *		Check null treatment clause and sets ignore_nulls
  *
- * Optimization: Once hasAbsorbableState becomes false, both flags remain false
- * permanently, so we skip recalculation.
+ * Window functions should call this to check if they are being called with
+ * a null treatment clause when they don't allow it, or to set ignore_nulls.
  */
-static void
-nfa_update_absorption_flags(RPRNFAContext *ctx)
+void
+WinCheckAndInitializeNullTreatment(WindowObject winobj,
+								   bool allowNullTreatment,
+								   FunctionCallInfo fcinfo)
 {
-	RPRNFAState *state;
-	bool		hasAbsorbable = false;
-	bool		allAbsorbable = true;
-
-	/*
-	 * Optimization: Once hasAbsorbableState becomes false, it stays false. No
-	 * need to recalculate - both flags remain false permanently.
-	 */
-	if (!ctx->hasAbsorbableState)
-	{
-		ctx->allStatesAbsorbable = false;
-		return;
-	}
-
-	/* No states means no absorbable states */
-	if (ctx->states == NULL)
+	Assert(WindowObjectIsValid(winobj));
+	if (winobj->ignore_nulls != NO_NULLTREATMENT && !allowNullTreatment)
 	{
-		ctx->hasAbsorbableState = false;
-		ctx->allStatesAbsorbable = false;
-		return;
-	}
+		const char *funcname = get_func_name(fcinfo->flinfo->fn_oid);
 
-	/*
-	 * 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
-	 * point.
-	 */
-	for (state = ctx->states; state != NULL; state = state->next)
-	{
-		if (state->isAbsorbable)
-			hasAbsorbable = true;
-		else
-			allAbsorbable = false;
+		if (!funcname)
+			elog(ERROR, "could not get function name");
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("function %s does not allow RESPECT/IGNORE NULLS",
+						funcname)));
 	}
-
-	ctx->hasAbsorbableState = hasAbsorbable;
-	ctx->allStatesAbsorbable = allAbsorbable;
+	else if (winobj->ignore_nulls == PARSER_IGNORE_NULLS)
+		winobj->ignore_nulls = IGNORE_NULLS;
 }
 
 /*
- * nfa_states_covered
- *
- * Check if all states in newer context are "covered" by older context.
+ * WinGetPartitionLocalMemory
+ *		Get working memory that lives till end of partition processing
  *
- * A newer state is covered when older context has an absorbable state at the
- * same pattern element (elemIdx) with count >= newer's count at that depth.
- * The covering state must be absorbable because only absorbable states can
- * guarantee to produce superset matches.
+ * On first call within a given partition, this allocates and zeroes the
+ * requested amount of space.  Subsequent calls just return the same chunk.
  *
- * If all newer states are covered, newer context's eventual matches will be
- * a subset of older context's matches, making newer redundant.
+ * Memory obtained this way is normally used to hold state that should be
+ * automatically reset for each new partition.  If a window function wants
+ * to hold state across the whole query, fcinfo->fn_extra can be used in the
+ * usual way for that.
  */
-static bool
-nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *newer)
+void *
+WinGetPartitionLocalMemory(WindowObject winobj, Size sz)
 {
-	RPRNFAState *newerState;
-
-	for (newerState = newer->states; newerState != NULL; newerState = newerState->next)
-	{
-		RPRNFAState *olderState;
-		RPRPatternElement *elem;
-		int			depth;
-		bool		found = false;
-
-		/* All states are absorbable (caller checks allStatesAbsorbable) */
-		elem = &pattern->elements[newerState->elemIdx];
-		depth = elem->depth;
-
-		for (olderState = older->states; olderState != NULL; olderState = olderState->next)
-		{
-			/* Covering state must also be absorbable */
-			if (olderState->isAbsorbable &&
-				olderState->elemIdx == newerState->elemIdx &&
-				olderState->counts[depth] >= newerState->counts[depth])
-			{
-				found = true;
-				break;
-			}
-		}
-
-		if (!found)
-			return false;
-	}
-
-	return true;
+	Assert(WindowObjectIsValid(winobj));
+	if (winobj->localmem == NULL)
+		winobj->localmem =
+			MemoryContextAllocZero(winobj->winstate->partcontext, sz);
+	return winobj->localmem;
 }
 
 /*
- * nfa_try_absorb_context
- *
- * Try to absorb ctx (newer) into an older in-progress context.
- * Returns true if ctx was absorbed and freed.
- *
- * Absorption requires three conditions:
- *   1. ctx must have all states absorbable (allStatesAbsorbable).
- *      If ctx has any non-absorbable state, it may produce unique matches.
- *   2. older must have at least one absorbable state (hasAbsorbableState).
- *      Without absorbable states, older cannot cover newer's states.
- *   3. All ctx states must be covered by older's absorbable states.
- *      This ensures older will produce all matches that ctx would produce.
- *
- * Context list is ordered by creation time (oldest first via prev chain).
- * Each row creates at most one context, so earlier contexts have smaller
- * matchStartRow values.
+ * WinGetCurrentPosition
+ *		Return the current row's position (counting from 0) within the current
+ *		partition.
  */
-static bool
-nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx)
+int64
+WinGetCurrentPosition(WindowObject winobj)
 {
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRNFAContext *older;
-
-	/* Early exit: ctx must have all states absorbable */
-	if (!ctx->allStatesAbsorbable)
-		return false;
-
-	for (older = ctx->prev; older != NULL; older = older->prev)
-	{
-		/*
-		 * By invariant: ctx->prev chain is in creation order (oldest first),
-		 * and each row creates at most one context. So all contexts in this
-		 * chain have matchStartRow < ctx->matchStartRow.
-		 */
-
-		/* Older must also be in-progress */
-		if (older->states == NULL)
-			continue;
-
-		/* Older must have at least one absorbable state */
-		if (!older->hasAbsorbableState)
-			continue;
-
-		/* Check if all newer states are covered by older */
-		if (nfa_states_covered(pattern, older, ctx))
-		{
-			int64		absorbedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
-
-			nfa_context_free(winstate, ctx);
-			nfa_record_context_absorbed(winstate, absorbedLen);
-			return true;
-		}
-	}
-
-	return false;
+	Assert(WindowObjectIsValid(winobj));
+	return winobj->winstate->currentpos;
 }
 
 /*
- * nfa_absorb_contexts
- *
- * Absorb redundant contexts to reduce memory usage and computation.
- *
- * For patterns like A+, newer contexts starting later will produce subset
- * matches of older contexts with higher counts. By absorbing these redundant
- * contexts early, we avoid duplicate work.
+ * WinGetPartitionRowCount
+ *		Return total number of rows contained in the current partition.
  *
- * Iterates from tail (newest) toward head (oldest) via prev chain.
- * Only in-progress contexts (states != NULL) are candidates for absorption;
- * completed contexts represent valid match results.
+ * Note: this is a relatively expensive operation because it forces the
+ * whole partition to be "spooled" into the tuplestore at once.  Once
+ * executed, however, additional calls within the same partition are cheap.
  */
-static void
-nfa_absorb_contexts(WindowAggState *winstate)
+int64
+WinGetPartitionRowCount(WindowObject winobj)
 {
-	RPRNFAContext *ctx;
-	RPRNFAContext *nextCtx;
-
-	for (ctx = winstate->nfaContextTail; ctx != NULL; ctx = nextCtx)
-	{
-		nextCtx = ctx->prev;
-
-		/*
-		 * Only absorb in-progress contexts; completed contexts are valid
-		 * results
-		 */
-		if (ctx->states != NULL)
-			nfa_try_absorb_context(winstate, ctx);
-	}
+	Assert(WindowObjectIsValid(winobj));
+	spool_tuples(winobj->winstate, -1);
+	return winobj->winstate->spooled_rows;
 }
 
 /*
- * nfa_eval_var_match
+ * WinSetMarkPosition
+ *		Set the "mark" position for the window object, which is the oldest row
+ *		number (counting from 0) it is allowed to fetch during all subsequent
+ *		operations within the current partition.
  *
- * Evaluate if a VAR element matches the current row.
- * Undefined variables (varId >= defineVariableList length) default to TRUE.
+ * Window functions do not have to call this, but are encouraged to move the
+ * mark forward when possible to keep the tuplestore size down and prevent
+ * having to spill rows to disk.
  */
-static inline bool
-nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
-				   bool *varMatched)
+void
+WinSetMarkPosition(WindowObject winobj, int64 markpos)
 {
-	/* This function should only be called for VAR elements */
-	Assert(RPRElemIsVar(elem));
-
-	if (varMatched == NULL)
-		return false;
-	if (elem->varId >= list_length(winstate->defineVariableList))
-		return true;
-	return varMatched[elem->varId];
-}
+	WindowAggState *winstate;
 
-/*
- * nfa_match
- *
- * Match phase (convergence): evaluate VAR elements against current row.
- * Only updates counts and removes dead states. Minimal transitions.
- *
- * For VAR elements:
- *   - matched: count++, keep state (unless count > max)
- *   - not matched: remove state (exit alternatives already exist from
- *     previous advance when count >= min was satisfied)
- *
- * For simple VARs (min=max=1) followed by END:
- *   - Advance to END and update group count before absorb phase
- *   - This ensures absorption can compare states by group completion
- *
- * Non-VAR elements (ALT, END, FIN) are kept as-is for advance phase.
- */
-static void
-nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
-{
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRPatternElement *elements = pattern->elements;
-	RPRNFAState **prevPtr = &ctx->states;
-	RPRNFAState *state;
-	RPRNFAState *nextState;
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
 
-	/*
-	 * Evaluate VAR elements against current row. For simple VARs with END
-	 * next, advance to END and update group count inline so absorb phase can
-	 * compare states properly.
-	 */
-	for (state = ctx->states; state != NULL; state = nextState)
+	if (markpos < winobj->markpos)
+		elog(ERROR, "cannot move WindowObject's mark position backward");
+	tuplestore_select_read_pointer(winstate->buffer, winobj->markptr);
+	if (markpos > winobj->markpos)
 	{
-		RPRPatternElement *elem = &elements[state->elemIdx];
-
-		nextState = state->next;
-
-		if (RPRElemIsVar(elem))
-		{
-			bool		matched;
-			int			depth = elem->depth;
-			int32		count = state->counts[depth];
-
-			matched = nfa_eval_var_match(winstate, elem, varMatched);
-
-			if (matched)
-			{
-				/* Increment count */
-				if (count < RPR_COUNT_MAX)
-					count++;
-
-				/* Max constraint should not be exceeded */
-				Assert(elem->max == RPR_QUANTITY_INF || count <= elem->max);
-
-				state->counts[depth] = count;
-
-				/*
-				 * For simple VAR (min=max=1) with END next, advance to END
-				 * and update group count inline. This keeps state in place,
-				 * preserving lexical order.
-				 */
-				if (elem->min == 1 && elem->max == 1 &&
-					RPRElemIsEnd(&elements[elem->next]))
-				{
-					RPRPatternElement *endElem = &elements[elem->next];
-					int			endDepth = endElem->depth;
-					int32		endCount = state->counts[endDepth];
-
-					Assert(count == 1);
-
-					/* Increment group count with overflow protection */
-					if (endCount < RPR_COUNT_MAX)
-						endCount++;
-
-					/*
-					 * END's max can never be exceeded here because
-					 * nfa_advance_end only loops when count < max, so
-					 * endCount entering inline advance is at most max-1, and
-					 * incrementing yields at most max.
-					 */
-					Assert(endElem->max == RPR_QUANTITY_INF ||
-						   endCount <= endElem->max);
-
-					state->elemIdx = elem->next;
-					state->counts[endDepth] = endCount;
-				}
-				/* else: stay at VAR for advance phase */
-			}
-			else
-			{
-				/*
-				 * Not matched - remove state. Exit alternatives were already
-				 * created by advance phase when count >= min was satisfied.
-				 */
-				*prevPtr = nextState;
-				nfa_state_free(winstate, state);
-				continue;
-			}
-		}
-		/* Non-VAR elements: keep as-is for advance phase */
-
-		prevPtr = &state->next;
+		tuplestore_skiptuples(winstate->buffer,
+							  markpos - winobj->markpos,
+							  true);
+		winobj->markpos = markpos;
+	}
+	tuplestore_select_read_pointer(winstate->buffer, winobj->readptr);
+	if (markpos > winobj->seekpos)
+	{
+		tuplestore_skiptuples(winstate->buffer,
+							  markpos - winobj->seekpos,
+							  true);
+		winobj->seekpos = markpos;
 	}
 }
 
 /*
- * nfa_route_to_elem
+ * WinRowsArePeers
+ *		Compare two rows (specified by absolute position in partition) to see
+ *		if they are equal according to the ORDER BY clause.
  *
- * Route state to next element. If VAR, add to ctx->states and process
- * skip path if optional. Otherwise, continue epsilon expansion via recursion.
+ * NB: this does not consider the window frame mode.
  */
-static void
-nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
-				  RPRNFAState *state, RPRPatternElement *nextElem,
-				  int64 currentPos)
+bool
+WinRowsArePeers(WindowObject winobj, int64 pos1, int64 pos2)
 {
-	if (RPRElemIsVar(nextElem))
-	{
-		RPRNFAState *skipState = NULL;
-
-		/* Create skip state before add_unique, which may free state */
-		if (RPRElemCanSkip(nextElem))
-			skipState = nfa_state_create(winstate, nextElem->next,
-										 state->counts, state->isAbsorbable);
-
-		nfa_add_state_unique(winstate, ctx, state);
+	WindowAggState *winstate;
+	WindowAgg  *node;
+	TupleTableSlot *slot1;
+	TupleTableSlot *slot2;
+	bool		res;
 
-		if (skipState != NULL)
-			nfa_advance_state(winstate, ctx, skipState, currentPos);
-	}
-	else
-	{
-		nfa_advance_state(winstate, ctx, state, currentPos);
-	}
-}
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
+	node = (WindowAgg *) winstate->ss.ps.plan;
 
-/*
- * nfa_advance_alt
- *
- * Handle ALT element: expand all branches in lexical order via DFS.
- */
-static void
-nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
-				RPRNFAState *state, RPRPatternElement *elem,
-				int64 currentPos)
-{
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRPatternElement *elements = pattern->elements;
-	RPRElemIdx	altIdx = elem->next;
+	/* If no ORDER BY, all rows are peers; don't bother to fetch them */
+	if (node->ordNumCols == 0)
+		return true;
 
-	while (altIdx >= 0 && altIdx < pattern->numElements)
-	{
-		RPRPatternElement *altElem = &elements[altIdx];
-		RPRNFAState *newState;
+	/*
+	 * Note: OK to use temp_slot_2 here because we aren't calling any
+	 * frame-related functions (those tend to clobber temp_slot_2).
+	 */
+	slot1 = winstate->temp_slot_1;
+	slot2 = winstate->temp_slot_2;
 
-		/* Stop if element is outside ALT scope (not a branch) */
-		if (altElem->depth <= elem->depth)
-			break;
+	if (!window_gettupleslot(winobj, pos1, slot1))
+		elog(ERROR, "specified position is out of window: " INT64_FORMAT,
+			 pos1);
+	if (!window_gettupleslot(winobj, pos2, slot2))
+		elog(ERROR, "specified position is out of window: " INT64_FORMAT,
+			 pos2);
 
-		/* Create independent state for each branch */
-		newState = nfa_state_create(winstate, altIdx,
-									state->counts, state->isAbsorbable);
+	res = are_peers(winstate, slot1, slot2);
 
-		/* Recursively process this branch before next */
-		nfa_advance_state(winstate, ctx, newState, currentPos);
-		altIdx = altElem->jump;
-	}
+	ExecClearTuple(slot1);
+	ExecClearTuple(slot2);
 
+	return res;
 }
 
 /*
- * nfa_advance_begin
+ * WinGetFuncArgInPartition
+ *		Evaluate a window function's argument expression on a specified
+ *		row of the partition.  The row is identified in lseek(2) style,
+ *		i.e. relative to the current, first, or last row.
+ *
+ * argno: argument number to evaluate (counted from 0)
+ * relpos: signed rowcount offset from the seek position
+ * seektype: WINDOW_SEEK_CURRENT, WINDOW_SEEK_HEAD, or WINDOW_SEEK_TAIL
+ * set_mark: If the row is found and set_mark is true, the mark is moved to
+ *		the row as a side-effect.
+ * isnull: output argument, receives isnull status of result
+ * isout: output argument, set to indicate whether target row position
+ *		is out of partition (can pass NULL if caller doesn't care about this)
  *
- * Handle BEGIN element: group entry logic.
- * BEGIN is only visited at initial group entry (count is always 0).
- * If min=0, creates a skip path past the group.
- * Loop-back from END goes directly to first child, bypassing BEGIN.
+ * Specifying a nonexistent row is not an error, it just causes a null result
+ * (plus setting *isout true, if isout isn't NULL).
  */
-static void
-nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
-				  RPRNFAState *state, RPRPatternElement *elem,
-				  int64 currentPos)
+Datum
+WinGetFuncArgInPartition(WindowObject winobj, int argno,
+						 int relpos, int seektype, bool set_mark,
+						 bool *isnull, bool *isout)
 {
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRPatternElement *elements = pattern->elements;
-	RPRNFAState *skipState = NULL;
+	WindowAggState *winstate;
+	int64		abs_pos;
+	int64		mark_pos;
+	Datum		datum;
+	bool		null_treatment;
+	int			notnull_offset;
+	int			notnull_relpos;
+	int			forward;
+	bool		myisout;
 
-	state->counts[elem->depth] = 0;
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
+
+	null_treatment = (winobj->ignore_nulls == IGNORE_NULLS && relpos != 0);
 
-	/* Optional group: create skip path (but don't route yet) */
-	if (elem->min == 0)
+	switch (seektype)
 	{
-		skipState = nfa_state_create(winstate, elem->jump,
-									 state->counts, state->isAbsorbable);
+		case WINDOW_SEEK_CURRENT:
+			if (null_treatment)
+				abs_pos = winstate->currentpos;
+			else
+				abs_pos = winstate->currentpos + relpos;
+			break;
+		case WINDOW_SEEK_HEAD:
+			if (null_treatment)
+				abs_pos = 0;
+			else
+				abs_pos = relpos;
+			break;
+		case WINDOW_SEEK_TAIL:
+			spool_tuples(winstate, -1);
+			abs_pos = winstate->spooled_rows - 1 + relpos;
+			break;
+		default:
+			elog(ERROR, "unrecognized window seek type: %d", seektype);
+			abs_pos = 0;		/* keep compiler quiet */
+			break;
 	}
 
-	if (skipState != NULL && RPRElemIsReluctant(elem))
+	/* Easy case if IGNORE NULLS is not specified */
+	if (!null_treatment)
 	{
-		RPRNFAState *savedMatch = ctx->matchedState;
+		/* get tuple and evaluate in partition */
+		datum = gettuple_eval_partition(winobj, argno,
+										abs_pos, isnull, &myisout);
+		if (!myisout && set_mark)
+			WinSetMarkPosition(winobj, abs_pos);
+		if (isout)
+			*isout = myisout;
+		return datum;
+	}
 
-		/* Reluctant: skip first (prefer fewer iterations), enter second */
-		nfa_route_to_elem(winstate, ctx, skipState,
-						  &elements[elem->jump], currentPos);
+	/* Prepare for loop */
+	notnull_offset = 0;
+	notnull_relpos = abs(relpos);
+	forward = relpos > 0 ? 1 : -1;
+	myisout = false;
+	datum = 0;
 
+	/*
+	 * IGNORE NULLS + WINDOW_SEEK_CURRENT + relpos > 0 case, we would fetch
+	 * beyond the current row + relpos to find out the target row. If we mark
+	 * at abs_pos, next call to WinGetFuncArgInPartition or
+	 * WinGetFuncArgInFrame (in case when a window function have multiple
+	 * args) could fail with "cannot fetch row before WindowObject's mark
+	 * position". So keep the mark position at currentpos.
+	 */
+	if (seektype == WINDOW_SEEK_CURRENT && relpos > 0)
+		mark_pos = winstate->currentpos;
+	else
+	{
 		/*
-		 * If skip path reached FIN, shortest match is found. Skip group entry
-		 * to prevent longer matches.
+		 * For other cases we have no idea what position of row callers would
+		 * fetch next time. Also for relpos < 0 case (we go backward), we
+		 * cannot set mark either. For those cases we always set mark at 0.
 		 */
-		if (ctx->matchedState != savedMatch)
-		{
-			nfa_state_free(winstate, state);
-			return;
-		}
-
-		state->elemIdx = elem->next;
-		nfa_route_to_elem(winstate, ctx, state,
-						  &elements[state->elemIdx], currentPos);
+		mark_pos = 0;
 	}
-	else
+
+	/*
+	 * Get the next nonnull value in the partition, moving forward or backward
+	 * until we find a value or reach the partition's end.  We cache the
+	 * nullness status because we may repeat this process many times.
+	 */
+	do
 	{
-		/* Greedy: enter first, skip second */
-		state->elemIdx = elem->next;
-		nfa_route_to_elem(winstate, ctx, state,
-						  &elements[state->elemIdx], currentPos);
+		int			nn_info;	/* NOT NULL status */
+
+		abs_pos += forward;
+		if (abs_pos < 0)		/* clearly out of partition */
+			break;
 
-		if (skipState != NULL)
+		/* check NOT NULL cached info */
+		nn_info = get_notnull_info(winobj, abs_pos, argno);
+		if (nn_info == NN_NOTNULL)	/* this row is known to be NOT NULL */
+			notnull_offset++;
+		else if (nn_info == NN_NULL)	/* this row is known to be NULL */
+			continue;			/* keep on moving forward or backward */
+		else					/* need to check NULL or not */
 		{
-			nfa_route_to_elem(winstate, ctx, skipState,
-							  &elements[elem->jump], currentPos);
+			/*
+			 * NOT NULL info does not exist yet.  Get tuple and evaluate func
+			 * arg in partition. We ignore the return value from
+			 * gettuple_eval_partition because we are just interested in
+			 * whether we are inside or outside of partition, NULL or NOT
+			 * NULL.
+			 */
+			(void) gettuple_eval_partition(winobj, argno,
+										   abs_pos, isnull, &myisout);
+			if (myisout)		/* out of partition? */
+				break;
+			if (!*isnull)
+				notnull_offset++;
+			/* record the row status */
+			put_notnull_info(winobj, abs_pos, argno, *isnull);
 		}
-	}
+	} while (notnull_offset < notnull_relpos);
+
+	/* get tuple and evaluate func arg in partition */
+	datum = gettuple_eval_partition(winobj, argno,
+									abs_pos, isnull, &myisout);
+	if (!myisout && set_mark)
+		WinSetMarkPosition(winobj, mark_pos);
+	if (isout)
+		*isout = myisout;
+
+	return datum;
 }
 
 /*
- * nfa_advance_end
+ * WinGetFuncArgInFrame
+ *		Evaluate a window function's argument expression on a specified
+ *		row of the window frame.  The row is identified in lseek(2) style,
+ *		i.e. relative to the first or last row of the frame.  (We do not
+ *		support WINDOW_SEEK_CURRENT here, because it's not very clear what
+ *		that should mean if the current row isn't part of the frame.)
+ *
+ * argno: argument number to evaluate (counted from 0)
+ * relpos: signed rowcount offset from the seek position
+ * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
+ * set_mark: If the row is found/in frame and set_mark is true, the mark is
+ *		moved to the row as a side-effect.
+ * isnull: output argument, receives isnull status of result
+ * isout: output argument, set to indicate whether target row position
+ *		is out of frame (can pass NULL if caller doesn't care about this)
+ *
+ * Specifying a nonexistent or not-in-frame row is not an error, it just
+ * causes a null result (plus setting *isout true, if isout isn't NULL).
+ *
+ * Note that some exclusion-clause options lead to situations where the
+ * rows that are in-frame are not consecutive in the partition.  But we
+ * count only in-frame rows when measuring relpos.
  *
- * Handle END element: group repetition logic.
- * Decides whether to loop back or exit based on count vs min/max.
+ * The set_mark flag is interpreted as meaning that the caller will specify
+ * a constant (or, perhaps, monotonically increasing) relpos in successive
+ * calls, so that *if there is no exclusion clause* there will be no need
+ * to fetch a row before the previously fetched row.  But we do not expect
+ * the caller to know how to account for exclusion clauses.  Therefore,
+ * if there is an exclusion clause we take responsibility for adjusting the
+ * mark request to something that will be safe given the above assumption
+ * about relpos.
  */
-static void
-nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
-				RPRNFAState *state, RPRPatternElement *elem,
-				int64 currentPos)
+Datum
+WinGetFuncArgInFrame(WindowObject winobj, int argno,
+					 int relpos, int seektype, bool set_mark,
+					 bool *isnull, bool *isout)
 {
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRPatternElement *elements = pattern->elements;
-	int			depth = elem->depth;
-	int32		count = state->counts[depth];
-
-	if (count < elem->min)
-	{
-		RPRPatternElement *jumpElem;
-		RPRNFAState *ffState = NULL;
-
-		/* Snapshot state for ff path before modifying for loop-back */
-		if (RPRElemCanEmptyLoop(elem))
-			ffState = nfa_state_create(winstate, state->elemIdx,
-									   state->counts, state->isAbsorbable);
-
-		/* Loop back for real matches (primary path) */
-		for (int d = depth + 1; d < pattern->maxDepth; d++)
-			state->counts[d] = 0;
-		state->elemIdx = elem->jump;
-		jumpElem = &elements[state->elemIdx];
-		nfa_route_to_elem(winstate, ctx, state, jumpElem,
-						  currentPos);
-
-		/*
-		 * Fast-forward fallback for nullable bodies.  E.g. (A?){2,3} when A
-		 * doesn't match: the loop-back produces empty iterations that cycle
-		 * detection would kill.  Instead, exit directly treating all
-		 * remaining required iterations as empty.  Route to elem->next (not
-		 * nfa_advance_end) to avoid creating competing greedy/reluctant loop
-		 * states.
-		 */
-		if (ffState != NULL)
-		{
-			RPRPatternElement *nextElem;
+	WindowAggState *winstate;
+	ExprContext *econtext;
+	TupleTableSlot *slot;
 
-			ffState->counts[depth] = 0;
-			ffState->elemIdx = elem->next;
-			nextElem = &elements[ffState->elemIdx];
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
+	econtext = winstate->ss.ps.ps_ExprContext;
+	slot = winstate->temp_slot_1;
 
-			/* END->END: increment outer END's count */
-			if (RPRElemIsEnd(nextElem) &&
-				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
-				ffState->counts[nextElem->depth]++;
+	if (winobj->ignore_nulls == IGNORE_NULLS)
+		return ignorenulls_getfuncarginframe(winobj, argno, relpos, seektype,
+											 set_mark, isnull, isout);
 
-			nfa_route_to_elem(winstate, ctx, ffState, nextElem,
-							  currentPos);
-		}
-	}
-	else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
+	if (WinGetSlotInFrame(winobj, slot,
+						  relpos, seektype, set_mark,
+						  isnull, isout) == 0)
 	{
-		/* Must exit: reached max iterations. */
-		RPRPatternElement *nextElem;
-
-		state->counts[depth] = 0;
-		state->elemIdx = elem->next;
-		nextElem = &elements[state->elemIdx];
-
-		/* END->END: increment outer END's count */
-		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
-			state->counts[nextElem->depth]++;
-
-		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
+		econtext->ecxt_outertuple = slot;
+		return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
+							econtext, isnull);
 	}
-	else
-	{
-		/*
-		 * Between min and max (with at least one iteration) - can exit or
-		 * loop. Greedy: loop first (prefer more iterations). Reluctant: exit
-		 * first (prefer fewer iterations).
-		 */
-		RPRNFAState *exitState;
-		RPRPatternElement *jumpElem;
-		RPRPatternElement *nextElem;
-
-		/*
-		 * Create exit state first (need original counts before modifying
-		 * state)
-		 */
-		exitState = nfa_state_create(winstate, elem->next,
-									 state->counts, state->isAbsorbable);
-		exitState->counts[depth] = 0;
-		nextElem = &elements[exitState->elemIdx];
-
-		/* END->END: increment outer END's count */
-		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
-			exitState->counts[nextElem->depth]++;
-
-		/* Prepare loop state */
-		for (int d = depth + 1; d < pattern->maxDepth; d++)
-			state->counts[d] = 0;
-		state->elemIdx = elem->jump;
-		jumpElem = &elements[state->elemIdx];
-
-		if (RPRElemIsReluctant(elem))
-		{
-			RPRNFAState *savedMatch = ctx->matchedState;
-
-			/* Exit first (preferred for reluctant) */
-			nfa_route_to_elem(winstate, ctx, exitState, nextElem,
-							  currentPos);
-
-			/*
-			 * If exit path reached FIN, shortest match is found. Skip loop to
-			 * prevent longer matches from replacing it.
-			 */
-			if (ctx->matchedState != savedMatch)
-			{
-				nfa_state_free(winstate, state);
-				return;
-			}
 
-			/* Loop second */
-			nfa_route_to_elem(winstate, ctx, state, jumpElem,
-							  currentPos);
-		}
-		else
-		{
-			/* Loop first (preferred for greedy) */
-			nfa_route_to_elem(winstate, ctx, state, jumpElem,
-							  currentPos);
-			/* Exit second */
-			nfa_route_to_elem(winstate, ctx, exitState, nextElem,
-							  currentPos);
-		}
-	}
+	if (isout)
+		*isout = true;
+	*isnull = true;
+	return (Datum) 0;
 }
 
 /*
- * nfa_advance_var
+ * WinGetSlotInFrame
+ * slot: TupleTableSlot to store the result
+ * relpos: signed rowcount offset from the seek position
+ * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
+ * set_mark: If the row is found/in frame and set_mark is true, the mark is
+ *		moved to the row as a side-effect.
+ * isnull: output argument, receives isnull status of result
+ * isout: output argument, set to indicate whether target row position
+ *		is out of frame (can pass NULL if caller doesn't care about this)
  *
- * Handle VAR element: loop/exit transitions.
- * After match phase, all VAR states have matched - decide next action.
+ * Returns 0 if we successfullt got the slot. false if out of frame.
+ * (also isout is set)
  */
-static void
-nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
-				RPRNFAState *state, RPRPatternElement *elem,
-				int64 currentPos)
+static int
+WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
+				  int relpos, int seektype, bool set_mark,
+				  bool *isnull, bool *isout)
 {
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRPatternElement *elements = pattern->elements;
-	int			depth = elem->depth;
-	int32		count = state->counts[depth];
-	bool		canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max);
-	bool		canExit = (count >= elem->min);
+	WindowAggState *winstate;
+	int64		abs_pos;
+	int64		mark_pos;
+	int			num_reduced_frame;
 
-	/* After a successful match, count >= 1, so at least one must be true */
-	Assert(canLoop || canExit);
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
 
-	if (canLoop && canExit)
+	switch (seektype)
 	{
-		/*
-		 * Both loop and exit possible. Greedy: loop first (prefer longer
-		 * match). Reluctant: exit first (prefer shorter match).
-		 */
-		RPRNFAState *cloneState;
-		RPRPatternElement *nextElem;
-		bool		reluctant = RPRElemIsReluctant(elem);
+		case WINDOW_SEEK_CURRENT:
+			elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame");
+			abs_pos = mark_pos = 0; /* keep compiler quiet */
+			break;
+		case WINDOW_SEEK_HEAD:
+			/* rejecting relpos < 0 is easy and simplifies code below */
+			if (relpos < 0)
+				goto out_of_frame;
+			update_frameheadpos(winstate);
+			abs_pos = winstate->frameheadpos + relpos;
+			mark_pos = abs_pos;
 
-		/*
-		 * Clone state for the second-priority path. For greedy, clone is the
-		 * loop state; for reluctant, clone is the exit state.
-		 */
-		if (reluctant)
-		{
-			RPRNFAState *savedMatch = ctx->matchedState;
+			/*
+			 * Account for exclusion option if one is active, but advance only
+			 * abs_pos not mark_pos.  This prevents changes of the current
+			 * row's peer group from resulting in trying to fetch a row before
+			 * some previous mark position.
+			 *
+			 * Note that in some corner cases such as current row being
+			 * outside frame, these calculations are theoretically too simple,
+			 * but it doesn't matter because we'll end up deciding the row is
+			 * out of frame.  We do not attempt to avoid fetching rows past
+			 * end of frame; that would happen in some cases anyway.
+			 */
+			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
+			{
+				case 0:
+					/* no adjustment needed */
+					break;
+				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
+					if (abs_pos >= winstate->currentpos &&
+						winstate->currentpos >= winstate->frameheadpos)
+						abs_pos++;
+					break;
+				case FRAMEOPTION_EXCLUDE_GROUP:
+					update_grouptailpos(winstate);
+					if (abs_pos >= winstate->groupheadpos &&
+						winstate->grouptailpos > winstate->frameheadpos)
+					{
+						int64		overlapstart = Max(winstate->groupheadpos,
+													   winstate->frameheadpos);
 
-			/* Clone for exit, original stays for loop */
-			cloneState = nfa_state_create(winstate, elem->next,
-										  state->counts, state->isAbsorbable);
-			cloneState->counts[depth] = 0;
-			nextElem = &elements[cloneState->elemIdx];
+						abs_pos += winstate->grouptailpos - overlapstart;
+					}
+					break;
+				case FRAMEOPTION_EXCLUDE_TIES:
+					update_grouptailpos(winstate);
+					if (abs_pos >= winstate->groupheadpos &&
+						winstate->grouptailpos > winstate->frameheadpos)
+					{
+						int64		overlapstart = Max(winstate->groupheadpos,
+													   winstate->frameheadpos);
 
-			/* When exiting directly to an outer END, increment its count */
-			if (RPRElemIsEnd(nextElem))
-			{
-				if (cloneState->counts[nextElem->depth] < RPR_COUNT_MAX)
-					cloneState->counts[nextElem->depth]++;
+						if (abs_pos == overlapstart)
+							abs_pos = winstate->currentpos;
+						else
+							abs_pos += winstate->grouptailpos - overlapstart - 1;
+					}
+					break;
+				default:
+					elog(ERROR, "unrecognized frame option state: 0x%x",
+						 winstate->frameOptions);
+					break;
 			}
-
-			/* Exit first (preferred for reluctant) */
-			nfa_route_to_elem(winstate, ctx, cloneState, nextElem,
-							  currentPos);
+			num_reduced_frame = row_is_in_reduced_frame(winobj,
+														winstate->frameheadpos);
+			if (num_reduced_frame < 0)
+				goto out_of_frame;
+			else if (num_reduced_frame > 0)
+				if (relpos >= num_reduced_frame)
+					goto out_of_frame;
+			break;
+		case WINDOW_SEEK_TAIL:
+			/* rejecting relpos > 0 is easy and simplifies code below */
+			if (relpos > 0)
+				goto out_of_frame;
 
 			/*
-			 * If exit path reached FIN, the shortest match is found. Skip
-			 * loop state to prevent longer matches from replacing it.
+			 * RPR cares about frame head pos. Need to call
+			 * update_frameheadpos
 			 */
-			if (ctx->matchedState != savedMatch)
-			{
-				nfa_state_free(winstate, state);
-				return;
-			}
-
-			/* Loop second */
-			nfa_add_state_unique(winstate, ctx, state);
-		}
-		else
-		{
-			/* Clone for loop, original used for exit */
-			cloneState = nfa_state_create(winstate, state->elemIdx,
-										  state->counts, state->isAbsorbable);
-
-			/* Loop first (preferred for greedy) */
-			nfa_add_state_unique(winstate, ctx, cloneState);
+			update_frameheadpos(winstate);
 
-			/* Exit second */
-			state->counts[depth] = 0;
-			state->elemIdx = elem->next;
-			nextElem = &elements[state->elemIdx];
+			update_frametailpos(winstate);
+			abs_pos = winstate->frametailpos - 1 + relpos;
 
 			/*
-			 * When exiting directly to an outer END, increment its iteration
-			 * count.  Simple VARs (min=max=1) handle this via inline advance
-			 * in nfa_match, but quantified VARs bypass that path.
+			 * Account for exclusion option if one is active.  If there is no
+			 * exclusion, we can safely set the mark at the accessed row.  But
+			 * if there is, we can only mark the frame start, because we can't
+			 * be sure how far back in the frame the exclusion might cause us
+			 * to fetch in future.  Furthermore, we have to actually check
+			 * against frameheadpos here, since it's unsafe to try to fetch a
+			 * row before frame start if the mark might be there already.
 			 */
-			if (RPRElemIsEnd(nextElem))
+			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
 			{
-				if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
-					state->counts[nextElem->depth]++;
-			}
-
-			nfa_route_to_elem(winstate, ctx, state, nextElem,
-							  currentPos);
-		}
-	}
-	else if (canLoop)
-	{
-		/* Loop only: keep state as-is */
-		nfa_add_state_unique(winstate, ctx, state);
-	}
-	else if (canExit)
-	{
-		/* Exit only: advance to next element */
-		RPRPatternElement *nextElem;
-
-		state->counts[depth] = 0;
-		state->elemIdx = elem->next;
-		nextElem = &elements[state->elemIdx];
-
-		/* See comment above: increment outer END count for quantified VARs */
-		if (RPRElemIsEnd(nextElem))
-		{
-			if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
-				state->counts[nextElem->depth]++;
-		}
-
-		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
-	}
-}
-
-/*
- * nfa_advance_state
- *
- * Recursively process a single state through epsilon transitions.
- * DFS traversal ensures states are added to ctx->states in lexical order.
- */
-static void
-nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
-				  RPRNFAState *state, int64 currentPos)
-{
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRPatternElement *elem;
-
-	Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements);
+				case 0:
+					/* no adjustment needed */
+					mark_pos = abs_pos;
+					break;
+				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
+					if (abs_pos <= winstate->currentpos &&
+						winstate->currentpos < winstate->frametailpos)
+						abs_pos--;
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				case FRAMEOPTION_EXCLUDE_GROUP:
+					update_grouptailpos(winstate);
+					if (abs_pos < winstate->grouptailpos &&
+						winstate->groupheadpos < winstate->frametailpos)
+					{
+						int64		overlapend = Min(winstate->grouptailpos,
+													 winstate->frametailpos);
 
-	/* Cycle detection: if this elemIdx was already visited in this DFS, bail */
-	if (winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] &
-		((bitmapword) 1 << BITNUM(state->elemIdx)))
-	{
-		nfa_state_free(winstate, state);
-		return;
-	}
-	winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
-		((bitmapword) 1 << BITNUM(state->elemIdx));
+						abs_pos -= overlapend - winstate->groupheadpos;
+					}
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				case FRAMEOPTION_EXCLUDE_TIES:
+					update_grouptailpos(winstate);
+					if (abs_pos < winstate->grouptailpos &&
+						winstate->groupheadpos < winstate->frametailpos)
+					{
+						int64		overlapend = Min(winstate->grouptailpos,
+													 winstate->frametailpos);
 
-	elem = &pattern->elements[state->elemIdx];
+						if (abs_pos == overlapend - 1)
+							abs_pos = winstate->currentpos;
+						else
+							abs_pos -= overlapend - 1 - winstate->groupheadpos;
+					}
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				default:
+					elog(ERROR, "unrecognized frame option state: 0x%x",
+						 winstate->frameOptions);
+					mark_pos = 0;	/* keep compiler quiet */
+					break;
+			}
 
-	switch (elem->varId)
-	{
-		case RPR_VARID_FIN:
-			/* FIN: record match */
-			nfa_add_matched_state(winstate, ctx, state, currentPos);
+			num_reduced_frame = row_is_in_reduced_frame(winobj,
+														winstate->frameheadpos + relpos);
+			if (num_reduced_frame < 0)
+				goto out_of_frame;
+			else if (num_reduced_frame > 0)
+				abs_pos = winstate->frameheadpos + relpos +
+					num_reduced_frame - 1;
 			break;
-
-		case RPR_VARID_ALT:
-			nfa_advance_alt(winstate, ctx, state, elem, currentPos);
+		default:
+			elog(ERROR, "unrecognized window seek type: %d", seektype);
+			abs_pos = mark_pos = 0; /* keep compiler quiet */
 			break;
+	}
 
-		case RPR_VARID_BEGIN:
-			nfa_advance_begin(winstate, ctx, state, elem, currentPos);
-			break;
+	if (!window_gettupleslot(winobj, abs_pos, slot))
+		goto out_of_frame;
 
-		case RPR_VARID_END:
-			nfa_advance_end(winstate, ctx, state, elem, currentPos);
-			break;
+	/* The code above does not detect all out-of-frame cases, so check */
+	if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0)
+		goto out_of_frame;
 
-		default:
-			/* VAR element */
-			nfa_advance_var(winstate, ctx, state, elem, currentPos);
-			break;
-	}
+	if (isout)
+		*isout = false;
+	if (set_mark)
+		WinSetMarkPosition(winobj, mark_pos);
+	return 0;
+
+out_of_frame:
+	if (isout)
+		*isout = true;
+	*isnull = true;
+	return -1;
 }
 
 /*
- * nfa_advance
+ * WinGetFuncArgCurrent
+ *		Evaluate a window function's argument expression on the current row.
  *
- * Advance phase (divergence): transition from all surviving states.
- * Called after match phase with matched VAR states, or at context creation
- * for initial epsilon expansion (with currentPos = startPos - 1).
+ * argno: argument number to evaluate (counted from 0)
+ * isnull: output argument, receives isnull status of result
  *
- * Processes states in order, using recursive DFS to maintain lexical order.
+ * Note: this isn't quite equivalent to WinGetFuncArgInPartition or
+ * WinGetFuncArgInFrame targeting the current row, because it will succeed
+ * even if the WindowObject's mark has been set beyond the current row.
+ * This should generally be used for "ordinary" arguments of a window
+ * function, such as the offset argument of lead() or lag().
  */
-static void
-nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
+Datum
+WinGetFuncArgCurrent(WindowObject winobj, int argno, bool *isnull)
 {
-	RPRNFAState *states = ctx->states;
-	RPRNFAState *state;
-
-	ctx->states = NULL;			/* Will rebuild */
-
-	/* Process each state in lexical order (DFS order from previous advance) */
-	while (states != NULL)
-	{
-		RPRNFAState *savedMatchedState = ctx->matchedState;
-
-		/* Clear visited bitmap before each state's DFS expansion */
-		memset(winstate->nfaVisitedElems, 0,
-			   sizeof(bitmapword) * winstate->nfaVisitedNWords);
+	WindowAggState *winstate;
+	ExprContext *econtext;
 
-		state = states;
-		states = states->next;
-		state->next = NULL;
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
 
-		nfa_advance_state(winstate, ctx, state, currentPos);
+	econtext = winstate->ss.ps.ps_ExprContext;
 
-		/*
-		 * Early termination: if a FIN was newly reached in this advance,
-		 * remaining old states have worse lexical order and can be pruned.
-		 * Only check for new FIN arrivals (not ones from previous rows).
-		 */
-		if (ctx->matchedState != savedMatchedState && states != NULL)
-		{
-			nfa_state_free_list(winstate, states);
-			break;
-		}
-	}
+	econtext->ecxt_outertuple = winstate->ss.ss_ScanTupleSlot;
+	return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
+						econtext, isnull);
 }
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 009c0f5019d..b958280e94c 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -4,7 +4,7 @@
  *	  Row Pattern Recognition pattern compilation for planner
  *
  * This file contains functions for optimizing RPR pattern AST and
- * compiling it to bytecode for execution by WindowAgg.
+ * compiling it to a flat element array for NFA execution by WindowAgg.
  *
  * Key components:
  *   1. Pattern Optimization: Simplifies patterns before compilation
diff --git a/src/include/executor/execRPR.h b/src/include/executor/execRPR.h
new file mode 100644
index 00000000000..7b2b0febb76
--- /dev/null
+++ b/src/include/executor/execRPR.h
@@ -0,0 +1,40 @@
+/*-------------------------------------------------------------------------
+ *
+ * execRPR.h
+ *	  prototypes for execRPR.c (NFA-based Row Pattern Recognition engine)
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/execRPR.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXECRPR_H
+#define EXECRPR_H
+
+#include "nodes/execnodes.h"
+#include "windowapi.h"
+
+/* NFA context management */
+extern RPRNFAContext *ExecRPRStartContext(WindowAggState *winstate,
+										  int64 startPos);
+extern RPRNFAContext *ExecRPRGetHeadContext(WindowAggState *winstate,
+											int64 pos);
+extern void ExecRPRFreeContext(WindowAggState *winstate, RPRNFAContext *ctx);
+
+/* NFA processing */
+extern void ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
+							  bool hasLimitedFrame, int64 frameOffset);
+extern void ExecRPRCleanupDeadContexts(WindowAggState *winstate,
+									   RPRNFAContext *excludeCtx);
+extern void ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos);
+
+/* NFA statistics */
+extern void ExecRPRRecordContextSuccess(WindowAggState *winstate,
+										int64 matchLen);
+extern void ExecRPRRecordContextFailure(WindowAggState *winstate,
+										int64 failedLen);
+
+#endif							/* EXECRPR_H */
-- 
2.50.1 (Apple Git-155)

---

From: Henson Choi <[email-masked]>
Date: Mon, 9 Mar 2026 13:02:02 +0900

Hi Tatsuo,

> Excellnt findings!  BTW, I realized that we cannot use $1 of function
> in PATTERN clause like: A{$1}.
>
> ERROR:  42601: syntax error at or near "$1"
> LINE 10:         PATTERN (A{$1})
>                             ^
> LOCATION:  scanner_yyerror, scan.l:1211
>
> Should we document somewhere?
>

The PATTERN quantifier {n} only accepts Iconst (integer literal) in the
grammar.  When a host variable or function parameter is used (e.g.,
A{$1}), the user gets a generic syntax error.

Oracle accepts broader syntax and validates later, producing an error
at a later stage rather than a syntax error at parse time.

PostgreSQL itself already has precedent for this pattern -- in fact,
within the same window clause, frame offset (ROWS/RANGE/GROUPS) accepts
a_expr in the grammar and then rejects variables in parse analysis via
transformFrameOffset() -> checkExprIsVarFree().

I'd lean against documenting this.  The SQL standard already defines
the quantifier bound as <unsigned integer literal>, so there is nothing
beyond the standard to call out, and documenting what is *not* allowed
tends to raise questions that wouldn't otherwise occur to users.

Rather, I think accepting a broader grammar and validating later would
be the more appropriate response, producing a descriptive error like:

  "argument of bounded quantifier must be an integer literal"

I can either include this in the current patch set or handle it as a
separate follow-up after the main series is committed.  What do you
think?

Regards,
Henson

---

From: SungJun Jang <[email-masked]>
Date: Wed, 11 Mar 2026 13:37:59 +0900

Hi hackers,

I ran a cross-validation test comparing PostgreSQL RPR (current patch) and
Trino MATCH_RECOGNIZE to verify both result correctness and performance
characteristics across different scales.

The dataset consists of two segments where rows are arranged sequentially
as:

A → B → C → D

Each segment contains a fixed distribution of categories A/B/C/D. Category
E does not exist in the dataset and is used to test match failure behavior.

Test scales ranged from 20,000 to 100,000 rows.

All tests were executed using:

Test Environment

Item        | PostgreSQL              | Trino
------------+-------------------------+-----------------------
Version     | 19devel (RPR patch)    | 471
Runtime     | Local                  | Local Docker container
Platform    | Linux x86_64           | Linux x86_64
RPR syntax  | WINDOW clause          | MATCH_RECOGNIZE

Dataset Structure

Each segment contains sequential categories:

A : ~1/3 of rows
B : ~1/3 of rows
C : ~1/3 of rows
D : 1 row

Example at 1x scale:

Segment size : 10,000 rows
Total rows : 20,000

Test Cases

Test 1
Pattern:
PATTERN (A+ B+ C+ D)

Expected result:
1 match per segment (2 total)

Reason:
Data is arranged exactly as A → B → C → D.

Test 2
Pattern:
PATTERN (A+ B+ C+ E)

Expected result:
0 rows (no match)

Reason:
Category E does not exist in the dataset.

Correctness Results

Across all scales both systems returned identical results.

Test 1 → 1 match per segment (2 total)
Test 2 → 0 rows

Performance Results

Scale | Total Rows | PG Test1 | PG Test2 | Trino Test1 | Trino Test2
------+------------+----------+----------+-------------+-------------
1x    |  20,000    | 19 ms    | 17 ms    | ~0.3 s      | ~358 s
2x    |  40,000    | 37 ms    | 37 ms    | ~0.8 s      | ~1,364 s
3x    |  60,000    | 57 ms    | 51 ms    | ~1.5 s      | ~4,424 s
4x    |  80,000    | 73 ms    | 68 ms    | ~2.3 s      | ~9,989 s
5x    | 100,000    | 99 ms    | 92 ms    | ~3.3 s      | ~20,014 s

Note:
Trino measurements are adjusted to remove JVM startup overhead (~2.4s
measured via SELECT 1).

Observations

Match success (Test 1)

Both systems scale approximately linearly because the entire segment is
consumed in a single pass after a successful match.

PostgreSQL shows lower absolute latency.

Match failure (Test 2)

PostgreSQL maintains near-linear scaling.

This appears to be due to its NFA implementation combined with Context
Absorption, which discards redundant matching contexts when they reach the
same NFA state but start later in the input.

Example:

Ctx1 start=0 length=2
Ctx2 start=1 length=1

Ctx1 subsumes Ctx2, so Ctx2 is discarded.

This keeps the number of active contexts small and prevents quadratic
growth.

Measured scaling:

17 ms → 92 ms (1x → 5x)

which is consistent with O(n).

Trino also uses an NFA approach but appears to lack Context Absorption or a
similar optimization.

As a result, it explores matching contexts from all possible start
positions, leading to rapidly increasing backtracking cost.

Measured scaling:

358 s → 20,014 s (1x → 5x)

This exceeds theoretical O(n²) growth and appears closer to O(n²)–O(n³) in
practice, likely compounded by JVM memory and GC overhead.

Summary

Correctness

PostgreSQL RPR and Trino MATCH_RECOGNIZE produce identical matching results
across all tested scales.

Performance

Match success:
Both systems scale roughly linearly.

Match failure:
PostgreSQL maintains O(n) scaling while Trino shows O(n²) or worse behavior.

At the largest tested scale:

PostgreSQL : ~92 ms
Trino : ~20,014 s (≈ 5.6 hours)

PostgreSQL is therefore approximately 217,000× faster in this scenario.

Conclusion

Both systems use NFA-based pattern matching.

The key difference appears to be Context Absorption in PostgreSQL, which
removes redundant matching contexts and guarantees linear scaling even when
patterns fail to match.

This optimization prevents the non-linear performance degradation typically
associated with row pattern recognition.

Best regards

SungJun

2026년 3월 9일 (월) PM 2:22, Tatsuo Ishii <[email-masked]>님이 작성:

> Hi Henson,
> Ok.
> I think handing it as a separate follow-up after the commit is enough
> unless other developers complain.
>
> Best regards,
> --
> Tatsuo Ishii
> SRA OSS K.K.
> English: http://www.sraoss.co.jp/index_en/
> Japanese:http://www.sraoss.co.jp
>

---

From: Henson Choi <[email-masked]>
Date: Wed, 11 Mar 2026 19:32:15 +0900

Hi SungJun,

Thanks for the cross-validation report.

> Absorption,
which discards redundant matching contexts when they reach the same NFA
> state
but start later in the input.
> This keeps the number of active contexts small and prevents quadratic

Your description is accurate. An earlier-started context subsumes
all matches of a later-started context (monotonicity principle),
so the newer one can be safely removed.

However, Context Absorption is not universally applicable. It
requires careful analysis of several factors, and we continue to
refine the conditions under which absorption can be safely applied.

The current scope of absorption is determined by three factors:

1. Pattern structure:
   Currently supported:
   - Simple unbounded quantifiers: A+, (A B)+
   - Leading unbounded quantifier in group: (A+ B)+
   - First iteration of nested unbounded alternation (e.g., ((A+)|(B+))+)
   Planned improvements:
   - Patterns with a prefix before the repeating group (e.g., A (B+) C)
   - Fixed-length iteration groups (e.g., (A B{2})+ where each
     iteration consumes exactly 3 rows)

2. Frame specification:
   - AFTER MATCH SKIP TO NEXT ROW is not eligible (only
     SKIP PAST LAST ROW)
   - Frame end must be UNBOUNDED FOLLOWING

3. DEFINE clause evaluation:
   - Absorption relies on the monotonicity assumption that DEFINE
     conditions produce the same result regardless of starting
     position. Currently DEFINE uses simple boolean conditions
     where this holds. However, future additions such as
     CLASSIFIER() or running aggregates (e.g., RUNNING SUM())
     may introduce context-dependent evaluation that breaks
     this invariance, requiring absorption to be disabled for
     those cases.

That said, many practically useful patterns fall within these
conditions. Here are examples from our regression tests using
synthetic stock data:

  V-shape recovery (decline then rise):
    PATTERN (DECLINE{4,} RISE{4,})
    DEFINE
      DECLINE AS price < PREV(price),
      RISE AS price > PREV(price)

  W-bottom (decline, bounce, re-decline, recovery):
    PATTERN (DECLINE{3,} BOUNCE{3,} DIP{3,} RECOVER{3,})
    DEFINE
      DECLINE AS price < PREV(price),
      BOUNCE AS price > PREV(price),
      DIP AS price < PREV(price),
      RECOVER AS price > PREV(price)

  Consolidation then breakout:
    PATTERN (FLAT{5,} BREAKOUT)
    DEFINE
      FLAT AS price BETWEEN PREV(price) * 0.98 AND PREV(price) * 1.02,
      BREAKOUT AS price > PREV(price) * 1.05

  Price-volume divergence (bearish signal):
    PATTERN (INIT DIVERGE{3,})
    DEFINE
      DIVERGE AS price > PREV(price) AND volume < PREV(volume)

  Volume climax reversal:
    PATTERN (RALLY{3,} CLIMAX SELLOFF{2,})
    DEFINE
      RALLY AS price > PREV(price),
      CLIMAX AS volume > PREV(volume) * 1.5,
      SELLOFF AS price < PREV(price)

All of these use unbounded quantifiers with position-invariant
DEFINE conditions -- exactly the cases where Context Absorption
guarantees O(n) scaling. So despite the constraints, it remains
a powerful optimization for the workloads where RPR is most
commonly applied.

Best regards,
Henson

---

From: Zsolt Parragi <[email-masked]>
Date: Wed, 11 Mar 2026 22:39:58 +0000

Hello

+ /*
+ * Make sure that the row pattern definition search condition is a boolean
+ * expression.
+ */
+ foreach_ptr(TargetEntry, te, defineClause)
+ (void) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");

Isn't this incorrect? I think it should update te->expr, as currently
it is possible to construct queries where this produces unexpected
results.

CREATE TYPE truthyint AS (v int);

CREATE FUNCTION truthyint_to_bool(truthyint) RETURNS boolean AS $$
  SELECT ($1).v <> 0;
$$ LANGUAGE SQL IMMUTABLE STRICT;

CREATE CAST (truthyint AS boolean)
  WITH FUNCTION truthyint_to_bool(truthyint)
  AS ASSIGNMENT;

CREATE TABLE test_coerce (id serial, val truthyint);
INSERT INTO test_coerce VALUES
  (1, ROW(1)),
  (2, ROW(0)),
  (3, ROW(5)),
  (4, ROW(0));

SELECT id, val, COUNT(*) OVER w AS cnt
FROM test_coerce
WINDOW w AS (
    ORDER BY id
    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
    PATTERN (A+)
    DEFINE A AS val
)
ORDER BY id;

Same query provides the correct result with a table that has an actual
boolean column.

---

From: Henson Choi <[email-masked]>
Date: Thu, 12 Mar 2026 08:52:08 +0900

Hi Zsolt, Tatsuo,

> it is possible to construct queries where this produces unexpected

Good catch, Zsolt. You're right — the return value of
coerce_to_boolean() must be assigned back to te->expr, otherwise
any implicit cast (e.g., a user-defined type with an assignment
cast to boolean) is silently discarded.

The fix is straightforward:

    foreach_ptr(TargetEntry, te, defineClause)
        te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr,
"DEFINE");

I've confirmed that your example query produces incorrect results
without the fix (the truthyint value is evaluated as-is without
the cast) and correct results with it.

Patch 13 is attached with the fix and a regression test case based
on your example.

By the way, thank you for joining the TDE hooks discussion when
I was just getting started as a contributor — that meant a lot.
Great work on pg_tde at Percona, and rooting for the extensible
SMGR effort as well. Hope it's all going well!

Tatsuo, please review when you get a chance.

Regards,
Henson
From e47fb8a119cb235665a9e128c8c1871836aff7b8 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 12 Mar 2026 08:41:21 +0900
Subject: [PATCH] Fix coerce_to_boolean result not applied in DEFINE clause

---
 src/backend/parser/parse_rpr.c         |  2 +-
 src/test/regress/expected/rpr_base.out | 33 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 29 ++++++++++++++++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index dff91e439d2..e574b69d9b5 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -392,7 +392,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	 * expression.
 	 */
 	foreach_ptr(TargetEntry, te, defineClause)
-		(void) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
+		te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
 
 	/* mark column origins */
 	markTargetListOrigins(pstate, defineClause);
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index ffd0f8ed9f1..7931ad07d7d 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -285,6 +285,39 @@ ORDER BY id;
   2 |   0
 (2 rows)
 
+-- Implicit cast to boolean via custom type
+CREATE TYPE truthyint AS (v int);
+CREATE FUNCTION truthyint_to_bool(truthyint) RETURNS boolean AS $$
+  SELECT ($1).v <> 0;
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+CREATE CAST (truthyint AS boolean)
+  WITH FUNCTION truthyint_to_bool(truthyint)
+  AS ASSIGNMENT;
+CREATE TABLE rpr_coerce (id int, val truthyint);
+INSERT INTO rpr_coerce VALUES (1, ROW(1)), (2, ROW(0)), (3, ROW(5)), (4, ROW(0));
+SELECT id, val, cnt
+FROM (SELECT id, val,
+             COUNT(*) OVER w AS cnt
+      FROM rpr_coerce
+      WINDOW w AS (
+          ORDER BY id
+          ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+          PATTERN (A+)
+          DEFINE A AS val
+      )
+) s ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 | (1) |   1
+  2 | (0) |   0
+  3 | (5) |   1
+  4 | (0) |   0
+(4 rows)
+
+DROP TABLE rpr_coerce;
+DROP CAST (truthyint AS boolean);
+DROP FUNCTION truthyint_to_bool(truthyint);
+DROP TYPE truthyint;
 DROP TABLE rpr_bool;
 -- Complex expressions
 CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 3d6ca2bc2a2..f8815c7376a 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -231,6 +231,35 @@ WINDOW w AS (
 )
 ORDER BY id;
 
+-- Implicit cast to boolean via custom type
+CREATE TYPE truthyint AS (v int);
+CREATE FUNCTION truthyint_to_bool(truthyint) RETURNS boolean AS $$
+  SELECT ($1).v <> 0;
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+CREATE CAST (truthyint AS boolean)
+  WITH FUNCTION truthyint_to_bool(truthyint)
+  AS ASSIGNMENT;
+
+CREATE TABLE rpr_coerce (id int, val truthyint);
+INSERT INTO rpr_coerce VALUES (1, ROW(1)), (2, ROW(0)), (3, ROW(5)), (4, ROW(0));
+
+SELECT id, val, cnt
+FROM (SELECT id, val,
+             COUNT(*) OVER w AS cnt
+      FROM rpr_coerce
+      WINDOW w AS (
+          ORDER BY id
+          ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+          PATTERN (A+)
+          DEFINE A AS val
+      )
+) s ORDER BY id;
+
+DROP TABLE rpr_coerce;
+DROP CAST (truthyint AS boolean);
+DROP FUNCTION truthyint_to_bool(truthyint);
+DROP TYPE truthyint;
+
 DROP TABLE rpr_bool;
 
 -- Complex expressions
-- 
2.50.1 (Apple Git-155)

---

From: Henson Choi <[email-masked]>
Date: Thu, 12 Mar 2026 09:39:57 +0900

2026년 3월 12일 (목) AM 9:21, Tatsuo Ishii <[email-masked]>님이 작성:

> Hi Zsolt,
>
> Thanks for the report and the test case!
>
> > Good catch, Zsolt. You're right ― the return value of
> Yeah, current patch needs to be fixed. Question is, the output of the
> expression of DEFINE clause must be a strict boolean or, it is allowed
> to accept an expression coercive to boolean?
>
> If we prefer the former, we should use exprType() instead.
>
> Best regards,
> --
> Tatsuo Ishii
> SRA OSS K.K.

---

From: Henson Choi <[email-masked]>
Date: Thu, 12 Mar 2026 09:44:29 +0900

Hi Tatsuo,

Sorry about the empty reply — sent it by accident.

> If we prefer the former, we should use exprType() instead.

Good question. Both the SQL standard and PostgreSQL's existing
behavior point toward allowing coercion.

In the SQL standard (ISO/IEC 19075-5), DEFINE specifies a "Boolean
condition" for each pattern variable. The standard does not suggest
a stricter type requirement on DEFINE than on other boolean contexts
like WHERE or HAVING.

PostgreSQL's WHERE clause already accepts implicit casts to boolean
via coerce_to_boolean(). You can verify this with Zsolt's setup:

    CREATE TABLE test_coerce (id serial, val truthyint);
    INSERT INTO test_coerce VALUES
      (1, ROW(1)), (2, ROW(0)), (3, ROW(5)), (4, ROW(0));

    SELECT id, val FROM test_coerce WHERE val ORDER BY id;
    -- returns rows 1 and 3 (where val casts to true)

As Zsolt noted, the same query works correctly with an actual
boolean column. The issue is specifically that the implicit cast
is not being applied.

All other boolean contexts in the parser (WHERE, HAVING, JOIN/ON,
WHEN, etc.) use coerce_to_boolean() and assign the result back.
I think DEFINE should be consistent with these — it would be
surprising if `DEFINE A AS val` rejected a type that `WHERE val`
accepts.

Regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Fri, 13 Mar 2026 09:53:39 +0900

Hi Tatsuo,

Thank you for your thoughtful feedback on the tle identity concern.

Ok. we should be consistent with WHERE etc. unless the standard
> requres DEFINE stricter. But if the tle is in the target list
> (possibly with resjunk attribute), it could be different from the one
> in the DEFINE because of the casting by coerce_to_boolean(). i.e.:
>
> - tle in the target list has no cast node
> - tle in the DEFINE may have cast node
>
> I don't know how this could affect subsequent RPR process but I would
> like to keep the prerequisites that tle in DEINE and the traget list
> is identical.
>

> is identical.

That is a very good point regarding tle identity.

On the implementation side, the DEFINE clause TargetEntries are
already deep-copied via copyObject in the parser (parse_rpr.c),
so adding a cast node to the DEFINE tle does not affect the
target list tle -- they are independent copies at that point.

On the semantic side, the two serve different purposes: the target
list produces output columns (where the user expects the original
type), while the DEFINE expression is evaluated as a boolean for
pattern matching. This is the same pattern as WHERE, where the
WHERE expression may carry a cast node but the target list for the
same column does not.

That said, if you would prefer to keep them structurally identical,
an alternative would be to reject non-boolean types in DEFINE with
a clear error (using exprType() check). However, this would be
stricter than WHERE/HAVING behavior, which might surprise users.

I would suggest that allowing coercion is the right choice for
consistency, but I am happy to go with whichever approach you
prefer.

On a separate topic, I have found a server crash bug in the current
PREV/NEXT implementation. Nesting navigation functions causes the
backend to crash:

    CREATE TABLE t (id int, price int);
    INSERT INTO t VALUES (1,10),(2,20),(3,15),(4,25),(5,30);

    SELECT * FROM t WINDOW w AS (
        ORDER BY id
        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
        PATTERN (A B+)
        DEFINE B AS prev(prev(b.price)) > 0
    );
    -- server process crashes

The SQL standard (ISO/IEC 19075-5, Section 5.6.2) prohibits nesting
PREV/NEXT within PREV/NEXT. The first argument of PREV/NEXT must
contain a column reference, not another navigation function call.
Currently there is no parser-level check for this, so the nested
call passes through to the executor and crashes.

I have attached a test file (rpr_nav_nesting_test.txt) containing
6 test cases that cover the prohibited nesting patterns:

  - PREV(PREV(price))        direct same-kind nesting
  - NEXT(NEXT(price))        direct same-kind nesting
  - PREV(NEXT(price))        cross nesting
  - NEXT(PREV(price))        cross nesting
  - PREV(ABS(PREV(price)))   nesting hidden inside another function
  - NEXT(ABS(NEXT(price)))   nesting hidden inside another function

All of these currently crash the server. After a fix, they should
all produce a syntax error.

Finally, while reviewing the standard and the current
PREV/NEXT implementation, I have some thoughts on the
design.

The current approach uses all three ExprContext slots
(scan/outer/inner) with varno rewriting, which fixes offsets at
+/-1. The standard requires variable offsets like PREV(price, 3),
and the three-slot model cannot support this.

I think a simpler approach would be to use just one slot
(ecxt_outertuple) and swap it during expression evaluation:

  For `price > PREV(price, 2)`:
        ^^^^    ^^^^  ^^^^^
         |       |      |
         v       v      v
    1. [price]       read from current slot   --> datum_a
    2. [PREV( ,2)]   swap slot to (currentpos - 2)
    3.   [price]     read from swapped slot   --> datum_b
    4. [)]           restore slot
    5. [>]           evaluate datum_a > datum_b

Since step 1 already extracts the value as a Datum before the
swap in step 2, the result is safe. No varno rewriting is needed
-- all column reads just use the one slot, and the swap/restore
controls which row they see.

One thing I noticed is that the SQL standard explicitly requires
all column references inside a navigation function to be qualified
by the same row pattern variable (Section 5.2):

  "All row pattern column references in an aggregate or row
   pattern navigation operation are qualified by the same row
   pattern variable."

This means the navigation argument is always evaluated in a single
row context -- which is exactly what makes the one-slot swap model
work. It seems like the standard may have been designed with this
kind of implementation in mind.

With this approach, the existing varno rewriting infrastructure and
the extra slots for PREV/NEXT would no longer be needed, which
should simplify the code considerably.

I also believe this design would scale well to future extensions.
Variable offsets (PREV(price, N)) would work naturally, and when
we add FIRST/LAST navigation or row pattern variable qualifiers,
only the target row calculation would change -- the swap/restore
mechanism itself would remain the same.

What do you think about this approach?

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Sat, 14 Mar 2026 23:14:26 +0900

Hi Tatsuo,

Looks great. Do you have any idea how to let the existing ExecEvalExpr
> handle the swap/restore mechanism?

Yes. I think the key insight is that ExecEvalExpr compiles expressions into
a flat array of ExprEvalStep operations, which makes the swap/restore
natural
to implement without any changes to the ExecEvalExpr caller interface.

I should also say that this approach was shaped by the constraints you
identified early on -- the three-slot model's limitations and the need
for variable offsets. Without that clear problem framing, I would not
have thought in this direction.

The idea is to introduce two new ExprEvalOp steps:

  EEOP_RPR_NAV_SET     -- save current slot, swap to target row
  EEOP_RPR_NAV_RESTORE -- restore original slot

These two steps bracket the inner expression steps in the flat array.

For `price > PREV(price)`:
  1. EEOP_OUTER_VAR      -- fetch price from current slot --> datum_a
  2. EEOP_RPR_NAV_SET    -- save current slot, swap to (currentpos - 1)
  3. EEOP_OUTER_VAR      -- fetch price from swapped slot --> datum_b
  4. EEOP_RPR_NAV_RESTORE -- restore original slot
  5. EEOP_GT             -- evaluate datum_a > datum_b
  6. EEOP_DONE

For `price > PREV(price, 3)` (with offset):
  1. EEOP_OUTER_VAR      -- fetch price from current slot --> datum_a
  2. EEOP_CONST          -- evaluate offset constant 3 --> datum_off
  3. EEOP_RPR_NAV_SET    -- save current slot, swap to (currentpos - 3)
  4. EEOP_OUTER_VAR      -- fetch price from swapped slot --> datum_b
  5. EEOP_RPR_NAV_RESTORE -- restore original slot
  6. EEOP_GT             -- evaluate datum_a > datum_b
  7. EEOP_DONE

For `price < NEXT(price)`:
  1. EEOP_OUTER_VAR      -- fetch price from current slot --> datum_a
  2. EEOP_RPR_NAV_SET    -- save current slot, swap to (currentpos + 1)
  3. EEOP_OUTER_VAR      -- fetch price from swapped slot --> datum_b
  4. EEOP_RPR_NAV_RESTORE -- restore original slot
  5. EEOP_LT             -- evaluate datum_a < datum_b
  6. EEOP_DONE

ExprState holds the flat array of ExprEvalSteps, and each step carries
its own payload. The RPR navigation steps would store a pointer to the
window execution state in their payload, so that they can access the
current row position and the tuplestore at evaluation time. Since
PREV/NEXT are only permitted in the DEFINE clause, the window execution
state is always valid when these steps are evaluated, which eliminates
the risk of accessing stale or invalid state.

The EEOP_RPR_NAV_SET step would:
  1. Save the current slot for later restore
  2. Compute the target position from currentpos and the direction offset
  3. Fetch the corresponding TupleTableSlot from the tuplestore
  4. Replace econtext->ecxt_outertuple with the target slot

The EEOP_RPR_NAV_RESTORE step would:
  1. Retrieve the previously saved slot
  2. Restore econtext->ecxt_outertuple to the original slot

This design also fits well with the future roadmap for FIRST/LAST
navigation.
Once the match history infrastructure is in place, FIRST/LAST can look
up the first or last row matched by a given pattern variable from the
history, compute the target position from that, and pass it to
EEOP_RPR_NAV_SET -- the save/restore mechanism itself stays the same.

I would like to work on an experimental implementation of this approach,
including the parser-level check to reject nested PREV/NEXT calls as
required by the standard. The nesting check will be part of the same
implementation rather than a separate fix, as the parser structure may
need to be adjusted as part of the redesign anyway.

Since the expression compilation infrastructure is not an area I am
deeply familiar with, it may take some time to get right. I would
appreciate your patience in reviewing it when it is ready.

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Mon, 16 Mar 2026 14:39:47 +0900

Hi Tatsuo,

Thank you for the careful review and excellent questions.

I was not able to find the definition for EEOP_GT. Is this a new one?

You're right -- EEOP_GT does not exist. I oversimplified the example.

In PostgreSQL, comparison operators like `>` are not dedicated opcodes.
They are dispatched through EEOP_FUNCEXPR variants as regular function
calls. For example, `price > PREV(price)`:

  - The parser resolves `>` to the appropriate pg_operator entry
  - The expression compiler emits an EEOP_FUNCEXPR variant
  - At runtime, op->d.func.fn_addr points to the operator function,

I am afraid ExecEvalExpr does not allow this because ExecEvalExpr does
> not accept arguments like WindowObject or WindowAggState that are
> necessary for accessing the tuplestore created for the Window
> aggregate node.

This is the key question. You are right that ExecEvalExpr's signature
does not accept WindowAggState, and I do not intend to change it.

The approach is to pass WindowAggState through the step's own payload,
the same way EEOP_WINDOW_FUNC carries WindowFuncExprState in
op->d.window_func.wfstate. Each ExprEvalStep has a union of per-opcode
payload structs. A new payload for RPR navigation would look like:

    struct
    {
        WindowAggState *winstate;   /* access to tuplestore */
        int64       offset;         /* signed offset: PREV=-1, NEXT=+1 */
    }           rpr_nav;

The expression compiler (ExecInitExprRec) populates this when it
encounters a PREV/NEXT node, storing the pointer to the enclosing
WindowAggState. At runtime, EEOP_RPR_NAV_SET retrieves it from the
payload and uses it to fetch the target row:

    /* pseudo-code for EEOP_RPR_NAV_SET */
    winstate = op->d.rpr_nav.winstate;
    /* 1. save current slot into winstate for later restore */
    winstate->saved_outertuple = econtext->ecxt_outertuple;
    /* 2. compute target position */
    target_pos = winstate->currentpos + op->d.rpr_nav.offset;
    /* 3. fetch target row from tuplestore via winstate */
    target_slot = fetch_row_from_tuplestore(winstate, target_pos);
    /* 4. swap */
    econtext->ecxt_outertuple = target_slot;

    /* pseudo-code for EEOP_RPR_NAV_RESTORE */
    winstate = op->d.rpr_nav.winstate;
    econtext->ecxt_outertuple = winstate->saved_outertuple;

The key difference from EEOP_WINDOW_FUNC: existing steps read
precomputed values, but PREV/NEXT requires fetching a *different*
row at runtime -- because the target depends on the current row
position during pattern matching, which is not known in advance.
The mechanism for passing state is the same (step payload), but
the runtime behavior is a departure from the existing pattern.

I have examined the alternatives carefully:

  - The precomputation approach (EEOP_WINDOW_FUNC style) does not apply
    here: there is no fixed set of values to precompute, because the
    target row depends on the current position during pattern matching.

  - The three-slot model (current implementation) cannot support variable
    offsets like PREV(price, 3), and requires varno rewriting that adds
    complexity elsewhere.

  - A callback-based approach (registering a fetch function in econtext)
    would still require passing WindowAggState somehow, just through a
    different indirection.

The step payload approach is the only path I have found that:

  - Leaves ExecEvalExpr's signature unchanged
  - Eliminates varno rewriting entirely
  - Supports variable offsets naturally
  - Extends to FIRST/LAST navigation later

If anyone sees a cleaner path, I would genuinely welcome the
discussion.

I plan to have an experimental implementation ready before next week.
The initial version will pass WindowAggState broadly; we can
narrow the interface to the minimum required during review.
Let's continue the discussion with actual code.

Best regards,
Henson

---

From: SungJun Jang <[email-masked]>
Date: Tue, 17 Mar 2026 13:59:24 +0900

Hi hackers,

Thank you for the excellent report! I am glad to see that PostgreSQL
> RPR is much faster than Trino, especially in the match failure cases.
>
> Is it possible to share the data generation script and the query for
> PostgreSQL so that I could locally perform the tests?

Here is a self-contained guide to reproduce the RPR ABCD pattern test
locally
using PostgreSQL, Trino, and Oracle.

Requirements:

PostgreSQL 19devel build with the RPR patch applied (local)

Trino and Oracle can be set up via Docker (see README for details):

  https://github.com/assam258-5892/docker-databases

Start all services:

  cd ~/docker-databases && docker compose up -d trino-service oracle-service

Note: docker compose up does not pull images automatically if they are
already
cached locally. To fetch the latest images, run docker compose pull first.

Note: The Oracle image requires an Oracle account. Register at
https://container-registry.oracle.com, accept the license for the database
image, then log in before pulling:

  docker login container-registry.oracle.com

Connect to each database shell:

  Trino:  docker compose exec -it trino-service trino
  Oracle: docker compose exec -it oracle-service sqlplus / as sysdba

Step 1: Create the test table and data

PostgreSQL (1x scale, 20,000 rows):

DROP TABLE IF EXISTS abcd_test;
CREATE TABLE abcd_test AS
SELECT v,
       CASE
           WHEN v % 10000 < 3333 THEN 'A'
           WHEN v % 10000 >= 3333 AND v % 10000 < 6666 THEN 'B'
           WHEN v % 10000 >= 6666 AND v % 10000 < 9999 THEN 'C'
           WHEN v % 10000 = 9999 THEN 'D'
       END AS cat
FROM generate_series(0, 19999) AS v;

ANALYZE abcd_test;

Trino (1x scale, 20,000 rows):

CREATE SCHEMA IF NOT EXISTS memory.test;

DROP TABLE IF EXISTS memory.test.abcd_test;
CREATE TABLE memory.test.abcd_test AS
WITH nums AS (
    SELECT a.v * 10000 + b.v AS v
    FROM UNNEST(sequence(0, 1)) AS a(v)
    CROSS JOIN UNNEST(sequence(0, 9999)) AS b(v)
)
SELECT CAST(v AS INTEGER) AS v,
       CASE
           WHEN v % 10000 < 3333 THEN 'A'
           WHEN v % 10000 >= 3333 AND v % 10000 < 6666 THEN 'B'
           WHEN v % 10000 >= 6666 AND v % 10000 < 9999 THEN 'C'
           WHEN v % 10000 = 9999 THEN 'D'
       END AS cat
FROM nums;

Note: Trino sequence() is limited to 10,000 elements per call, so a CROSS
JOIN
is used. For scale Sx, change sequence(0, 1) to sequence(0, S*2-1).

Oracle (1x scale, 20,000 rows):

DROP TABLE abcd_test PURGE;
CREATE TABLE abcd_test AS
SELECT v,
       CASE
           WHEN MOD(v, 10000) < 3333 THEN 'A'
           WHEN MOD(v, 10000) >= 3333 AND MOD(v, 10000) < 6666 THEN 'B'
           WHEN MOD(v, 10000) >= 6666 AND MOD(v, 10000) < 9999 THEN 'C'
           WHEN MOD(v, 10000) = 9999 THEN 'D'
       END AS cat
FROM (SELECT LEVEL - 1 AS v FROM dual CONNECT BY LEVEL <= 20000);

EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ABCD_TEST');

Verify data distribution (all engines):

SELECT cat, COUNT(*) AS cnt FROM abcd_test GROUP BY cat ORDER BY cat;

Expected (1x): A=6666, B=6666, C=6666, D=2

Step 2: Run Test 1 — A+ B+ C+ D (match expected)

Expected: 2 rows returned (one match per segment)

PostgreSQL:

SELECT match_first, match_last, match_len
FROM (
    SELECT v,
           first_value(v) OVER w AS match_first,
           last_value(v) OVER w AS match_last,
           count(*) OVER w AS match_len
    FROM abcd_test
    WINDOW w AS (
        ORDER BY v
        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
        AFTER MATCH SKIP PAST LAST ROW
        PATTERN (A+ B+ C+ D)
        DEFINE
            A AS cat = 'A',
            B AS cat = 'B',
            C AS cat = 'C',
            D AS cat = 'D'
    )
) result
WHERE match_len > 0;

Trino / Oracle:

SELECT match_first, match_last, match_len
FROM abcd_test
MATCH_RECOGNIZE (
    ORDER BY v
    MEASURES
        FIRST(v) AS match_first,
        LAST(v) AS match_last,
        COUNT(*) AS match_len
    ONE ROW PER MATCH
    AFTER MATCH SKIP PAST LAST ROW
    PATTERN (A+ B+ C+ D)
    DEFINE
        A AS cat = 'A',
        B AS cat = 'B',
        C AS cat = 'C',
        D AS cat = 'D'
) mr;

Note: Trino uses memory.test.abcd_test as the table name.

Expected result (1x):

match_first | match_last | match_len
------------|------------|----------
          0 |       9999 |     10000
      10000 |      19999 |     10000

Step 3: Run Test 2 — A+ B+ C+ E (match failure)

Expected: 0 rows (E does not exist)

Use the same queries as Test 1 with two changes:

PATTERN: (A+ B+ C+ D) → (A+ B+ C+ E)

DEFINE: cat = 'D' → cat = 'E'

Warning: Trino Test 2 at 1x scale takes approximately 5-6 minutes.

Step 4: Scale up (optional)

Re-create the test table at 2x scale (40,000 rows) and then repeat Step 2
and
Step 3.

PostgreSQL (2x scale, 40,000 rows):

DROP TABLE IF EXISTS abcd_test;
CREATE TABLE abcd_test AS
SELECT v,
       CASE
           WHEN v % 20000 < 6666 THEN 'A'
           WHEN v % 20000 >= 6666 AND v % 20000 < 13332 THEN 'B'
           WHEN v % 20000 >= 13332 AND v % 20000 < 19999 THEN 'C'
           WHEN v % 20000 = 19999 THEN 'D'
       END AS cat
FROM generate_series(0, 39999) AS v;

ANALYZE abcd_test;

Trino (2x scale, 40,000 rows):

CREATE SCHEMA IF NOT EXISTS memory.test;

DROP TABLE IF EXISTS memory.test.abcd_test;
CREATE TABLE memory.test.abcd_test AS
WITH nums AS (
    SELECT a.v * 10000 + b.v AS v
    FROM UNNEST(sequence(0, 3)) AS a(v)
    CROSS JOIN UNNEST(sequence(0, 9999)) AS b(v)
)
SELECT CAST(v AS INTEGER) AS v,
       CASE
           WHEN v % 20000 < 6666 THEN 'A'
           WHEN v % 20000 >= 6666 AND v % 20000 < 13332 THEN 'B'
           WHEN v % 20000 >= 13332 AND v % 20000 < 19999 THEN 'C'
           WHEN v % 20000 = 19999 THEN 'D'
       END AS cat
FROM nums;

Oracle (2x scale, 40,000 rows):

DROP TABLE abcd_test PURGE;
CREATE TABLE abcd_test AS
SELECT v,
       CASE
           WHEN MOD(v, 20000) < 6666 THEN 'A'
           WHEN MOD(v, 20000) >= 6666 AND MOD(v, 20000) < 13332 THEN 'B'
           WHEN MOD(v, 20000) >= 13332 AND MOD(v, 20000) < 19999 THEN 'C'
           WHEN MOD(v, 20000) = 19999 THEN 'D'
       END AS cat
FROM (SELECT LEVEL - 1 AS v FROM dual CONNECT BY LEVEL <= 40000);

EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ABCD_TEST');

Expected (2x): A=13332, B=13332, C=13332, D=4

Then run Step 2 and Step 3 as-is.

Warning: Trino Test 2 at 2x scale takes approximately 20-25 minutes.

Please let me know if you encounter any issues reproducing this.

Best regards
SungJun

---

From: Zsolt Parragi <[email-masked]>
Date: Wed, 18 Mar 2026 23:06:59 +0000

Hello

I found one more bug, the following testcase crashes the server:

CREATE TEMP TABLE t (id int, val text);
INSERT INTO t VALUES
  (1,'A'),(2,'B'),
  (3,'A'),(4,'B'),
  (5,'A'),(6,'B'),
  (7,'A'),(8,'B'),
  (9,'X');

SELECT id, val, count(*) OVER w AS match_count
FROM t
WINDOW w AS (
  ORDER BY id
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  INITIAL
  PATTERN (A B (A B){1,2} A B)
  DEFINE
    A AS val = 'A',
    B AS val = 'B'
);

Where the code does

+ child->min += 1;

It should also increment max if it's not infinity.

if (child->max != RPR_QUANTITY_INF)
    child->max += 1;

+ expr = te->expr;
...
+ attno_map((Node *) expr);

It it okay to mutate an object in the plan cache in the executor?
Wouldn't it be better to first copy it?

> consecutive rises, and other common pattern matching use cases.

The data file is not included in the patchset

+ if (wc->frameOptions & FRAMEOPTION_GROUPS)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("FRAME option GROUP is not permitted when row pattern
recognition is used"),
+ errhint("Use: ROWS instead"),
+ parser_errposition(pstate,
+ windef->frameLocation >= 0 ?
+ windef->frameLocation : windef->location)));

Here and at a few other places, shouldn't this use a different error
code, like ERRCODE_WINDOWING_ERROR?

Also a typo, the error message should say GROUPS.

+ elog(ERROR, "PREV/NEXT must have 1 argument but function %d has %d args",
+ func->funcid, nargs);

Isn't this a user triggerable error, which should have a proper error code?

---

From: Henson Choi <[email-masked]>
Date: Thu, 19 Mar 2026 11:28:29 +0900

Hi Tatsuo, Zsolt,

I just confirmed the crash.
>
> Henson, What do you think?
>

Good catch by Zsolt. Your careful review is really helping a lot.

I've fixed both the prefix merge and suffix merge paths in
mergeGroupPrefixSuffix() to also increment child->max. I'll
include the fix with a regression test in the next patch series.

Regarding the suggested fix:

While the quantifier value is int (INT32_MAX = RPR_QUANTITY_INF),
making such repetition counts practically impossible, the +1
approach is not semantically equivalent -- it could silently turn
a finite max into infinity. Instead, I took a fallback approach:
skip the merge entirely when min or max would reach
RPR_QUANTITY_INF after increment, consistent with the overflow
checks in mergeConsecutiveVars/Groups.

> Good point. You are right, the plan cache should be read only.
> However, Henson is working on a different approach and the code will
> not be used any more...
>

Yes, I'm currently working on a slot-based approach (1-slot
PREV/NEXT) that won't need attno_map, so the plan cache mutation
issue will be gone.

> I forggot to include the data file. Please apply attached patch on top
> of v45.
>

Got it, thanks. By the way, how about splitting the test patch
into two -- one for sql + data files and another for expected
output files? The single test patch is getting quite large and
cfbot often fails to apply it.

Right. Maybe ERRCODE_TOO_MANY_ARGUMENTS?

 I'm currently reworking PREV/NEXT to accept 2 arguments, so this
error handling will change. I'll take care of the proper error
codes along with the GROUPS typo and ERRCODE_WINDOWING_ERROR change.

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Sat, 21 Mar 2026 00:36:18 +0900

Hi Tatsuo,

Sorry for sending these before your next revision of v45 --
I'll be busy with a project proposal for the next couple of
weeks, so I wanted to share them before that.

Here are the patches on top of v45, incorporating the fixes and
improvements I mentioned earlier, plus additional changes from a
thorough code review. There are quite a few patches since I went
ahead without prior discussion -- please review each one individually
and feel free to drop any that you disagree with.

Here's a summary of each patch (to be applied on top of v45):

  0001: Fix mergeGroupPrefixSuffix() max increment
        When absorbing prefix/suffix, child->max was not incremented,
        causing incorrect quantifier bounds. (reported by Zsolt)

  0002: Fix RPR error codes and GROUPS typo
        Use ERRCODE_WINDOWING_ERROR instead of ERRCODE_SYNTAX_ERROR
        for semantic window-frame violations. Fix "GROUP" typo to
        "GROUPS" in frame validation error message. (reported by Zsolt)

  0003: Add check_stack_depth() and CHECK_FOR_INTERRUPTS()
        Add stack depth protection to recursive pattern optimization
        and interrupt checks to NFA engine loops.

  0004: Fix window_last_value set_mark during RPR
        Restore set_mark=true in window_last_value (normal behavior)
        and add an RPR-specific override inside WinGetFuncArgInFrame.
        This keeps the RPR workaround localized rather than changing
        the caller's semantics unconditionally.

  0005: Fix row_is_in_reduced_frame in WINDOW_SEEK_TAIL
        Pass frameheadpos directly to row_is_in_reduced_frame
        instead of frameheadpos+relpos. Currently only last_value()
        calls this path with relpos=0 so no actual bug, but the
        old expression would be incorrect for negative relpos.
        Also add a bounds check for future callers.

  0006: Clarify ST_NONE intent
        Add comment explaining ST_NONE = 0 is the default for
        non-RPR windows.

  0007: Clarify inverse transition optimization comment
        Document why RPR disables inverse transition: the reduced
        frame changes row by row.

  0008: Reject unused DEFINE variables
        I know you preferred silently ignoring unused DEFINE
        variables [1], and I agree the standard doesn't mandate
        an error. However, if we later add qualified column
        references (e.g. B AS A.price > 100), B's expression
        depends on A being present. If A is not used in PATTERN
        and the planner silently removes it, B's reference to A
        becomes dangling. I worry that silently allowing this now
        could create forward-compatibility problems once qualified
        references are introduced. For that reason, I'm inclined
        to think rejecting them now may be safer than changing
        behavior later, which would be a user-visible compatibility
        break. This is also consistent with Oracle's behavior
        (ORA-62503), as SungJun reported, and it helps catch user
        typos in DEFINE variable names at parse time.

  0009: Clarify RPR documentation in advanced.sgml
        Improve absorption explanation and clarify non-match row
        aggregation behavior with concrete examples.

  0010: Fix typos in RPR comments and parser README

  0011: Clarify excludeLocation and empty quantifier in gram.y
        Add comments explaining the conditional location assignment
        pattern and the empty quantifier rule.

  0012: Clarify RPR_VARID_MAX definition
        Document varId range 0-250 and reserved control element
        values 252+.

  0013: Move local variables to function scope
        In row_is_in_reduced_frame, move declarations out of
        switch case blocks.

  0014: Reset reduced_frame_map pointer in release_partition
        Set reduced_frame_map = NULL and alloc_sz = 0 to prevent
        dangling pointer after partition context reset.

  0015: Remove redundant list manipulation in nfa_add_matched_state
        Simplify doubly-linked list operation that was duplicated
        by the subsequent ExecRPRFreeContext() call.

  experimental: Implement 1-slot PREV/NEXT navigation
        The slot-based PREV/NEXT approach I mentioned earlier.
        This no longer needs attno_map, so the plan cache mutation
        issue is gone. PREV/NEXT now also accepts an optional
        second argument for offset, e.g. PREV(price, 2).

        Note: all existing regression tests pass with this patch,
        but my understanding of TupleSlot internals and JIT is still
        shallow -- the implementation largely copies surrounding
        patterns, so there may be issues I haven't caught yet. I'd
        like to keep this as a separate patch on top of v46 until it
        is trustworthy enough, rather than folding it into the main
        series. I'll continue reviewing and hardening it.

One design decision regarding PREV/NEXT: the SQL standard
requires that the first argument of PREV/NEXT contain at least
one column reference [2] -- e.g. PREV(1) is a syntax error.
Beyond the standard's rationale (no starting row for offsetting),
there is a practical reason: when the target row falls outside
the frame, PREV should return NULL, but a constant expression
like PREV(42) would still evaluate to 42 since it never reads
from the slot. I think it's correct to follow the standard and
reject PREV/NEXT without a column reference. What do you think?

[1]
https://www.postgresql.org/message-id/20260305.142049.1864331791480656300.ishii%40postgresql.org
[2] ISO/IEC 19075-5, Subclause 5.6.2

Best regards,
Henson
From a9c7017524a14820f07955366cca5e6a2a154319 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 19 Mar 2026 11:37:34 +0900
Subject: [PATCH] Fix mergeGroupPrefixSuffix() to also increment max when
 absorbing prefix/suffix

When mergeGroupPrefixSuffix() found a prefix or suffix that matched a GROUP
node's children, it incremented GROUP's min quantifier but left max unchanged.
This produced invalid quantifier state (min > max) when the GROUP had a finite
max bound.

Fix by also incrementing max when it is finite. Guard both the prefix and
suffix cases with an overflow check (min < RPR_QUANTITY_INF - 1) to prevent
wraparound into the sentinel value.

Add Assert() calls in fillRPRPatternVar(), fillRPRPatternGroup(), and
finalizeRPRPattern() to catch invalid min/max combinations at pattern
compilation time.
---
 src/backend/optimizer/plan/rpr.c  | 33 ++++++++++++++++++++++++-----
 src/test/regress/expected/rpr.out | 35 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 23 ++++++++++++++++++++
 3 files changed, 86 insertions(+), 5 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index b958280e94c..c50cbdc18f1 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -541,13 +541,17 @@ mergeGroupPrefixSuffix(List *children)
 
 				/* Compare with GROUP's (possibly unwrapped) children */
 				if (rprPatternChildrenEqual(prefixElements, groupContent) &&
-					child->min < RPR_QUANTITY_INF)
+					child->min < RPR_QUANTITY_INF - 1 &&
+					(child->max == RPR_QUANTITY_INF ||
+					 child->max < RPR_QUANTITY_INF - 1))
 				{
 					/*
-					 * Match! Merge by incrementing GROUP's min. Remove the
-					 * prefix elements from output.
+					 * Match! Merge by incrementing GROUP's quantifier. Remove
+					 * the prefix elements from output.
 					 */
 					child->min += 1;
+					if (child->max != RPR_QUANTITY_INF)
+						child->max += 1;
 
 					/* Rebuild result without matched prefix */
 					trimmed = NIL;
@@ -595,12 +599,17 @@ mergeGroupPrefixSuffix(List *children)
 				/* Compare with GROUP's children */
 				if (list_length(suffixElements) == groupChildCount &&
 					rprPatternChildrenEqual(suffixElements, groupContent) &&
-					child->min < RPR_QUANTITY_INF)
+					child->min < RPR_QUANTITY_INF - 1 &&
+					(child->max == RPR_QUANTITY_INF ||
+					 child->max < RPR_QUANTITY_INF - 1))
 				{
 					/*
-					 * Match! Absorb suffix by incrementing min and skipping.
+					 * Match! Absorb suffix by incrementing quantifier and
+					 * skipping.
 					 */
 					child->min += 1;
+					if (child->max != RPR_QUANTITY_INF)
+						child->max += 1;
 					skipUntil = suffixStart + groupChildCount;
 
 					/*
@@ -1152,6 +1161,9 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	elem->depth = depth;
 	elem->min = node->min;
 	elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+	Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+		   elem->max >= 1 &&
+		   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
 	elem->next = RPR_ELEMIDX_INVALID;
 	elem->jump = RPR_ELEMIDX_INVALID;
 	if (node->reluctant)
@@ -1191,6 +1203,9 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		elem->depth = depth;
 		elem->min = node->min;
 		elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+			   elem->max >= 1 &&
+			   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
 		elem->next = RPR_ELEMIDX_INVALID;	/* set by finalize */
 		elem->jump = RPR_ELEMIDX_INVALID;	/* set after END */
 		if (node->reluctant)
@@ -1216,6 +1231,9 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		endElem->depth = depth;
 		endElem->min = node->min;
 		endElem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		Assert(endElem->min >= 0 && endElem->min < RPR_QUANTITY_INF &&
+			   endElem->max >= 1 &&
+			   (endElem->max == RPR_QUANTITY_INF || endElem->min <= endElem->max));
 		endElem->next = RPR_ELEMIDX_INVALID;
 		endElem->jump = groupStartIdx;	/* loop to first child */
 		if (node->reluctant)
@@ -1398,6 +1416,11 @@ finalizeRPRPattern(RPRPattern *result)
 
 		if (elem->next == RPR_ELEMIDX_INVALID)
 			elem->next = (i < finIdx - 1) ? i + 1 : finIdx;
+
+		/* Verify quantifier range is valid */
+		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+			   elem->max >= 1 &&
+			   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
 	}
 
 	/* Add FIN marker at the end */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index ecbe835cbb4..e72171050c7 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -588,6 +588,41 @@ SELECT company, tdate, price, count(*) OVER w
  company2 | 07-10-2023 |  1300 |     0
 (20 rows)
 
+-- test prefix/suffix merge optimization with bounded quantifier
+-- Pattern A B (A B){1,2} A B should be optimized to (A B){3,4}
+CREATE TEMP TABLE rpr_t (id int, val text);
+INSERT INTO rpr_t VALUES
+  (1,'A'),(2,'B'),
+  (3,'A'),(4,'B'),
+  (5,'A'),(6,'B'),
+  (7,'A'),(8,'B'),
+  (9,'X');
+SELECT id, val, count(*) OVER w AS match_count
+FROM rpr_t
+WINDOW w AS (
+  ORDER BY id
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP TO NEXT ROW
+  INITIAL
+  PATTERN (A B (A B){1,2} A B)
+  DEFINE
+    A AS val = 'A',
+    B AS val = 'B'
+);
+ id | val | match_count 
+----+-----+-------------
+  1 | A   |           8
+  2 | B   |           0
+  3 | A   |           6
+  4 | B   |           0
+  5 | A   |           0
+  6 | B   |           0
+  7 | A   |           0
+  8 | B   |           0
+  9 | X   |           0
+(9 rows)
+
+DROP TABLE rpr_t;
 -- last_value() should remain consistent
 SELECT company, tdate, price, last_value(price) OVER w
  FROM stock
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b308f8d7cb4..95794d409e1 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -249,6 +249,29 @@ SELECT company, tdate, price, count(*) OVER w
   A AS price > 100
 );
 
+-- test prefix/suffix merge optimization with bounded quantifier
+-- Pattern A B (A B){1,2} A B should be optimized to (A B){3,4}
+CREATE TEMP TABLE rpr_t (id int, val text);
+INSERT INTO rpr_t VALUES
+  (1,'A'),(2,'B'),
+  (3,'A'),(4,'B'),
+  (5,'A'),(6,'B'),
+  (7,'A'),(8,'B'),
+  (9,'X');
+SELECT id, val, count(*) OVER w AS match_count
+FROM rpr_t
+WINDOW w AS (
+  ORDER BY id
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP TO NEXT ROW
+  INITIAL
+  PATTERN (A B (A B){1,2} A B)
+  DEFINE
+    A AS val = 'A',
+    B AS val = 'B'
+);
+DROP TABLE rpr_t;
+
 -- last_value() should remain consistent
 SELECT company, tdate, price, last_value(price) OVER w
  FROM stock
-- 
2.50.1 (Apple Git-155)

From 9abc7a58b48d389e279d1a3676351f36b9ebc6c3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 19 Mar 2026 16:40:35 +0900
Subject: [PATCH] Fix RPR error codes and GROUPS typo in frame validation

Frame validation errors in transformRPR() were reported with
ERRCODE_SYNTAX_ERROR. These are semantic errors detected after parsing,
so ERRCODE_WINDOWING_ERROR is the correct code per SQL standard.

Also fix a typo in the error message: "FRAME option GROUP" ->
"FRAME option GROUPS" to match the actual SQL keyword.
---
 src/backend/parser/parse_rpr.c         | 10 +++++-----
 src/test/regress/expected/rpr_base.out |  8 ++++----
 src/test/regress/sql/rpr_base.sql      |  4 ++--
 3 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index e574b69d9b5..66252cd185e 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -72,15 +72,15 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	/* Frame type must be "ROW" */
 	if (wc->frameOptions & FRAMEOPTION_GROUPS)
 		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("FRAME option GROUP is not permitted when row pattern recognition is used"),
+				(errcode(ERRCODE_WINDOWING_ERROR),
+				 errmsg("FRAME option GROUPS is not permitted when row pattern recognition is used"),
 				 errhint("Use: ROWS instead"),
 				 parser_errposition(pstate,
 									windef->frameLocation >= 0 ?
 									windef->frameLocation : windef->location)));
 	if (wc->frameOptions & FRAMEOPTION_RANGE)
 		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
+				(errcode(ERRCODE_WINDOWING_ERROR),
 				 errmsg("FRAME option RANGE is not permitted when row pattern recognition is used"),
 				 errhint("Use: ROWS instead"),
 				 parser_errposition(pstate,
@@ -107,7 +107,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			   (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING));
 
 		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
+				(errcode(ERRCODE_WINDOWING_ERROR),
 				 errmsg("FRAME must start at CURRENT ROW when row pattern recognition is used"),
 				 errdetail("Current frame starts with %s.", startBound),
 				 errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
@@ -133,7 +133,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			   (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES));
 
 		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
+				(errcode(ERRCODE_WINDOWING_ERROR),
 				 errmsg("EXCLUDE options are not permitted when row pattern recognition is used"),
 				 errdetail("Frame definition includes %s.", excludeType),
 				 errhint("Remove the EXCLUDE clause from the window definition."),
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 7931ad07d7d..50a9e7daea9 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -483,11 +483,11 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME option GROUP is not permitted when row pattern recognition is used
+ERROR:  FRAME option GROUPS is not permitted when row pattern recognition is used
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
 HINT:  Use: ROWS instead
--- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+-- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -656,11 +656,11 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
-ERROR:  FRAME option GROUP is not permitted when row pattern recognition is used
+ERROR:  FRAME option GROUPS is not permitted when row pattern recognition is used
 LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
             ^
 HINT:  Use: ROWS instead
--- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+-- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
 DROP TABLE rpr_frame;
 -- ============================================================
 -- PARTITION BY + FRAME Tests
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index f8815c7376a..e54d54e400a 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -397,7 +397,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+-- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
 
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
@@ -517,7 +517,7 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+-- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
 
 DROP TABLE rpr_frame;
 
-- 
2.50.1 (Apple Git-155)

From 0bea151eb70ae266cf29ee24395e8d178bece917 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 14 Mar 2026 09:44:40 +0900
Subject: [PATCH] Add check_stack_depth() and CHECK_FOR_INTERRUPTS() to RPR
 engine

Several recursive functions in the RPR implementation lacked stack
overflow protection, and the NFA evaluation loop lacked interrupt
handling.

Add check_stack_depth() to all recursive RPR functions:
- execRPR.c: nfa_advance_state() (NFA epsilon-closure DFS)
- rpr.c: optimizeRPRPattern(), scanRPRPatternRecursive(),
  fillRPRPattern(), computeAbsorbabilityRecursive(),
  collectPatternVariablesRecursive()
- parse_rpr.c: validateRPRPatternVarCount()

Add CHECK_FOR_INTERRUPTS() in the main NFA processing loops in
execRPR.c to allow query cancellation during long pattern matches.
---
 src/backend/executor/execRPR.c   | 17 ++++++++++++++++-
 src/backend/optimizer/plan/rpr.c | 11 +++++++++++
 src/backend/parser/parse_rpr.c   |  3 +++
 3 files changed, 30 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 0d5ba7516e9..06934b95da3 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -24,6 +24,7 @@
 
 #include "executor/execRPR.h"
 #include "executor/executor.h"
+#include "miscadmin.h"
 #include "optimizer/rpr.h"
 #include "utils/memutils.h"
 
@@ -1994,6 +1995,8 @@ nfa_update_absorption_flags(RPRNFAContext *ctx)
 	 */
 	for (state = ctx->states; state != NULL; state = state->next)
 	{
+		CHECK_FOR_INTERRUPTS();
+
 		if (state->isAbsorbable)
 			hasAbsorbable = true;
 		else
@@ -2035,6 +2038,8 @@ nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *new
 
 		for (olderState = older->states; olderState != NULL; olderState = olderState->next)
 		{
+			CHECK_FOR_INTERRUPTS();
+
 			/* Covering state must also be absorbable */
 			if (olderState->isAbsorbable &&
 				olderState->elemIdx == newerState->elemIdx &&
@@ -2195,6 +2200,8 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 	{
 		RPRPatternElement *elem = &elements[state->elemIdx];
 
+		CHECK_FOR_INTERRUPTS();
+
 		nextState = state->next;
 
 		if (RPRElemIsVar(elem))
@@ -2670,6 +2677,9 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
 
 	Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements);
 
+	/* Protect against stack overflow for deeply complex patterns */
+	check_stack_depth();
+
 	/* Cycle detection: if this elemIdx was already visited in this DFS, bail */
 	if (winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] &
 		((bitmapword) 1 << BITNUM(state->elemIdx)))
@@ -2722,13 +2732,15 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 {
 	RPRNFAState *states = ctx->states;
 	RPRNFAState *state;
+	RPRNFAState *savedMatchedState;
 
 	ctx->states = NULL;			/* Will rebuild */
 
 	/* Process each state in lexical order (DFS order from previous advance) */
 	while (states != NULL)
 	{
-		RPRNFAState *savedMatchedState = ctx->matchedState;
+		CHECK_FOR_INTERRUPTS();
+		savedMatchedState = ctx->matchedState;
 
 		/* Clear visited bitmap before each state's DFS expansion */
 		memset(winstate->nfaVisitedElems, 0,
@@ -2912,6 +2924,9 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 	RPRNFAContext *ctx;
 	bool	   *varMatched = winstate->nfaVarMatched;
 
+	/* Allow query cancellation once per row for simple/low-state patterns */
+	CHECK_FOR_INTERRUPTS();
+
 	/*
 	 * Phase 1: Match all contexts (convergence).  Evaluate VAR elements,
 	 * update counts, remove dead states.
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index c50cbdc18f1..0b4d93b933e 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -37,6 +37,7 @@
 
 #include "postgres.h"
 
+#include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "optimizer/rpr.h"
 
@@ -933,6 +934,8 @@ optimizeRPRPattern(RPRPatternNode *pattern)
 	/* Pattern nodes from parser are never NULL */
 	Assert(pattern != NULL);
 
+	check_stack_depth();
+
 	switch (pattern->nodeType)
 	{
 		case RPR_PATTERN_VAR:
@@ -991,6 +994,8 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 	/* Pattern nodes from parser are never NULL */
 	Assert(node != NULL);
 
+	check_stack_depth();
+
 	/* Check recursion depth limit before overflow occurs */
 	if (depth >= RPR_DEPTH_MAX)
 		ereport(ERROR,
@@ -1367,6 +1372,8 @@ fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 	/* Pattern nodes from parser are never NULL */
 	Assert(node != NULL);
 
+	check_stack_depth();
+
 	switch (node->nodeType)
 	{
 		case RPR_PATTERN_SEQ:
@@ -1609,6 +1616,8 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
 {
 	RPRPatternElement *elem = &pattern->elements[startIdx];
 
+	check_stack_depth();
+
 	if (RPRElemIsAlt(elem))
 	{
 		/* ALT: recursively check each branch */
@@ -1716,6 +1725,8 @@ collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 
 	Assert(node != NULL);
 
+	check_stack_depth();
+
 	switch (node->nodeType)
 	{
 		case RPR_PATTERN_VAR:
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 66252cd185e..92fef2d9ba7 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -25,6 +25,7 @@
 
 #include "postgres.h"
 
+#include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/rpr.h"
@@ -175,6 +176,8 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
 
+	check_stack_depth();
+
 	switch (node->nodeType)
 	{
 		case RPR_PATTERN_VAR:
-- 
2.50.1 (Apple Git-155)

From ddf11ac02969b003d1f9b4296d2806dd64c88469 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 13:39:20 +0900
Subject: [PATCH] Fix window_last_value set_mark to not advance mark during RPR

window_last_value passes set_mark=true to WinGetFuncArgInFrame, which is
correct for normal use: advancing the mark allows the tuplestore to discard
rows no longer needed, reducing memory usage.

However, when RPR is active, the reduced frame changes row by row. Advancing
the mark would prevent revisiting earlier rows that still fall within a future
row's reduced frame, producing incorrect results.

Rather than requiring each caller to know about RPR, add an override in
WinGetFuncArgInFrame: if RPR is defined, force set_mark=false regardless
of what the caller passed. This keeps the fix localized and transparent
to existing callers.
---
 src/backend/executor/nodeWindowAgg.c | 7 +++++++
 src/backend/utils/adt/windowfuncs.c  | 2 +-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 0a9ba5bd4e7..d3ce9897a4f 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4700,6 +4700,13 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 	econtext = winstate->ss.ps.ps_ExprContext;
 	slot = winstate->temp_slot_1;
 
+	/*
+	 * When RPR is active, the reduced frame changes row by row, so we must
+	 * not advance the mark — doing so would prevent revisiting earlier rows.
+	 */
+	if (rpr_is_defined(winstate))
+		set_mark = false;
+
 	if (winobj->ignore_nulls == IGNORE_NULLS)
 		return ignorenulls_getfuncarginframe(winobj, argno, relpos, seektype,
 											 set_mark, isnull, isout);
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index efb60c99052..74ef109f72e 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -682,7 +682,7 @@ window_last_value(PG_FUNCTION_ARGS)
 
 	WinCheckAndInitializeNullTreatment(winobj, true, fcinfo);
 	result = WinGetFuncArgInFrame(winobj, 0,
-								  0, WINDOW_SEEK_TAIL, false,
+								  0, WINDOW_SEEK_TAIL, true,
 								  &isnull, NULL);
 	if (isnull)
 		PG_RETURN_NULL();
-- 
2.50.1 (Apple Git-155)

From 40a5d3ae0b3f76ffb2ad73100d83d4efb2a3fcc6 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 14:14:17 +0900
Subject: [PATCH] Fix row_is_in_reduced_frame argument and add bounds check in
 WINDOW_SEEK_TAIL

In WinGetSlotInFrame, the WINDOW_SEEK_TAIL case passed frameheadpos+relpos
to row_is_in_reduced_frame to determine the reduced frame size. Since relpos
is non-positive, this queries a position at or before the frame head. When
relpos is negative the queried position falls outside the frame entirely,
which is invalid and could trigger internal Assert failures.

The correct argument is frameheadpos: the reduced frame always starts there,
and its size N gives the tail position as frameheadpos+N-1. Pass frameheadpos
directly instead.

Also add a bounds check (-relpos >= N -> out_of_frame) to handle future
callers that may pass negative relpos values. Currently only last_value()
calls this path with relpos=0, so the check has no effect at present.
---
 src/backend/executor/nodeWindowAgg.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index d3ce9897a4f..c92235c54c7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4906,12 +4906,16 @@ WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
 			}
 
 			num_reduced_frame = row_is_in_reduced_frame(winobj,
-														winstate->frameheadpos + relpos);
+														winstate->frameheadpos);
 			if (num_reduced_frame < 0)
 				goto out_of_frame;
 			else if (num_reduced_frame > 0)
+			{
+				if (-relpos >= num_reduced_frame)
+					goto out_of_frame;
 				abs_pos = winstate->frameheadpos + relpos +
 					num_reduced_frame - 1;
+			}
 			break;
 		default:
 			elog(ERROR, "unrecognized window seek type: %d", seektype);
-- 
2.50.1 (Apple Git-155)

From e267e4f1e40bd6919efb4ebef769e60da3a7ac4e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 16:59:21 +0900
Subject: [PATCH] Clarify ST_NONE intent in RPSkipTo enum and WindowClause
 initialization

ST_NONE serves as the sentinel value (0) indicating that a WindowClause
has no RPR AFTER MATCH clause. This was implicit via palloc0 zero-filling
but not documented.

Add a comment to the ST_NONE enum value explaining its role as the default
for non-RPR windows, and add an explicit assignment in
transformWindowDefinitions() to make the intent clear in code.
---
 src/backend/parser/parse_clause.c | 2 ++
 src/include/nodes/parsenodes.h    | 3 ++-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index b30c22933ec..3796a69ac3a 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -2889,6 +2889,8 @@ transformWindowDefinitions(ParseState *pstate,
 		 * And prepare the new WindowClause.
 		 */
 		wc = makeNode(WindowClause);
+		wc->rpSkipTo = ST_NONE; /* ST_NONE marks this as a non-RPR window;
+								 * overridden by transformRPR() if RPR is used */
 		wc->name = windef->name;
 		wc->refname = windef->refname;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 432b0573990..c5bf2ce80bf 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -583,7 +583,8 @@ typedef struct SortBy
  */
 typedef enum RPSkipTo
 {
-	ST_NONE,					/* AFTER MATCH omitted */
+	ST_NONE,					/* no AFTER MATCH clause; default for non-RPR
+								 * windows */
 	ST_NEXT_ROW,				/* SKIP TO NEXT ROW */
 	ST_PAST_LAST_ROW,			/* SKIP TO PAST LAST ROW */
 } RPSkipTo;
-- 
2.50.1 (Apple Git-155)

From 124119ef649d05bcd3e20f5e5a1c353a186f99db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 19:01:43 +0900
Subject: [PATCH] Clarify why RPR disables inverse transition optimization

When RPR is active, the reduced frame depends on pattern matching results
which can differ entirely from row to row. This makes it impossible to
use the inverse transition function to incrementally remove rows from the
aggregate state, so a full restart is required for every row.

Add a comment explaining this reasoning at the restart decision logic.
---
 src/backend/executor/nodeWindowAgg.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index c92235c54c7..d144fa39375 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -853,7 +853,9 @@ eval_windowaggregates(WindowAggState *winstate)
 	 *	   transition function, or
 	 *	 - we have an EXCLUSION clause, or
 	 *	 - if the new frame doesn't overlap the old one
-	 *   - if RPR is enabled
+	 *   - if RPR (Row Pattern Recognition) is enabled, because the reduced
+	 *     frame depends on pattern matching results which can differ entirely
+	 *     from row to row, making inverse transition optimization inapplicable
 	 *
 	 * Note that we don't strictly need to restart in the last case, but if
 	 * we're going to remove all rows from the aggregation anyway, a restart
-- 
2.50.1 (Apple Git-155)

From 213650dbee9962492bfdd65ce3ed61fee674e5b4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 19:42:27 +0900
Subject: [PATCH] Reject DEFINE variables not used in PATTERN

DEFINE variables that do not appear in PATTERN are now rejected with
an error at parse time. When DEFINE supports qualified column references
(e.g. A.price), a DEFINE expression may reference other pattern variables.
If the planner silently removes unused variables, those references become
dangling, leading to incorrect evaluation. Rejecting at parse time avoids
this class of bugs entirely.

Remove filterDefineClause() from the planner and replace it with
buildDefineVariableList(), which builds the variable name list without
filtering. Update regression tests accordingly.
---
 src/backend/executor/execRPR.c          |  5 +-
 src/backend/optimizer/plan/createplan.c | 15 ++---
 src/backend/optimizer/plan/rpr.c        | 34 ++--------
 src/backend/parser/parse_rpr.c          |  9 ++-
 src/include/optimizer/rpr.h             |  4 +-
 src/test/regress/expected/rpr_base.out  | 82 +++++--------------------
 src/test/regress/sql/rpr_base.sql       | 46 ++------------
 7 files changed, 43 insertions(+), 152 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 06934b95da3..a0a462256ad 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -204,8 +204,7 @@
  * IV-1. Entry Point
  *
  *   create_windowagg_plan() (createplan.c)
- *     +-- collectPatternVariables()   Collect variable names
- *     +-- filterDefineClause()        Remove unused DEFINE entries
+ *     +-- buildDefineVariableList()    Build variable name list from DEFINE
  *     +-- buildRPRPattern()           NFA compilation (6 phases)
  *
  * IV-2. The 6 Phases of buildRPRPattern()
@@ -1297,7 +1296,7 @@
  *   transformRPR                  parse_rpr.c           Parser entry point
  *   transformDefineClause         parse_rpr.c           DEFINE transformation
  *   collectPatternVariables       rpr.c                 Variable collection
- *   filterDefineClause            rpr.c                 DEFINE filtering
+ *   buildDefineVariableList       rpr.c                 DEFINE variable list
  *   buildRPRPattern               rpr.c                 NFA compilation main
  *   optimizeRPRPattern            rpr.c                 AST optimization
  *   fillRPRPattern                rpr.c                 NFA element generation
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index da71e7f3d64..9ac24cc222d 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2539,19 +2539,16 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		ordNumCols++;
 	}
 
-	/* Build RPR pattern and filter defineClause */
+	/* Build RPR pattern and defineVariableList */
 	if (wc->rpPattern)
 	{
-		List	   *patternVars;
-
 		/*
-		 * Filter defineClause to include only variables used in PATTERN. This
-		 * eliminates unnecessary DEFINE evaluations at runtime.
+		 * Build defineVariableList from defineClause.  The parser already
+		 * rejects DEFINE variables not used in PATTERN, so no filtering is
+		 * needed.
 		 */
-		patternVars = collectPatternVariables(wc->rpPattern);
-		filteredDefineClause = filterDefineClause(wc->defineClause,
-												  patternVars,
-												  &defineVariableList);
+		buildDefineVariableList(wc->defineClause, &defineVariableList);
+		filteredDefineClause = wc->defineClause;
 
 		/* Compile and optimize RPR patterns */
 		compiledPattern = buildRPRPattern(wc->rpPattern,
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 0b4d93b933e..85b1a00d095 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1771,50 +1771,28 @@ collectPatternVariables(RPRPatternNode *pattern)
 }
 
 /*
- * filterDefineClause
- *		Filter defineClause to include only variables used in PATTERN.
+ * buildDefineVariableList
+ *		Build defineVariableList from defineClause.
  *
- * This eliminates unnecessary DEFINE evaluations at runtime.
- * Also builds defineVariableList from the filtered result.
+ * The parser already ensures that all DEFINE variables appear in PATTERN,
+ * so no filtering is needed here.
  *
- * Returns filtered defineClause (list of TargetEntry).
  * Sets *defineVariableList to list of variable names (String nodes).
  */
-List *
-filterDefineClause(List *defineClause, List *patternVars,
-				   List **defineVariableList)
+void
+buildDefineVariableList(List *defineClause, List **defineVariableList)
 {
-	List	   *filteredDefineClause = NIL;
 	ListCell   *lc;
-	ListCell   *lc2;
 
 	*defineVariableList = NIL;
 
-	/* Filter defineClause: keep only variables used in PATTERN */
 	foreach(lc, defineClause)
 	{
 		TargetEntry *te = (TargetEntry *) lfirst(lc);
 
-		foreach(lc2, patternVars)
-		{
-			if (strcmp(strVal(lfirst(lc2)), te->resname) == 0)
-			{
-				filteredDefineClause = lappend(filteredDefineClause, te);
-				break;
-			}
-		}
-	}
-
-	/* Build defineVariableList from filtered defineClause */
-	foreach(lc, filteredDefineClause)
-	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		*defineVariableList = lappend(*defineVariableList,
 									  makeString(pstrdup(te->resname)));
 	}
-
-	return filteredDefineClause;
 }
 
 /*
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 92fef2d9ba7..55283ab4bbe 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -244,8 +244,11 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 				}
 			}
 			if (!found)
-				*varNames = lappend(*varNames,
-									makeString(pstrdup(rt->name)));
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+								rt->name),
+						 parser_errposition(pstate, rt->location)));
 		}
 	}
 }
@@ -265,7 +268,7 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  *   6. Marks column origins and assigns collation information
  *
  * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
- * Variables in DEFINE but not in PATTERN are filtered out by the planner.
+ * Variables in DEFINE but not in PATTERN are rejected as an error.
  *
  * XXX Pattern variable qualified column references in DEFINE (e.g.
  * "A.price") are not yet supported.  Currently rejected by
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index fa2d075925c..f93a128096b 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -52,8 +52,8 @@
 #define RPRElemCanSkip(e)		((e)->min == 0)
 
 extern List *collectPatternVariables(RPRPatternNode *pattern);
-extern List *filterDefineClause(List *defineClause, List *patternVars,
-								List **defineVariableList);
+extern void buildDefineVariableList(List *defineClause,
+									List **defineVariableList);
 extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 								   RPSkipTo rpSkipTo, int frameOptions);
 
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 50a9e7daea9..3168468d0ae 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -353,12 +353,9 @@ WINDOW w AS (
     DEFINE A AS id > 0, B AS id > 5  -- B not in pattern
 )
 ORDER BY id;
- id | cnt 
-----+-----
-  1 |   2
-  2 |   0
-(2 rows)
-
+ERROR:  DEFINE variable "b" is not used in PATTERN
+LINE 7:     DEFINE A AS id > 0, B AS id > 5  -- B not in pattern
+                                ^
 DROP TABLE rpr_unused;
 -- ============================================================
 -- FRAME Options Tests
@@ -2860,9 +2857,9 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0, B AS B.val > 0
 );
-ERROR:  pattern variable qualified column reference "b.val" is not supported in DEFINE clause
+ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
-                                      ^
+                                 ^
 -- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
 -- FROM-clause range variable qualified name: not allowed (prohibited by §6.5)
 SELECT COUNT(*) OVER w
@@ -2941,12 +2938,9 @@ WINDOW w AS (
     DEFINE A AS val > 0, B AS val > 5, C AS val > 10
 )
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   2
-  2 |  20 |   0
-(2 rows)
-
+ERROR:  DEFINE variable "b" is not used in PATTERN
+LINE 7:     DEFINE A AS val > 0, B AS val > 5, C AS val > 10
+                                 ^
 DROP TABLE rpr_err;
 -- NULL handling
 CREATE TABLE rpr_null (id INT, val INT);
@@ -5670,7 +5664,7 @@ DROP TABLE rpr_stress;
 -- Tests for error conditions in rpr.c
 CREATE TABLE rpr_errors (id INT, val INT);
 INSERT INTO rpr_errors VALUES (1, 10), (2, 20);
--- Test: PATTERN variable without DEFINE (A), DEFINE variable not in PATTERN (B)
+-- Test: DEFINE variable not in PATTERN (error)
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -5679,55 +5673,11 @@ WINDOW w AS (
     DEFINE
       B AS TRUE
 );
- id | val | count 
-----+-----+-------
-  1 |  10 |     0
-  2 |  20 |     0
-(2 rows)
-
--- Expected: Success - A is implicitly TRUE, B is filtered out
--- Test: 3 variables in PATTERN, 253 in DEFINE (DEFINE filtering test)
-SELECT COUNT(*) OVER w FROM rpr_errors
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3)
-    DEFINE
-    V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
-    V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
-    V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0,
-    V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0,
-    V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0,
-    V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0,
-    V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0,
-    V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0,
-    V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0,
-    V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0,
-    V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0,
-    V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0,
-    V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0,
-    V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0,
-    V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0,
-    V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0,
-    V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0,
-    V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0,
-    V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0,
-    V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0,
-    V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
-    V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
-    V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0, V252 AS val > 0, V253 AS val > 0
-);
- count 
--------
-     0
-     0
-(2 rows)
-
--- Expected: Success - unused DEFINE variables are filtered out
--- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
+ERROR:  DEFINE variable "b" is not used in PATTERN
+LINE 7:       B AS TRUE
+              ^
+-- Expected: Error - B is not used in PATTERN
+-- Test: 251 variables in PATTERN and DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -5759,7 +5709,7 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
     V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0, V252 AS val > 0
+    V251 AS val > 0
 );
  count 
 -------
@@ -5767,7 +5717,7 @@ WINDOW w AS (
      0
 (2 rows)
 
--- Expected: Success - unused DEFINE variables are filtered out
+-- Expected: Success - exactly at RPR_VARID_MAX boundary
 -- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e54d54e400a..cf6c062ae85 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -3639,7 +3639,7 @@ DROP TABLE rpr_stress;
 CREATE TABLE rpr_errors (id INT, val INT);
 INSERT INTO rpr_errors VALUES (1, 10), (2, 20);
 
--- Test: PATTERN variable without DEFINE (A), DEFINE variable not in PATTERN (B)
+-- Test: DEFINE variable not in PATTERN (error)
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -3648,45 +3648,9 @@ WINDOW w AS (
     DEFINE
       B AS TRUE
 );
--- Expected: Success - A is implicitly TRUE, B is filtered out
+-- Expected: Error - B is not used in PATTERN
 
--- Test: 3 variables in PATTERN, 253 in DEFINE (DEFINE filtering test)
-SELECT COUNT(*) OVER w FROM rpr_errors
-WINDOW w AS (
-    ORDER BY id
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3)
-    DEFINE
-    V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
-    V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
-    V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0,
-    V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0,
-    V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0,
-    V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0,
-    V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0,
-    V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0,
-    V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0,
-    V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0,
-    V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0,
-    V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0,
-    V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0,
-    V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0,
-    V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0,
-    V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0,
-    V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0,
-    V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0,
-    V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0,
-    V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0,
-    V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
-    V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
-    V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0, V252 AS val > 0, V253 AS val > 0
-);
--- Expected: Success - unused DEFINE variables are filtered out
-
--- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
+-- Test: 251 variables in PATTERN and DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -3718,9 +3682,9 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
     V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0, V252 AS val > 0
+    V251 AS val > 0
 );
--- Expected: Success - unused DEFINE variables are filtered out
+-- Expected: Success - exactly at RPR_VARID_MAX boundary
 
 -- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
-- 
2.50.1 (Apple Git-155)

From c4943f2f4fbe264238b806e49e3ab486e15534dc Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 20:45:20 +0900
Subject: [PATCH] Clarify RPR documentation in advanced.sgml
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Make the absorption optimization paragraph explicitly reference the
O(n²) complexity it mitigates, rather than using an ambiguous "this"
that could be misread as referring to the pattern simplification
paragraph immediately above.

Also clarify the aggregate behavior description for non-starting rows:
replace the vague "NULL or 0 depending on its aggregation definition"
with concrete examples (count() returns 0, sum() returns NULL).
---
 doc/src/sgml/advanced.sgml | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 3e696eefc66..1336f2daa14 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -585,10 +585,11 @@ DEFINE
     rows which satisfies the PATTERN is found, in the starting row all columns
     or functions are shown in the target list. Note that aggregations only
     look into the matched rows, rather than the whole frame. On the second or
-    subsequent rows all window functions are shown as NULL. Aggregates are
-    NULL or 0 depending on its aggregation definition. A count() aggregate
-    shows 0. For rows that do not match on the PATTERN, columns are shown AS
-    NULL too. Example of a <literal>SELECT</literal> using
+    subsequent rows all window functions are shown as NULL. Aggregates on
+    non-starting rows return their initial value: for example,
+    <function>count()</function> returns 0 and <function>sum()</function>
+    returns NULL. For rows that do not match the PATTERN, columns are shown
+    as NULL too. Example of a <literal>SELECT</literal> using
     the <literal>DEFINE</literal> and <literal>PATTERN</literal> clause is as
     follows.
 
@@ -653,7 +654,8 @@ FROM stock
    </para>
 
    <para>
-    To mitigate this, <productname>PostgreSQL</productname> employs
+    To mitigate the O(n<superscript>2</superscript>) complexity described
+    above, <productname>PostgreSQL</productname> also employs
     a context absorption optimization. When a pattern starts with a greedy
     unbounded element, newer matching contexts cannot produce longer matches
     than older contexts. By detecting and eliminating these redundant
-- 
2.50.1 (Apple Git-155)

From ce9d9867146c9d1fade273ef5b3a9fe15279200c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 21:15:03 +0900
Subject: [PATCH] Fix typos in RPR comments and parser README

Fix "do lopp" to "do loop" and "successfullt" to "successfully" in
nodeWindowAgg.c. Also fix tab-space mix for the parse_rpr.c entry
in parser/README.
---
 src/backend/executor/nodeWindowAgg.c | 7 ++++---
 src/backend/parser/README            | 2 +-
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index d144fa39375..942f071b457 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3729,7 +3729,7 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
 		 * contiguous.  So we can do the check followings safely. Note,
 		 * however, it is possible that a row is out of reduced frame if
 		 * there's a NULL in the middle. So we need to check it in the
-		 * following do lopp.
+		 * following do loop.
 		 */
 		if (seektype == WINDOW_SEEK_HEAD && relpos >= num_reduced_frame)
 			goto out_of_frame;
@@ -4704,7 +4704,8 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 
 	/*
 	 * When RPR is active, the reduced frame changes row by row, so we must
-	 * not advance the mark — doing so would prevent revisiting earlier rows.
+	 * not advance the mark — doing so would prevent revisiting earlier
+	 * rows.
 	 */
 	if (rpr_is_defined(winstate))
 		set_mark = false;
@@ -4739,7 +4740,7 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
  * isout: output argument, set to indicate whether target row position
  *		is out of frame (can pass NULL if caller doesn't care about this)
  *
- * Returns 0 if we successfullt got the slot. false if out of frame.
+ * Returns 0 if we successfully got the slot. false if out of frame.
  * (also isout is set)
  */
 static int
diff --git a/src/backend/parser/README b/src/backend/parser/README
index 2baffa9517e..22a5e91c8cf 100644
--- a/src/backend/parser/README
+++ b/src/backend/parser/README
@@ -26,7 +26,7 @@ parse_node.c	create nodes for various structures
 parse_oper.c	handle operators in expressions
 parse_param.c	handle Params (for the cases used in the core backend)
 parse_relation.c support routines for tables and column handling
-parse_rpr.c	    handle Row Pattern Recognition
+parse_rpr.c	handle Row Pattern Recognition
 parse_target.c	handle the result list of the query
 parse_type.c	support routines for data type handling
 parse_utilcmd.c	parse analysis for utility commands (done at execution time)
-- 
2.50.1 (Apple Git-155)

From 7b9d7fb735a467ff391898ed4ebc11bd6ef315fa Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 21:21:58 +0900
Subject: [PATCH] Add clarifying comments for excludeLocation and empty
 quantifier in gram.y

Add comments explaining that excludeLocation is set to -1 when no
EXCLUDE clause is present (opt_window_exclusion_clause returns 0),
and that the EMPTY quantifier rule's @$ location is unused since
min=max=1 never produces an error.
---
 src/backend/parser/gram.y | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9dcac63bdc7..6d390258532 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -16757,6 +16757,7 @@ opt_frame_clause:
 					n->frameOptions |= FRAMEOPTION_NONDEFAULT | FRAMEOPTION_RANGE;
 					n->frameOptions |= $3;
 					n->frameLocation = @1;
+					/* -1 when no EXCLUDE clause (opt_window_exclusion_clause returns 0) */
 					n->excludeLocation = ($3 != 0) ? @3 : -1;
 					$$ = n;
 				}
@@ -16767,6 +16768,7 @@ opt_frame_clause:
 					n->frameOptions |= FRAMEOPTION_NONDEFAULT | FRAMEOPTION_ROWS;
 					n->frameOptions |= $3;
 					n->frameLocation = @1;
+					/* -1 when no EXCLUDE clause (opt_window_exclusion_clause returns 0) */
 					n->excludeLocation = ($3 != 0) ? @3 : -1;
 					$$ = n;
 				}
@@ -16777,6 +16779,7 @@ opt_frame_clause:
 					n->frameOptions |= FRAMEOPTION_NONDEFAULT | FRAMEOPTION_GROUPS;
 					n->frameOptions |= $3;
 					n->frameLocation = @1;
+					/* -1 when no EXCLUDE clause (opt_window_exclusion_clause returns 0) */
 					n->excludeLocation = ($3 != 0) ? @3 : -1;
 					$$ = n;
 				}
@@ -17069,7 +17072,9 @@ row_pattern_primary:
 		;
 
 row_pattern_quantifier_opt:
-			/* EMPTY */				{ $$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner); }
+			/* EMPTY - no quantifier means exactly once; @$ is unused since
+			 * min=max=1 never produces an error */
+			{ $$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner); }
 			| '*'					{ $$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner); }
 			| '+'					{ $$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner); }
 			| Op
-- 
2.50.1 (Apple Git-155)

From f7d0cd30573e7ca2ab72c80a4ed6537b5451cdb4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 21:27:33 +0900
Subject: [PATCH] Clarify RPR_VARID_MAX definition in rpr.h

RPR_VARID_MAX = 251 allows varId 0 through 250, giving a maximum of
251 unique pattern variables. Values from 252 onward are reserved for
control elements (BEGIN, END, ALT, FIN). Add a comment explaining this
layout.
---
 src/include/optimizer/rpr.h | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index f93a128096b..e78092678bb 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -17,7 +17,11 @@
 #include "nodes/plannodes.h"
 
 /* Limits and special values */
-#define RPR_VARID_MAX		251 /* max pattern variables: 251 */
+/*
+ * Maximum number of unique pattern variables (varId 0 to RPR_VARID_MAX - 1).
+ * Values from RPR_VARID_BEGIN (252) onward are reserved for control elements.
+ */
+#define RPR_VARID_MAX		251
 #define RPR_QUANTITY_INF	INT32_MAX	/* unbounded quantifier */
 #define RPR_COUNT_MAX		INT32_MAX	/* max runtime count (NFA state) */
 #define RPR_ELEMIDX_MAX		INT16_MAX	/* max pattern elements */
-- 
2.50.1 (Apple Git-155)

From 0e944668568a73c3387636e29dc52bd056907437 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 21:40:05 +0900
Subject: [PATCH] Move local variables to function scope in
 row_is_in_reduced_frame

Variables i and num_reduced_rows were declared inside the switch block
before the first case label. Move them to the function top to follow
PostgreSQL coding conventions.
---
 src/backend/executor/nodeWindowAgg.c | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 942f071b457..6575cf9dd96 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3969,6 +3969,8 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 	WindowAggState *winstate = winobj->winstate;
 	int			state;
 	int			rtn;
+	int64		i;
+	int			num_reduced_rows;
 
 	if (!rpr_is_defined(winstate))
 	{
@@ -3996,9 +3998,6 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 
 	switch (state)
 	{
-			int64		i;
-			int			num_reduced_rows;
-
 		case RF_FRAME_HEAD:
 			num_reduced_rows = 1;
 			for (i = pos + 1;
-- 
2.50.1 (Apple Git-155)

From 6fbd1e03665f9c65d20d8159cd2923e46f25bff9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 21:48:36 +0900
Subject: [PATCH] Reset reduced_frame_map pointer in release_partition

After MemoryContextReset(partcontext), reduced_frame_map becomes a
dangling pointer. Set it to NULL and reset alloc_sz to zero so that
create_reduced_frame_map starts fresh for the next partition.
---
 src/backend/executor/nodeWindowAgg.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 6575cf9dd96..f1f9d60b39d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -1580,6 +1580,10 @@ release_partition(WindowAggState *winstate)
 	winstate->partition_spooled = false;
 	winstate->next_partition = true;
 
+	/* Reset RPR reduced frame map */
+	winstate->reduced_frame_map = NULL;
+	winstate->alloc_sz = 0;
+
 	/* Reset NFA state for new partition */
 	winstate->nfaContext = NULL;
 	winstate->nfaContextTail = NULL;
-- 
2.50.1 (Apple Git-155)

From a224ed18261a09d5b1e65aba3c97fc5741234589 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 22:11:30 +0900
Subject: [PATCH] Remove redundant list manipulation in nfa_add_matched_state

nfa_add_matched_state() manually unlinked pruned contexts before
calling ExecRPRFreeContext(), which internally calls nfa_unlink_context()
to perform the same list operations. Remove the manual forward-link
update and the post-loop tail fixup, letting nfa_unlink_context()
handle forward link, backward link, and tail update consistently.
---
 src/backend/executor/execRPR.c | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index a0a462256ad..bab5257f68f 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1808,14 +1808,12 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Prune contexts that started within this match's range */
 	if (winstate->rpSkipTo == ST_PAST_LAST_ROW)
 	{
-		RPRNFAContext *nextCtx;
 		int64		skippedLen;
 
 		while (ctx->next != NULL &&
 			   ctx->next->matchStartRow <= matchEndRow)
 		{
-			nextCtx = ctx->next;
-			ctx->next = ctx->next->next;
+			RPRNFAContext *nextCtx = ctx->next;
 
 			Assert(nextCtx->lastProcessedRow >= nextCtx->matchStartRow);
 			skippedLen = nextCtx->lastProcessedRow - nextCtx->matchStartRow + 1;
@@ -1823,8 +1821,6 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 
 			ExecRPRFreeContext(winstate, nextCtx);
 		}
-		if (ctx->next == NULL)
-			winstate->nfaContextTail = ctx;
 	}
 }
 
-- 
2.50.1 (Apple Git-155)

From f22f8e90163e89bbb235a363db3cfcac5b95f522 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 23:47:12 +0900
Subject: [PATCH] Implement 1-slot PREV/NEXT navigation for RPR

Add PREV(value [, offset]) and NEXT(value [, offset]) navigation functions
for use in the DEFINE clause of row pattern recognition.

These functions return the column value at a row offset rows before/after
the current row within the window frame, returning NULL if the target row
is outside the frame. The offset defaults to 1 if omitted; offset=0 refers
to the current row itself; NULL offset returns NULL.

Implementation adds nav_slot and nav_null_slot to WindowAggState to fetch
the target row without disturbing the main evaluation slot, along with a
dedicated nav_winobj for navigation reads. The RPRNavExpr node carries
the offset expression and direction (PREV/NEXT); it is evaluated via a
new EEOP_RPR_NAV expression step in the expression interpreter (and
corresponding LLVM JIT path).

Parser changes: PREV/NEXT are parsed as function calls and transformed into
RPRNavExpr nodes in transformDefineClause(). Validation rejects nested
PREV/NEXT calls, requires at least one column reference in the first
argument, and ensures the offset argument is a run-time constant.
Documentation updated to reflect the 2-argument form and NULL-offset
behavior.
---
 doc/src/sgml/func/func-window.sgml        |  22 +-
 src/backend/executor/execExpr.c           |  57 +++
 src/backend/executor/execExprInterp.c     | 105 +++++
 src/backend/executor/nodeWindowAgg.c      | 240 +++++------
 src/backend/jit/llvm/llvmjit_expr.c       |  29 ++
 src/backend/jit/llvm/llvmjit_types.c      |   2 +
 src/backend/nodes/nodeFuncs.c             |  33 ++
 src/backend/parser/parse_func.c           |  30 +-
 src/backend/parser/parse_rpr.c            |  79 ++++
 src/backend/utils/adt/ruleutils.c         |  16 +
 src/backend/utils/adt/windowfuncs.c       |  52 ++-
 src/include/catalog/pg_proc.dat           |   6 +
 src/include/executor/execExpr.h           |  19 +
 src/include/executor/nodeWindowAgg.h      |   3 +
 src/include/nodes/execnodes.h             |  10 +-
 src/include/nodes/primnodes.h             |  31 ++
 src/test/regress/expected/rpr.out         | 493 +++++++++++++++++++++-
 src/test/regress/expected/rpr_explain.out | 114 ++++-
 src/test/regress/sql/rpr.sql              | 291 ++++++++++++-
 src/test/regress/sql/rpr_explain.sql      |  84 +++-
 src/tools/pgindent/typedefs.list          |   3 +-
 21 files changed, 1524 insertions(+), 195 deletions(-)

diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index ae36e0f3135..cb1a852c4d2 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -304,12 +304,17 @@
         <indexterm>
          <primary>prev</primary>
         </indexterm>
-        <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> )
+        <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
         <returnvalue>anyelement</returnvalue>
        </para>
        <para>
-        Returns the column value at the previous row;
-        returns NULL if there is no previous row in the window frame.
+        Returns the column value at the row <parameter>offset</parameter>
+        rows before the current row within the window frame;
+        returns NULL if the target row is outside the window frame.
+        <parameter>offset</parameter> defaults to 1 if omitted.
+        <parameter>offset</parameter> must be a non-negative integer;
+        an offset of 0 refers to the current row itself.
+        If <parameter>offset</parameter> is NULL, NULL is returned.
        </para></entry>
       </row>
 
@@ -318,12 +323,17 @@
         <indexterm>
          <primary>next</primary>
         </indexterm>
-        <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> )
+        <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
         <returnvalue>anyelement</returnvalue>
        </para>
        <para>
-        Returns the column value at the next row;
-        returns NULL if there is no next row in the window frame.
+        Returns the column value at the row <parameter>offset</parameter>
+        rows after the current row within the window frame;
+        returns NULL if the target row is outside the window frame.
+        <parameter>offset</parameter> defaults to 1 if omitted.
+        <parameter>offset</parameter> must be a non-negative integer;
+        an offset of 0 refers to the current row itself.
+        If <parameter>offset</parameter> is NULL, NULL is returned.
        </para></entry>
       </row>
 
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 088eca24021..a8136638618 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1189,6 +1189,63 @@ ExecInitExprRec(Expr *node, ExprState *state,
 				break;
 			}
 
+		case T_RPRNavExpr:
+			{
+				/*
+				 * RPR navigation functions (PREV/NEXT) are compiled into
+				 * EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE opcodes instead of
+				 * a normal function call.  The SET opcode swaps
+				 * ecxt_outertuple to the target row, the argument expression
+				 * is compiled normally (reads from the swapped slot), and the
+				 * RESTORE opcode restores the original slot.
+				 */
+				RPRNavExpr *nav = (RPRNavExpr *) node;
+				WindowAggState *winstate;
+
+				Assert(state->parent && IsA(state->parent, WindowAggState));
+				winstate = (WindowAggState *) state->parent;
+
+				/* Emit SET opcode: swap slot to target row */
+				scratch.opcode = EEOP_RPR_NAV_SET;
+				scratch.d.rpr_nav.winstate = winstate;
+				scratch.d.rpr_nav.offset = nav->kind;	/* RPR_NAV_PREV=-1,
+														 * RPR_NAV_NEXT=1 */
+
+				if (nav->offset_arg != NULL)
+				{
+					/*
+					 * Allocate storage for the runtime offset value.  The
+					 * offset expression is compiled below so it runs before
+					 * EEOP_RPR_NAV_SET.
+					 */
+					Datum	   *offset_value = palloc_object(Datum);
+					bool	   *offset_isnull = palloc_object(bool);
+
+					/* Compile the offset expression into the temp storage */
+					ExecInitExprRec(nav->offset_arg, state,
+									offset_value, offset_isnull);
+
+					scratch.d.rpr_nav.offset_value = offset_value;
+					scratch.d.rpr_nav.offset_isnull = offset_isnull;
+				}
+				else
+				{
+					scratch.d.rpr_nav.offset_value = NULL;
+					scratch.d.rpr_nav.offset_isnull = NULL;
+				}
+
+				ExprEvalPushStep(state, &scratch);
+
+				/* Compile the argument expression normally */
+				ExecInitExprRec(nav->arg, state, resv, resnull);
+
+				/* Emit RESTORE opcode: restore original slot */
+				scratch.opcode = EEOP_RPR_NAV_RESTORE;
+				scratch.d.rpr_nav.winstate = winstate;
+				ExprEvalPushStep(state, &scratch);
+				break;
+			}
+
 		case T_FuncExpr:
 			{
 				FuncExpr   *func = (FuncExpr *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 61ff5ddc74c..789b8f0c1c7 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -56,11 +56,13 @@
  */
 #include "postgres.h"
 
+#include "common/int.h"
 #include "access/heaptoast.h"
 #include "catalog/pg_type.h"
 #include "commands/sequence.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
+#include "executor/nodeWindowAgg.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "nodes/miscnodes.h"
@@ -576,6 +578,8 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_WINDOW_FUNC,
 		&&CASE_EEOP_MERGE_SUPPORT_FUNC,
 		&&CASE_EEOP_SUBPLAN,
+		&&CASE_EEOP_RPR_NAV_SET,
+		&&CASE_EEOP_RPR_NAV_RESTORE,
 		&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
 		&&CASE_EEOP_AGG_DESERIALIZE,
 		&&CASE_EEOP_AGG_STRICT_INPUT_CHECK_ARGS,
@@ -2003,6 +2007,24 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+		/* RPR navigation: swap slot to target row */
+		EEO_CASE(EEOP_RPR_NAV_SET)
+		{
+			ExecEvalRPRNavSet(state, op, econtext);
+			outerslot = econtext->ecxt_outertuple;
+
+			EEO_NEXT();
+		}
+
+		/* RPR navigation: restore slot to original row */
+		EEO_CASE(EEOP_RPR_NAV_RESTORE)
+		{
+			ExecEvalRPRNavRestore(state, op, econtext);
+			outerslot = econtext->ecxt_outertuple;
+
+			EEO_NEXT();
+		}
+
 		/* evaluate a strict aggregate deserialization function */
 		EEO_CASE(EEOP_AGG_STRICT_DESERIALIZE)
 		{
@@ -5918,3 +5940,86 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
 
 	MemoryContextSwitchTo(oldContext);
 }
+
+/*
+ * Evaluate RPR PREV/NEXT navigation: swap slot to target row.
+ *
+ * Saves the current outertuple into winstate for later restore, computes
+ * the target row position, fetches the corresponding slot from the
+ * tuplestore, and replaces econtext->ecxt_outertuple with it.
+ *
+ * This is called both from the interpreter inline handler and from
+ * JIT-compiled expressions via build_EvalXFunc.
+ */
+void
+ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+	WindowAggState *winstate = op->d.rpr_nav.winstate;
+	int64		offset;
+	int64		target_pos;
+	TupleTableSlot *target_slot;
+
+	/* Save current slot for later restore */
+	winstate->nav_saved_outertuple = econtext->ecxt_outertuple;
+
+	/*
+	 * Determine the signed offset.  For 2-arg PREV/NEXT the offset expression
+	 * has already been evaluated into offset_value.  NULL or negative offsets
+	 * are errors (matching Oracle behavior).
+	 */
+	if (op->d.rpr_nav.offset_value != NULL)
+	{
+		int64		raw_offset;
+
+		if (*op->d.rpr_nav.offset_isnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					 errmsg("PREV/NEXT offset must not be null")));
+
+		raw_offset = DatumGetInt64(*op->d.rpr_nav.offset_value);
+
+		if (raw_offset < 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("PREV/NEXT offset must not be negative")));
+
+		/* Apply direction sign: PREV subtracts, NEXT adds */
+		offset = raw_offset * op->d.rpr_nav.offset;
+	}
+	else
+		offset = op->d.rpr_nav.offset;
+
+	/*
+	 * Calculate target position.  On overflow, use -1 so that
+	 * ExecRPRNavGetSlot treats it as out of range.
+	 */
+	if (pg_add_s64_overflow(winstate->currentpos, offset, &target_pos))
+		target_pos = -1;
+
+	/* Fetch target row slot (returns nav_null_slot if out of range) */
+	target_slot = ExecRPRNavGetSlot(winstate, target_pos);
+
+	/*
+	 * Update econtext to point to the target slot.  Also decompress the new
+	 * slot's attributes since FETCHSOME already ran for the original slot.
+	 * The caller (interpreter or JIT) is responsible for updating any local
+	 * slot cache (e.g. outerslot) from econtext after we return.
+	 */
+	slot_getallattrs(target_slot);
+	econtext->ecxt_outertuple = target_slot;
+}
+
+/*
+ * Evaluate RPR PREV/NEXT navigation: restore slot to original row.
+ *
+ * Restores econtext->ecxt_outertuple from the saved slot in winstate.
+ * The caller is responsible for updating any local slot cache.
+ */
+void
+ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
+					  ExprContext *econtext)
+{
+	WindowAggState *winstate = op->d.rpr_nav.winstate;
+
+	econtext->ecxt_outertuple = winstate->nav_saved_outertuple;
+}
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index f1f9d60b39d..83377b4f18d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -53,7 +53,6 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
-#include "utils/fmgroids.h"
 #include "utils/expandeddatum.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -176,14 +175,6 @@ typedef struct WindowStatePerAggData
 	bool		restart;		/* need to restart this agg in this cycle? */
 } WindowStatePerAggData;
 
-/*
- * Structure used by check_rpr_navigation() and rpr_navigation_walker().
- */
-typedef struct NavigationInfo
-{
-	bool		is_prev;		/* true if PREV */
-	int			num_vars;		/* number of var nodes */
-} NavigationInfo;
 
 static void initialize_windowaggregate(WindowAggState *winstate,
 									   WindowStatePerFunc perfuncstate,
@@ -242,9 +233,6 @@ static uint8 get_notnull_info(WindowObject winobj,
 							  int64 pos, int argno);
 static void put_notnull_info(WindowObject winobj,
 							 int64 pos, int argno, bool isnull);
-static void attno_map(Node *node);
-static bool attno_map_walker(Node *node, void *context);
-
 static bool rpr_is_defined(WindowAggState *winstate);
 static int	row_is_in_reduced_frame(WindowObject winobj, int64 pos);
 
@@ -255,9 +243,6 @@ static void register_reduced_frame_map(WindowAggState *winstate, int64 pos,
 									   int val);
 static void update_reduced_frame(WindowObject winobj, int64 pos);
 
-static void check_rpr_navigation(Node *node, bool is_prev);
-static bool rpr_navigation_walker(Node *node, void *context);
-
 /* Forward declarations - NFA row evaluation */
 static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
 
@@ -1301,6 +1286,21 @@ prepare_tuplestore(WindowAggState *winstate)
 		}
 	}
 
+	/* Create read/mark pointers for RPR navigation if needed */
+	if (winstate->nav_winobj)
+	{
+		/*
+		 * Allocate a mark pointer pinned at position 0 so that the tuplestore
+		 * never truncates rows that a PREV(expr, N) might need.
+		 */
+		winstate->nav_winobj->markptr =
+			tuplestore_alloc_read_pointer(winstate->buffer, 0);
+		winstate->nav_winobj->readptr =
+			tuplestore_alloc_read_pointer(winstate->buffer,
+										  EXEC_FLAG_BACKWARD);
+		winstate->nav_winobj->markpos = 0;
+	}
+
 	/*
 	 * If we are in RANGE or GROUPS mode, then determining frame boundaries
 	 * requires physical access to the frame endpoint rows, except in certain
@@ -1411,6 +1411,13 @@ begin_partition(WindowAggState *winstate)
 		winstate->aggregatedupto = 0;
 	}
 
+	/* reset mark and seek positions for RPR navigation */
+	if (winstate->nav_winobj)
+	{
+		winstate->nav_winobj->markpos = -1;
+		winstate->nav_winobj->seekpos = -1;
+	}
+
 	/* reset mark and seek positions for each real window function */
 	for (int i = 0; i < numfuncs; i++)
 	{
@@ -2756,15 +2763,18 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->temp_slot_2 = ExecInitExtraTupleSlot(estate, scanDesc,
 												   &TTSOpsMinimalTuple);
 
-	winstate->prev_slot = ExecInitExtraTupleSlot(estate, scanDesc,
-												 &TTSOpsMinimalTuple);
+	if (node->rpPattern != NULL)
+	{
+		winstate->nav_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+													&TTSOpsMinimalTuple);
+		winstate->nav_slot_pos = -1;
 
-	winstate->next_slot = ExecInitExtraTupleSlot(estate, scanDesc,
-												 &TTSOpsMinimalTuple);
+		winstate->nav_null_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+														 &TTSOpsMinimalTuple);
+		winstate->nav_null_slot = ExecStoreAllNullTuple(winstate->nav_null_slot);
 
-	winstate->null_slot = ExecInitExtraTupleSlot(estate, scanDesc,
-												 &TTSOpsMinimalTuple);
-	winstate->null_slot = ExecStoreAllNullTuple(winstate->null_slot);
+		winstate->nav_saved_outertuple = NULL;
+	}
 
 	/*
 	 * create frame head and tail slots only if needed (must create slots in
@@ -2934,6 +2944,23 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->agg_winobj = agg_winobj;
 	}
 
+	/*
+	 * Set up WindowObject for RPR navigation opcodes.  This is separate from
+	 * agg_winobj because it needs its own read pointer to avoid interfering
+	 * with aggregate processing.
+	 */
+	if (node->rpPattern != NULL)
+	{
+		WindowObject nav_winobj = makeNode(WindowObjectData);
+
+		nav_winobj->winstate = winstate;
+		nav_winobj->argstates = NIL;
+		nav_winobj->localmem = NULL;
+		nav_winobj->markptr = -1;
+		nav_winobj->readptr = -1;
+		winstate->nav_winobj = nav_winobj;
+	}
+
 	/* Set the status to running */
 	winstate->status = WINDOWAGG_RUN;
 
@@ -2974,7 +3001,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	if (node->defineClause != NIL)
 	{
 		/*
-		 * Tweak arg var of PREV/NEXT so that it refers to scan/inner slot.
+		 * 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(l, node->defineClause)
 		{
@@ -2991,7 +3020,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 			winstate->defineVariableList =
 				lappend(winstate->defineVariableList,
 						makeString(pstrdup(name)));
-			attno_map((Node *) expr);
 			exps = ExecInitExpr(expr, (PlanState *) winstate);
 			winstate->defineClauseList =
 				lappend(winstate->defineClauseList, exps);
@@ -3026,107 +3054,39 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 }
 
 /*
- * Rewrite varno of Var nodes that are the argument of PREV/NET so that they
- * see scan tuple (PREV) or inner tuple (NEXT).  Also we check the arguments
- * of PREV/NEXT include at least 1 column reference. This is required by the
- * SQL standard.
+ * ExecRPRNavGetSlot
+ *
+ * Fetch tuple at given position for RPR navigation opcodes.
+ * Returns nav_slot with the tuple loaded, or nav_null_slot if out of range.
+ * Called from EEOP_RPR_NAV_SET handler in execExprInterp.c.
  */
-static void
-attno_map(Node *node)
+TupleTableSlot *
+ExecRPRNavGetSlot(WindowAggState *winstate, int64 pos)
 {
-	(void) expression_tree_walker(node, attno_map_walker, NULL);
-}
+	WindowObject winobj = winstate->nav_winobj;
+	TupleTableSlot *slot = winstate->nav_slot;
 
-static bool
-attno_map_walker(Node *node, void *context)
-{
-	FuncExpr   *func;
-	int			nargs;
-	bool		is_prev;
+	if (pos < 0)
+		return winstate->nav_null_slot;
 
-	if (node == NULL)
-		return false;
+	/*
+	 * If nav_slot already holds this position, return it without re-fetching.
+	 * This is critical when multiple PREV/NEXT calls in the same expression
+	 * navigate to the same row, because re-fetching would free the slot's
+	 * tuple memory and invalidate any pass-by-ref Datum pointers from earlier
+	 * navigation results.
+	 */
+	if (winstate->nav_slot_pos == pos)
+		return slot;
 
-	if (IsA(node, FuncExpr))
+	if (!window_gettupleslot(winobj, pos, slot))
 	{
-		func = (FuncExpr *) node;
-
-		if (func->funcid == F_PREV || func->funcid == F_NEXT)
-		{
-			/*
-			 * The SQL standard allows to have two more arguments form of
-			 * PREV/NEXT.  But currently we allow only 1 argument form.
-			 */
-			nargs = list_length(func->args);
-			if (list_length(func->args) != 1)
-				elog(ERROR, "PREV/NEXT must have 1 argument but function %d has %d args",
-					 func->funcid, nargs);
-
-			/*
-			 * Check expr of PREV/NEXT aruguments and replace varno.
-			 */
-			is_prev = (func->funcid == F_PREV) ? true : false;
-			check_rpr_navigation(node, is_prev);
-		}
+		winstate->nav_slot_pos = -1;
+		return winstate->nav_null_slot;
 	}
-	return expression_tree_walker(node, attno_map_walker, NULL);
-}
 
-/*
- * Rewrite varno of Var of RPR navigation operations (PREV/NEXT).
- * If is_prev is true, we take care PREV, otherwise NEXT.
- */
-static void
-check_rpr_navigation(Node *node, bool is_prev)
-{
-	NavigationInfo context;
-
-	context.is_prev = is_prev;
-	context.num_vars = 0;
-	(void) expression_tree_walker(node, rpr_navigation_walker, &context);
-	if (context.num_vars < 1)
-		ereport(ERROR,
-				errmsg("row pattern navigation operation's argument must include at least one column reference"));
-}
-
-static bool
-rpr_navigation_walker(Node *node, void *context)
-{
-	NavigationInfo *nav = (NavigationInfo *) context;
-
-	if (node == NULL)
-		return false;
-
-	switch (nodeTag(node))
-	{
-		case T_Var:
-			{
-				Var		   *var = (Var *) node;
-
-				nav->num_vars++;
-
-				if (nav->is_prev)
-				{
-					/*
-					 * Rewrite varno from OUTER_VAR to regular var no so that
-					 * the var references scan tuple.
-					 */
-					var->varno = var->varnosyn;
-				}
-				else
-					var->varno = INNER_VAR;
-			}
-			break;
-		case T_Const:
-		case T_FuncExpr:
-		case T_OpExpr:
-			break;
-
-		default:
-			ereport(ERROR,
-					errmsg("row pattern navigation operation's argument includes unsupported expression"));
-	}
-	return expression_tree_walker(node, rpr_navigation_walker, context);
+	winstate->nav_slot_pos = pos;
+	return slot;
 }

@@ -3187,8 +3147,8 @@ ExecReScanWindowAgg(WindowAggState *node)
 	ExecClearTuple(node->agg_row_slot);
 	ExecClearTuple(node->temp_slot_1);
 	ExecClearTuple(node->temp_slot_2);
-	ExecClearTuple(node->prev_slot);
-	ExecClearTuple(node->next_slot);
+	if (node->nav_slot)
+		ExecClearTuple(node->nav_slot);
 	if (node->framehead_slot)
 		ExecClearTuple(node->framehead_slot);
 	if (node->frametail_slot)
@@ -4277,6 +4237,10 @@ register_result:
  * Returns true if the row exists, false if out of partition.
  * If row exists, fills varMatched array.
  * varMatched[i] = true if variable i matched at current row.
+ *
+ * Uses 1-slot model: only ecxt_outertuple is set to the current row.
+ * PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
+ * during expression evaluation, which temporarily swap the slot.
  */
 static bool
 nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
@@ -4287,37 +4251,25 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	ListCell   *lc;
 	int			varIdx = 0;
 	TupleTableSlot *slot;
+	int64		saved_pos;
 
-	/*
-	 * Set up slots for current, previous, and next rows. We don't call
-	 * get_slots() here to avoid recursion through row_is_in_frame ->
-	 * update_reduced_frame -> ExecRPRProcessRow.
-	 */
-
-	/* Current row -> ecxt_outertuple */
+	/* Fetch current row into temp_slot_1 */
 	slot = winstate->temp_slot_1;
 	if (!window_gettupleslot(winobj, pos, slot))
 		return false;			/* No row exists */
+
+	/* Set up 1-slot context: only ecxt_outertuple */
 	econtext->ecxt_outertuple = slot;
 
-	/* Previous row -> ecxt_scantuple (for PREV) */
-	if (pos > 0)
-	{
-		slot = winstate->prev_slot;
-		if (!window_gettupleslot(winobj, pos - 1, slot))
-			econtext->ecxt_scantuple = winstate->null_slot;
-		else
-			econtext->ecxt_scantuple = slot;
-	}
-	else
-		econtext->ecxt_scantuple = winstate->null_slot;
+	/*
+	 * Save and set currentpos so that EEOP_RPR_NAV_SET opcodes can calculate
+	 * target positions (currentpos +/- offset).
+	 */
+	saved_pos = winstate->currentpos;
+	winstate->currentpos = pos;
 
-	/* Next row -> ecxt_innertuple (for NEXT) */
-	slot = winstate->next_slot;
-	if (!window_gettupleslot(winobj, pos + 1, slot))
-		econtext->ecxt_innertuple = winstate->null_slot;
-	else
-		econtext->ecxt_innertuple = slot;
+	/* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
+	winstate->nav_slot_pos = -1;
 
 	foreach(lc, winstate->defineClauseList)
 	{
@@ -4335,6 +4287,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 			break;
 	}
 
+	winstate->currentpos = saved_pos;
+
 	return true;				/* Row exists */
 }
 
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 650f1d42a93..150a1a7226c 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -2432,6 +2432,35 @@ llvm_compile_expr(ExprState *state)
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
 
+			case EEOP_RPR_NAV_SET:
+				build_EvalXFunc(b, mod, "ExecEvalRPRNavSet",
+								v_state, op, v_econtext);
+
+				/*
+				 * Reload v_outerslot from econtext since the C function
+				 * swapped ecxt_outertuple to a different row.
+				 */
+				v_outerslot = l_load_struct_gep(b,
+												StructExprContext,
+												v_econtext,
+												FIELDNO_EXPRCONTEXT_OUTERTUPLE,
+												"v_outerslot");
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
+			case EEOP_RPR_NAV_RESTORE:
+				build_EvalXFunc(b, mod, "ExecEvalRPRNavRestore",
+								v_state, op, v_econtext);
+
+				/* Reload v_outerslot after restore */
+				v_outerslot = l_load_struct_gep(b,
+												StructExprContext,
+												v_econtext,
+												FIELDNO_EXPRCONTEXT_OUTERTUPLE,
+												"v_outerslot");
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_AGG_STRICT_DESERIALIZE:
 			case EEOP_AGG_DESERIALIZE:
 				{
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
index c8a1f841293..e78b31d775f 100644
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -168,6 +168,8 @@ void	   *referenced_functions[] =
 	ExecEvalScalarArrayOp,
 	ExecEvalHashedScalarArrayOp,
 	ExecEvalSubPlan,
+	ExecEvalRPRNavSet,
+	ExecEvalRPRNavRestore,
 	ExecEvalSysVar,
 	ExecEvalWholeRowVar,
 	ExecEvalXmlExpr,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index ddf6812d7b0..0799ca1108f 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -69,6 +69,9 @@ exprType(const Node *expr)
 		case T_MergeSupportFunc:
 			type = ((const MergeSupportFunc *) expr)->msftype;
 			break;
+		case T_RPRNavExpr:
+			type = ((const RPRNavExpr *) expr)->resulttype;
+			break;
 		case T_SubscriptingRef:
 			type = ((const SubscriptingRef *) expr)->refrestype;
 			break;
@@ -848,6 +851,9 @@ exprCollation(const Node *expr)
 		case T_MergeSupportFunc:
 			coll = ((const MergeSupportFunc *) expr)->msfcollid;
 			break;
+		case T_RPRNavExpr:
+			coll = ((const RPRNavExpr *) expr)->resultcollid;
+			break;
 		case T_SubscriptingRef:
 			coll = ((const SubscriptingRef *) expr)->refcollid;
 			break;
@@ -1146,6 +1152,9 @@ exprSetCollation(Node *expr, Oid collation)
 		case T_MergeSupportFunc:
 			((MergeSupportFunc *) expr)->msfcollid = collation;
 			break;
+		case T_RPRNavExpr:
+			((RPRNavExpr *) expr)->resultcollid = collation;
+			break;
 		case T_SubscriptingRef:
 			((SubscriptingRef *) expr)->refcollid = collation;
 			break;
@@ -1418,6 +1427,9 @@ exprLocation(const Node *expr)
 		case T_MergeSupportFunc:
 			loc = ((const MergeSupportFunc *) expr)->location;
 			break;
+		case T_RPRNavExpr:
+			loc = ((const RPRNavExpr *) expr)->location;
+			break;
 		case T_SubscriptingRef:
 			/* just use container argument's location */
 			loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2178,6 +2190,16 @@ expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_RPRNavExpr:
+			{
+				RPRNavExpr *expr = (RPRNavExpr *) node;
+
+				if (WALK(expr->arg))
+					return true;
+				if (expr->offset_arg && WALK(expr->offset_arg))
+					return true;
+			}
+			break;
 		case T_SubscriptingRef:
 			{
 				SubscriptingRef *sbsref = (SubscriptingRef *) node;
@@ -3081,6 +3103,17 @@ expression_tree_mutator_impl(Node *node,
 				return (Node *) newnode;
 			}
 			break;
+		case T_RPRNavExpr:
+			{
+				RPRNavExpr *nav = (RPRNavExpr *) node;
+				RPRNavExpr *newnode;
+
+				FLATCOPY(newnode, nav, RPRNavExpr);
+				MUTATE(newnode->arg, nav->arg, Expr *);
+				MUTATE(newnode->offset_arg, nav->offset_arg, Expr *);
+				return (Node *) newnode;
+			}
+			break;
 		case T_SubscriptingRef:
 			{
 				SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index ba9523ae3d4..c25e2c4d3a7 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -760,7 +760,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	/* next() and prev() are only allowed in a WINDOW DEFINE clause */
 	if (fdresult == FUNCDETAIL_NORMAL &&
 		pstate->p_expr_kind != EXPR_KIND_RPR_DEFINE &&
-		(funcid == F_PREV || funcid == F_NEXT))
+		(funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
+		 funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8))
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("%s can only be used in a DEFINE clause",
@@ -768,7 +769,32 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 				 parser_errposition(pstate, location)));
 
 	/* build the appropriate output structure */
-	if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
+	if (fdresult == FUNCDETAIL_NORMAL &&
+		(funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
+		 funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8))
+	{
+		/*
+		 * PREV() and NEXT() 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.
+		 */
+		bool		is_next = (funcid == F_NEXT_ANYELEMENT ||
+							   funcid == F_NEXT_ANYELEMENT_INT8);
+		bool		has_offset = (funcid == F_PREV_ANYELEMENT_INT8 ||
+								  funcid == F_NEXT_ANYELEMENT_INT8);
+		RPRNavExpr *navexpr = makeNode(RPRNavExpr);
+
+		navexpr->kind = is_next ? RPR_NAV_NEXT : RPR_NAV_PREV;
+		navexpr->arg = (Expr *) linitial(fargs);
+		navexpr->offset_arg = has_offset ? (Expr *) lsecond(fargs) : NULL;
+		navexpr->resulttype = rettype;
+		/* resultcollid will be set by parse_collate.c */
+		navexpr->location = location;
+
+		retval = (Node *) navexpr;
+	}
+	else if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		FuncExpr   *funcexpr = makeNode(FuncExpr);
 
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 55283ab4bbe..7c6001eec9f 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -28,6 +28,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
 #include "optimizer/rpr.h"
 #include "parser/parse_clause.h"
 #include "parser/parse_collate.h"
@@ -41,6 +42,9 @@ static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 									   List *rpDefs, List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
 								   WindowDef *windef, List **targetlist);
+static bool contains_rpr_nav_walker(Node *node, void *context);
+static bool contains_column_ref_walker(Node *node, void *context);
+static bool check_rpr_nav_nesting_walker(Node *node, void *context);
 
 /*
  * transformRPR
@@ -400,6 +404,10 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	foreach_ptr(TargetEntry, te, defineClause)
 		te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
 
+	/* check for nested PREV/NEXT and missing column references */
+	foreach_ptr(TargetEntry, te, defineClause)
+		(void) check_rpr_nav_nesting_walker((Node *) te->expr, pstate);
+
 	/* mark column origins */
 	markTargetListOrigins(pstate, defineClause);
 
@@ -408,3 +416,74 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 	return defineClause;
 }
+
+/*
+ * contains_rpr_nav_walker
+ *		Return true if the expression tree contains a PREV or NEXT call.
+ */
+static bool
+contains_rpr_nav_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+		return true;
+	return expression_tree_walker(node, contains_rpr_nav_walker, context);
+}
+
+/*
+ * contains_column_ref_walker
+ *		Return true if the expression tree contains at least one Var node
+ *		(column reference).
+ */
+static bool
+contains_column_ref_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Var))
+		return true;
+	return expression_tree_walker(node, contains_column_ref_walker, context);
+}
+
+/*
+ * check_rpr_nav_nesting_walker
+ *		Raise an error if PREV or NEXT appears nested inside another PREV/NEXT,
+ *		if the first argument contains no column reference, or if the optional
+ *		offset argument is not a run-time constant (i.e., it contains a column
+ *		reference).
+ */
+static bool
+check_rpr_nav_nesting_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+	{
+		RPRNavExpr *nav = (RPRNavExpr *) node;
+
+		if (contains_rpr_nav_walker((Node *) nav->arg, NULL))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("PREV and NEXT cannot be nested"),
+					 parser_errposition((ParseState *) context,
+										nav->location)));
+		if (!contains_column_ref_walker((Node *) nav->arg, NULL))
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("argument of row pattern navigation operation must include at least one column reference"),
+					 parser_errposition((ParseState *) context,
+										nav->location)));
+		if (nav->offset_arg != NULL &&
+			(contains_column_ref_walker((Node *) nav->offset_arg, NULL) ||
+			 contain_volatile_functions((Node *) nav->offset_arg)))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("PREV/NEXT offset must be a run-time constant"),
+					 parser_errposition((ParseState *) context,
+										nav->location)));
+		/* don't recurse into arg; nesting already checked above */
+		return false;
+	}
+	return expression_tree_walker(node, check_rpr_nav_nesting_walker, context);
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 928bed2e9fb..d19f3718b30 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9581,6 +9581,22 @@ get_rule_expr(Node *node, deparse_context *context,
 			get_func_expr((FuncExpr *) node, context, showimplicit);
 			break;
 
+		case T_RPRNavExpr:
+			{
+				RPRNavExpr *nav = (RPRNavExpr *) node;
+
+				appendStringInfoString(buf,
+									   nav->kind == RPR_NAV_PREV ? "PREV(" : "NEXT(");
+				get_rule_expr((Node *) nav->arg, context, showimplicit);
+				if (nav->offset_arg != NULL)
+				{
+					appendStringInfoString(buf, ", ");
+					get_rule_expr((Node *) nav->offset_arg, context, showimplicit);
+				}
+				appendStringInfoChar(buf, ')');
+			}
+			break;
+
 		case T_NamedArgExpr:
 			{
 				NamedArgExpr *na = (NamedArgExpr *) node;
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 74ef109f72e..091260d2cce 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -726,22 +726,62 @@ window_nth_value(PG_FUNCTION_ARGS)
 
 /*
  * prev
- * Dummy function to invoke RPR's navigation operator "PREV".
- * This is *not* a window function.
+ * 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)
 {
-	PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("prev() can only be used in a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
 }
 
 /*
  * next
- * Dummy function to invoke RPR's navigation operation "NEXT".
- * This is *not* a window function.
+ * Catalog placeholder for RPR's NEXT navigation operator.
+ * See window_prev() for details.
  */
 Datum
 window_next(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("next() can only be used in 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("prev() can only be used in 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("next() can only be used in 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 7ddaef3d04a..8496c1babc4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10957,9 +10957,15 @@
 { oid => '8126', descr => 'previous value',
   proname => 'prev', provolatile => 's', prorettype => 'anyelement',
   proargtypes => 'anyelement', prosrc => 'window_prev' },
+{ oid => '8128', descr => 'previous value at offset',
+  proname => 'prev', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'anyelement int8', prosrc => 'window_prev_offset' },
 { oid => '8127', descr => 'next value',
   proname => 'next', provolatile => 's', prorettype => 'anyelement',
   proargtypes => 'anyelement', prosrc => 'window_next' },
+{ oid => '8129', descr => 'next value at offset',
+  proname => 'next', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'anyelement int8', prosrc => 'window_next_offset' },
 
 # functions for range types
 { oid => '3832', descr => 'I/O',
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index aa9b361fa31..6067f3b6ff8 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -274,6 +274,10 @@ typedef enum ExprEvalOp
 	EEOP_MERGE_SUPPORT_FUNC,
 	EEOP_SUBPLAN,
 
+	/* row pattern navigation (RPR PREV/NEXT) */
+	EEOP_RPR_NAV_SET,
+	EEOP_RPR_NAV_RESTORE,
+
 	/* aggregation related nodes */
 	EEOP_AGG_STRICT_DESERIALIZE,
 	EEOP_AGG_DESERIALIZE,
@@ -691,6 +695,17 @@ typedef struct ExprEvalStep
 			SubPlanState *sstate;
 		}			subplan;
 
+		/* for EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE */
+		struct
+		{
+			WindowAggState *winstate;
+			int64		offset; /* 1-arg: static signed offset (PREV=-1,
+								 * NEXT=+1) */
+			Datum	   *offset_value;	/* 2-arg: runtime offset value, or
+										 * NULL */
+			bool	   *offset_isnull;	/* 2-arg: runtime offset null flag */
+		}			rpr_nav;
+
 		/* for EEOP_AGG_*DESERIALIZE */
 		struct
 		{
@@ -898,6 +913,10 @@ extern void ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
 									 ExprContext *econtext);
 extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
 							ExprContext *econtext);
+extern void ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op,
+							  ExprContext *econtext);
+extern void ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
+								  ExprContext *econtext);
 extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
 								ExprContext *econtext);
 extern void ExecEvalSysVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/nodeWindowAgg.h b/src/include/executor/nodeWindowAgg.h
index ada4a1c458c..f6f6645131c 100644
--- a/src/include/executor/nodeWindowAgg.h
+++ b/src/include/executor/nodeWindowAgg.h
@@ -20,4 +20,7 @@ extern WindowAggState *ExecInitWindowAgg(WindowAgg *node, EState *estate, int ef
 extern void ExecEndWindowAgg(WindowAggState *node);
 extern void ExecReScanWindowAgg(WindowAggState *node);
 
+/* RPR navigation support for expression evaluation opcodes */
+extern TupleTableSlot *ExecRPRNavGetSlot(WindowAggState *winstate, int64 pos);
+
 #endif							/* NODEWINDOWAGG_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 3681d905bde..9c692301313 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2704,10 +2704,12 @@ typedef struct WindowAggState
 	TupleTableSlot *temp_slot_1;
 	TupleTableSlot *temp_slot_2;
 
-	/* temporary slots for RPR */
-	TupleTableSlot *prev_slot;	/* PREV row navigation operator */
-	TupleTableSlot *next_slot;	/* NEXT row navigation operator */
-	TupleTableSlot *null_slot;	/* all NULL slot */
+	/* RPR navigation */
+	struct WindowObjectData *nav_winobj;	/* winobj for RPR nav fetch */
+	int64		nav_slot_pos;	/* position cached in nav_slot, or -1 */
+	TupleTableSlot *nav_slot;	/* slot for PREV/NEXT target row */
+	TupleTableSlot *nav_saved_outertuple;	/* saved slot during nav swap */
+	TupleTableSlot *nav_null_slot;	/* all NULL slot */
 
 	/*
 	 * Each byte corresponds to a row positioned at absolute its pos in
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 384df50c80a..7143570f4e0 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -648,6 +648,37 @@ typedef struct WindowFuncRunCondition
 	Expr	   *arg;
 } WindowFuncRunCondition;
 
+/*
+ * RPRNavExpr
+ *
+ * Represents a PREV() or NEXT() navigation call in an RPR DEFINE clause.
+ * At expression compile time this is translated into EEOP_RPR_NAV_SET /
+ * EEOP_RPR_NAV_RESTORE opcodes rather than a normal function call.
+ *
+ * kind:       RPR_NAV_PREV (-1) or RPR_NAV_NEXT (+1)
+ * arg:        the expression to evaluate against the target row
+ * offset_arg: optional explicit offset expression (2-arg form); NULL for
+ *             the 1-arg form which uses an implicit offset of 1
+ */
+typedef enum RPRNavKind
+{
+	RPR_NAV_PREV = -1,
+	RPR_NAV_NEXT = 1,
+} RPRNavKind;
+
+typedef struct RPRNavExpr
+{
+	Expr		xpr;
+	RPRNavKind	kind;			/* PREV or NEXT */
+	Expr	   *arg;			/* argument expression */
+	Expr	   *offset_arg;		/* offset expression, or NULL for 1-arg form */
+	Oid			resulttype;		/* result type (same as arg's type) */
+	/* OID of collation of result */
+	Oid			resultcollid pg_node_attr(query_jumble_ignore);
+	/* token location, or -1 if unknown */
+	ParseLoc	location;
+} RPRNavExpr;
+
 /*
  * MergeSupportFunc
  *
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index e72171050c7..79b5e63a657 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1018,6 +1018,497 @@ WINDOW w AS (
   5 |     0.1 |     0
 (5 rows)
 
+--
+-- Error cases: PREV/NEXT usage restrictions
+--
+-- PREV outside DEFINE clause
+SELECT prev(price) FROM stock;
+ERROR:  prev can only be used in a DEFINE clause
+LINE 1: SELECT prev(price) FROM stock;
+               ^
+-- NEXT outside DEFINE clause
+SELECT next(price) FROM stock;
+ERROR:  next can only be used in a DEFINE clause
+LINE 1: SELECT next(price) FROM stock;
+               ^
+-- Nested PREV
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > PREV(PREV(price))
+);
+ERROR:  PREV and NEXT cannot be nested
+LINE 7:     DEFINE A AS price > PREV(PREV(price))
+                                ^
+-- Nested NEXT
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > NEXT(NEXT(price))
+);
+ERROR:  PREV and NEXT cannot be nested
+LINE 7:     DEFINE A AS price > NEXT(NEXT(price))
+                                ^
+-- PREV nested inside NEXT
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > NEXT(PREV(price))
+);
+ERROR:  PREV and NEXT cannot be nested
+LINE 7:     DEFINE A AS price > NEXT(PREV(price))
+                                ^
+-- PREV nested inside expression inside NEXT
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > NEXT(price * PREV(price))
+);
+ERROR:  PREV and NEXT cannot be nested
+LINE 7:     DEFINE A AS price > NEXT(price * PREV(price))
+                                ^
+-- Triple nesting: error reported at outermost PREV
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > PREV(PREV(PREV(price)))
+);
+ERROR:  PREV and NEXT cannot be nested
+LINE 7:     DEFINE A AS price > PREV(PREV(PREV(price)))
+                                ^
+-- No column reference in PREV/NEXT argument
+-- PREV(1): constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(1) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(1) > 0
+                        ^
+-- NEXT(1 + 2): constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(1 + 2) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS NEXT(1 + 2) > 0
+                        ^
+-- 2-arg form: PREV(1, 1): constant expression as first arg
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(1, 1) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(1, 1) > 0
+                        ^
+-- Non-constant offset: column reference as offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(price, price) > 0
+);
+ERROR:  PREV/NEXT offset must be a run-time constant
+LINE 7:     DEFINE A AS PREV(price, price) > 0
+                        ^
+-- Non-constant offset: volatile function as offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(price, random()::int) > 0
+);
+ERROR:  PREV/NEXT offset must be a run-time constant
+LINE 7:     DEFINE A AS PREV(price, random()::int) > 0
+                        ^
+--
+-- 2-arg PREV/NEXT: functional tests
+--
+-- PREV(price, 2): match rows where current price > price 2 rows back
+-- stock: 100, 90, 80, 95, 110
+-- Pattern (A B+): A=any, B where price > PREV(price, 2)
+-- At pos 2 (80): A matches. pos 3 (95): 95 > PREV(95,2)=90 TRUE.
+--                             pos 4 (110): 110 > PREV(110,2)=80 TRUE. Match!
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS price > PREV(price, 2)
+);
+ company  |   tdate    | price | first_value | last_value | count 
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 |   100 |             |            |     0
+ company1 | 07-02-2023 |   200 |         200 |        150 |     2
+ company1 | 07-03-2023 |   150 |             |            |     0
+ company1 | 07-04-2023 |   140 |             |            |     0
+ company1 | 07-05-2023 |   150 |             |            |     0
+ company1 | 07-06-2023 |    90 |             |            |     0
+ company1 | 07-07-2023 |   110 |         110 |        120 |     3
+ company1 | 07-08-2023 |   130 |             |            |     0
+ company1 | 07-09-2023 |   120 |             |            |     0
+ company1 | 07-10-2023 |   130 |             |            |     0
+ company2 | 07-01-2023 |    50 |             |            |     0
+ company2 | 07-02-2023 |  2000 |        2000 |       1500 |     2
+ company2 | 07-03-2023 |  1500 |             |            |     0
+ company2 | 07-04-2023 |  1400 |             |            |     0
+ company2 | 07-05-2023 |  1500 |             |            |     0
+ company2 | 07-06-2023 |    60 |             |            |     0
+ company2 | 07-07-2023 |  1100 |        1100 |       1200 |     3
+ company2 | 07-08-2023 |  1300 |             |            |     0
+ company2 | 07-09-2023 |  1200 |             |            |     0
+ company2 | 07-10-2023 |  1300 |             |            |     0
+(20 rows)
+
+-- NEXT(price, 2): match rows where current price > price 2 rows ahead
+-- pos 0 (100): NEXT(100,2)=80, 100>80 TRUE. pos 1 (90): NEXT(90,2)=95, 90>95 FALSE. Match ends.
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price > NEXT(price, 2)
+);
+ company  |   tdate    | price | first_value | last_value | count 
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 |   100 |             |            |     0
+ company1 | 07-02-2023 |   200 |         200 |        200 |     1
+ company1 | 07-03-2023 |   150 |             |            |     0
+ company1 | 07-04-2023 |   140 |         140 |        150 |     2
+ company1 | 07-05-2023 |   150 |             |            |     0
+ company1 | 07-06-2023 |    90 |             |            |     0
+ company1 | 07-07-2023 |   110 |             |            |     0
+ company1 | 07-08-2023 |   130 |             |            |     0
+ company1 | 07-09-2023 |   120 |             |            |     0
+ company1 | 07-10-2023 |   130 |             |            |     0
+ company2 | 07-01-2023 |    50 |             |            |     0
+ company2 | 07-02-2023 |  2000 |        2000 |       2000 |     1
+ company2 | 07-03-2023 |  1500 |             |            |     0
+ company2 | 07-04-2023 |  1400 |        1400 |       1500 |     2
+ company2 | 07-05-2023 |  1500 |             |            |     0
+ company2 | 07-06-2023 |    60 |             |            |     0
+ company2 | 07-07-2023 |  1100 |             |            |     0
+ company2 | 07-08-2023 |  1300 |             |            |     0
+ company2 | 07-09-2023 |  1200 |             |            |     0
+ company2 | 07-10-2023 |  1300 |             |            |     0
+(20 rows)
+
+-- Expressions inside PREV/NEXT arg: expr is evaluated on target row
+-- PREV(price - 50, 1): fetches (price - 50) from 1 row back
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price > PREV(price - 50, 1)
+);
+ company  |   tdate    | price | first_value | last_value | count 
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 |   100 |             |            |     0
+ company1 | 07-02-2023 |   200 |         200 |        200 |     1
+ company1 | 07-03-2023 |   150 |             |            |     0
+ company1 | 07-04-2023 |   140 |         140 |        150 |     2
+ company1 | 07-05-2023 |   150 |             |            |     0
+ company1 | 07-06-2023 |    90 |             |            |     0
+ company1 | 07-07-2023 |   110 |         110 |        130 |     4
+ company1 | 07-08-2023 |   130 |             |            |     0
+ company1 | 07-09-2023 |   120 |             |            |     0
+ company1 | 07-10-2023 |   130 |             |            |     0
+ company2 | 07-01-2023 |    50 |             |            |     0
+ company2 | 07-02-2023 |  2000 |        2000 |       2000 |     1
+ company2 | 07-03-2023 |  1500 |             |            |     0
+ company2 | 07-04-2023 |  1400 |             |            |     0
+ company2 | 07-05-2023 |  1500 |        1500 |       1500 |     1
+ company2 | 07-06-2023 |    60 |             |            |     0
+ company2 | 07-07-2023 |  1100 |        1100 |       1300 |     2
+ company2 | 07-08-2023 |  1300 |             |            |     0
+ company2 | 07-09-2023 |  1200 |             |            |     0
+ company2 | 07-10-2023 |  1300 |        1300 |       1300 |     1
+(20 rows)
+
+-- NEXT(price * 2, 1): fetches (price * 2) from 1 row ahead
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price < NEXT(price * 2, 1)
+);
+ company  |   tdate    | price | first_value | last_value | count 
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 |   100 |         100 |        120 |     9
+ company1 | 07-02-2023 |   200 |             |            |     0
+ company1 | 07-03-2023 |   150 |             |            |     0
+ company1 | 07-04-2023 |   140 |             |            |     0
+ company1 | 07-05-2023 |   150 |             |            |     0
+ company1 | 07-06-2023 |    90 |             |            |     0
+ company1 | 07-07-2023 |   110 |             |            |     0
+ company1 | 07-08-2023 |   130 |             |            |     0
+ company1 | 07-09-2023 |   120 |             |            |     0
+ company1 | 07-10-2023 |   130 |             |            |     0
+ company2 | 07-01-2023 |    50 |          50 |       1400 |     4
+ company2 | 07-02-2023 |  2000 |             |            |     0
+ company2 | 07-03-2023 |  1500 |             |            |     0
+ company2 | 07-04-2023 |  1400 |             |            |     0
+ company2 | 07-05-2023 |  1500 |             |            |     0
+ company2 | 07-06-2023 |    60 |          60 |       1200 |     4
+ company2 | 07-07-2023 |  1100 |             |            |     0
+ company2 | 07-08-2023 |  1300 |             |            |     0
+ company2 | 07-09-2023 |  1200 |             |            |     0
+ company2 | 07-10-2023 |  1300 |             |            |     0
+(20 rows)
+
+-- Large offset: PREV(val, 999) on 1000-row series matches only last row
+-- NEXT(val, 999) matches only first row
+SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w
+FROM generate_series(1, 1000) AS t(val)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(val, 999) = 1
+)
+ORDER BY val DESC LIMIT 3;
+ val  | first_value | last_value | count 
+------+-------------+------------+-------
+ 1000 |        1000 |       1000 |     1
+  999 |             |            |     0
+  998 |             |            |     0
+(3 rows)
+
+SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w
+FROM generate_series(1, 1000) AS t(val)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(val, 999) = 1000
+)
+LIMIT 3;
+ val | first_value | last_value | count 
+-----+-------------+------------+-------
+   1 |           1 |          1 |     1
+   2 |             |            |     0
+   3 |             |            |     0
+(3 rows)
+
+-- PREV(price, 0): offset 0 means current row, always equal to price
+-- A+ matches entire partition as one group; count = partition size
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(price, 0) = price
+);
+ company  |   tdate    | price | first_value | last_value | count 
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 |   100 |         100 |        130 |    10
+ company1 | 07-02-2023 |   200 |             |            |     0
+ company1 | 07-03-2023 |   150 |             |            |     0
+ company1 | 07-04-2023 |   140 |             |            |     0
+ company1 | 07-05-2023 |   150 |             |            |     0
+ company1 | 07-06-2023 |    90 |             |            |     0
+ company1 | 07-07-2023 |   110 |             |            |     0
+ company1 | 07-08-2023 |   130 |             |            |     0
+ company1 | 07-09-2023 |   120 |             |            |     0
+ company1 | 07-10-2023 |   130 |             |            |     0
+ company2 | 07-01-2023 |    50 |          50 |       1300 |    10
+ company2 | 07-02-2023 |  2000 |             |            |     0
+ company2 | 07-03-2023 |  1500 |             |            |     0
+ company2 | 07-04-2023 |  1400 |             |            |     0
+ company2 | 07-05-2023 |  1500 |             |            |     0
+ company2 | 07-06-2023 |    60 |             |            |     0
+ company2 | 07-07-2023 |  1100 |             |            |     0
+ company2 | 07-08-2023 |  1300 |             |            |     0
+ company2 | 07-09-2023 |  1200 |             |            |     0
+ company2 | 07-10-2023 |  1300 |             |            |     0
+(20 rows)
+
+-- 2-arg PREV/NEXT outside DEFINE clause
+SELECT prev(price, 2) FROM stock;
+ERROR:  prev can only be used in a DEFINE clause
+LINE 1: SELECT prev(price, 2) FROM stock;
+               ^
+SELECT next(price, 2) FROM stock;
+ERROR:  next can only be used in 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
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(price, -1) IS NOT NULL
+);
+ERROR:  PREV/NEXT offset must not be negative
+-- 2-arg PREV/NEXT: NULL offset (typed)
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(price, NULL::int8) IS NOT NULL
+);
+ERROR:  PREV/NEXT offset must not be null
+-- 2-arg PREV/NEXT: NULL offset (untyped)
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(price, NULL) IS NOT NULL
+);
+ERROR:  PREV/NEXT offset must not be null
+-- 2-arg PREV/NEXT: host variable negative and NULL
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price > PREV(price, $1)
+);
+EXECUTE test_prev_offset(-1);
+ERROR:  PREV/NEXT offset must not be negative
+EXECUTE test_prev_offset(NULL);
+ERROR:  PREV/NEXT offset must not be null
+DEALLOCATE test_prev_offset;
+-- 2-arg PREV/NEXT: host variable with expression (0 + $1)
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price > PREV(price, 0 + $1)
+);
+EXECUTE test_prev_offset(-1);
+ERROR:  PREV/NEXT offset must not be negative
+EXECUTE test_prev_offset(NULL);
+ERROR:  PREV/NEXT offset must not be null
+DEALLOCATE test_prev_offset;
+-- 2-arg: two PREV with different offsets in same DEFINE clause
+-- B: price exceeds both 1-back and 2-back values
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS price > PREV(price, 1) AND price > PREV(price, 2)
+);
+ company  |   tdate    | price | first_value | last_value | count 
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 |   100 |             |            |     0
+ company1 | 07-02-2023 |   200 |             |            |     0
+ company1 | 07-03-2023 |   150 |             |            |     0
+ company1 | 07-04-2023 |   140 |             |            |     0
+ company1 | 07-05-2023 |   150 |             |            |     0
+ company1 | 07-06-2023 |    90 |             |            |     0
+ company1 | 07-07-2023 |   110 |         110 |        130 |     2
+ company1 | 07-08-2023 |   130 |             |            |     0
+ company1 | 07-09-2023 |   120 |             |            |     0
+ company1 | 07-10-2023 |   130 |             |            |     0
+ company2 | 07-01-2023 |    50 |             |            |     0
+ company2 | 07-02-2023 |  2000 |             |            |     0
+ company2 | 07-03-2023 |  1500 |             |            |     0
+ company2 | 07-04-2023 |  1400 |             |            |     0
+ company2 | 07-05-2023 |  1500 |             |            |     0
+ company2 | 07-06-2023 |    60 |             |            |     0
+ company2 | 07-07-2023 |  1100 |        1100 |       1300 |     2
+ company2 | 07-08-2023 |  1300 |             |            |     0
+ company2 | 07-09-2023 |  1200 |             |            |     0
+ company2 | 07-10-2023 |  1300 |             |            |     0
+(20 rows)
+
+-- 2-arg: PREV and NEXT with explicit offsets in same DEFINE clause
+-- A: price exceeds 1-back and is below 1-ahead (ascending interior point)
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+)
+    DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
+);
+ company  |   tdate    | price | first_value | last_value | count 
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 |   100 |             |            |     0
+ company1 | 07-02-2023 |   200 |             |            |     0
+ company1 | 07-03-2023 |   150 |             |            |     0
+ company1 | 07-04-2023 |   140 |             |            |     0
+ company1 | 07-05-2023 |   150 |             |            |     0
+ company1 | 07-06-2023 |    90 |             |            |     0
+ company1 | 07-07-2023 |   110 |         110 |        110 |     1
+ company1 | 07-08-2023 |   130 |             |            |     0
+ company1 | 07-09-2023 |   120 |             |            |     0
+ company1 | 07-10-2023 |   130 |             |            |     0
+ company2 | 07-01-2023 |    50 |             |            |     0
+ company2 | 07-02-2023 |  2000 |             |            |     0
+ company2 | 07-03-2023 |  1500 |             |            |     0
+ company2 | 07-04-2023 |  1400 |             |            |     0
+ company2 | 07-05-2023 |  1500 |             |            |     0
+ company2 | 07-06-2023 |    60 |             |            |     0
+ company2 | 07-07-2023 |  1100 |        1100 |       1100 |     1
+ company2 | 07-08-2023 |  1300 |             |            |     0
+ company2 | 07-09-2023 |  1200 |             |            |     0
+ company2 | 07-10-2023 |  1300 |             |            |     0
+(20 rows)
+
 --
 -- SKIP TO / Backtracking / Frame boundary
 --
@@ -1479,7 +1970,7 @@ count(*) OVER w
 (14 rows)
 
 -- ReScan test: LATERAL join forces WindowAgg rescan with RPR
--- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+-- Tests ExecReScanWindowAgg clearing nav_slot
 SELECT g.x, sub.*
 FROM generate_series(1, 2) g(x),
 LATERAL (
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index bd345906133..0d2b7550ea8 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3646,9 +3646,89 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (8 rows)
 
--- Using NULL comparisons
+-- Using 1-arg PREV (implicit offset 1)
 CREATE VIEW rpr_ev83 AS
 SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+          line           
+-------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v > PREV(v)) );
+(3 rows)
+
+-- Using 1-arg NEXT (implicit offset 1)
+CREATE VIEW rpr_ev84 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+          line           
+-------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v < NEXT(v)) );
+(3 rows)
+
+-- Using 2-arg PREV (explicit offset)
+CREATE VIEW rpr_ev85 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+                 line                 
+--------------------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v > PREV(v, (2)::bigint)) );
+(3 rows)
+
+-- Using 2-arg NEXT (explicit offset)
+CREATE VIEW rpr_ev86 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+                 line                 
+--------------------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v < NEXT(v, (2)::bigint)) );
+(3 rows)
+
+-- Using NULL comparisons
+CREATE VIEW rpr_ev87 AS
+SELECT count(*) OVER w
 FROM (
     SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
     FROM generate_series(1, 30) v
@@ -3659,7 +3739,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev87'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3695,7 +3775,7 @@ WINDOW w AS (
 -- Large Scale Statistics Verification
 -- ============================================================
 -- 500 rows - verify statistics scale correctly
-CREATE VIEW rpr_ev84 AS
+CREATE VIEW rpr_ev88 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3704,7 +3784,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev88'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a+ b c) 
@@ -3734,7 +3814,7 @@ WINDOW w AS (
 (9 rows)
 
 -- High match count scenario
-CREATE VIEW rpr_ev85 AS
+CREATE VIEW rpr_ev89 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3743,7 +3823,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev89'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -3773,7 +3853,7 @@ WINDOW w AS (
 (9 rows)
 
 -- High skip count scenario
-CREATE VIEW rpr_ev86 AS
+CREATE VIEW rpr_ev90 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3787,7 +3867,7 @@ WINDOW w AS (
         D AS v % 100 = 4,
         E AS v % 100 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev90'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN (a b c d e) 
@@ -3831,17 +3911,17 @@ WINDOW w AS (
 -- Test with row_number() as representative case.
 --
 -- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
-CREATE VIEW rpr_ev87 AS
+CREATE VIEW rpr_ev91 AS
 SELECT row_number() OVER w
 FROM generate_series(1, 10) AS s(v)
 WINDOW w AS (
     ORDER BY v
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 );
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev91;
                           QUERY PLAN                          
 --------------------------------------------------------------
- Subquery Scan on rpr_ev87
+ Subquery Scan on rpr_ev91
    ->  WindowAgg
          Window: w AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING)
          ->  Sort
@@ -3850,7 +3930,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
 (6 rows)
 
 -- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-CREATE VIEW rpr_ev88 AS
+CREATE VIEW rpr_ev92 AS
 SELECT row_number() OVER w
 FROM generate_series(1, 10) AS s(v)
 WINDOW w AS (
@@ -3861,10 +3941,10 @@ WINDOW w AS (
     DEFINE
         B AS v > PREV(v)
 );
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev92;
                                       QUERY PLAN                                      
 --------------------------------------------------------------------------------------
- Subquery Scan on rpr_ev88
+ Subquery Scan on rpr_ev92
    ->  WindowAgg
          Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
@@ -3877,7 +3957,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
 -- Planner optimization: non-RPR and RPR windows that share the same base frame
 -- after frame optimization are kept as separate WindowAgg nodes.
 --
-CREATE VIEW rpr_ev89 AS
+CREATE VIEW rpr_ev93 AS
 SELECT
     row_number() OVER w_normal AS rn_normal,
     row_number() OVER w_rpr AS rn_rpr
@@ -3890,10 +3970,10 @@ WINDOW
         PATTERN (A+)
         DEFINE A AS v > 1
     );
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev89;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev93;
                                         QUERY PLAN                                        
 ------------------------------------------------------------------------------------------
- Subquery Scan on rpr_ev89
+ Subquery Scan on rpr_ev93
    ->  WindowAgg
          Window: w_rpr AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a+"
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 95794d409e1..787ebddcaec 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -440,6 +440,295 @@ WINDOW w AS (
   B AS val > PREV(val) * 0.99
 );
 
+--
+-- 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 (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > PREV(PREV(price))
+);
+
+-- Nested NEXT
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > NEXT(NEXT(price))
+);
+
+-- PREV nested inside NEXT
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > NEXT(PREV(price))
+);
+
+-- PREV nested inside expression inside NEXT
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > NEXT(price * PREV(price))
+);
+
+-- Triple nesting: error reported at outermost PREV
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > PREV(PREV(PREV(price)))
+);
+
+-- No column reference in PREV/NEXT argument
+-- PREV(1): constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(1) > 0
+);
+
+-- NEXT(1 + 2): constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(1 + 2) > 0
+);
+
+-- 2-arg form: PREV(1, 1): constant expression as first arg
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(1, 1) > 0
+);
+
+-- Non-constant offset: column reference as offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(price, price) > 0
+);
+
+-- Non-constant offset: volatile function as offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(price, random()::int) > 0
+);
+
+--
+-- 2-arg PREV/NEXT: functional tests
+--
+
+-- PREV(price, 2): match rows where current price > price 2 rows back
+-- stock: 100, 90, 80, 95, 110
+-- Pattern (A B+): A=any, B where price > PREV(price, 2)
+-- At pos 2 (80): A matches. pos 3 (95): 95 > PREV(95,2)=90 TRUE.
+--                             pos 4 (110): 110 > PREV(110,2)=80 TRUE. Match!
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS price > PREV(price, 2)
+);
+
+-- NEXT(price, 2): match rows where current price > price 2 rows ahead
+-- pos 0 (100): NEXT(100,2)=80, 100>80 TRUE. pos 1 (90): NEXT(90,2)=95, 90>95 FALSE. Match ends.
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price > NEXT(price, 2)
+);
+
+-- Expressions inside PREV/NEXT arg: expr is evaluated on target row
+-- PREV(price - 50, 1): fetches (price - 50) from 1 row back
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price > PREV(price - 50, 1)
+);
+
+-- NEXT(price * 2, 1): fetches (price * 2) from 1 row ahead
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price < NEXT(price * 2, 1)
+);
+
+-- Large offset: PREV(val, 999) on 1000-row series matches only last row
+-- NEXT(val, 999) matches only first row
+SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w
+FROM generate_series(1, 1000) AS t(val)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(val, 999) = 1
+)
+ORDER BY val DESC LIMIT 3;
+
+SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w
+FROM generate_series(1, 1000) AS t(val)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(val, 999) = 1000
+)
+LIMIT 3;
+
+-- PREV(price, 0): offset 0 means current row, always equal to price
+-- A+ matches entire partition as one group; count = partition size
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    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
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(price, -1) IS NOT NULL
+);
+
+-- 2-arg PREV/NEXT: NULL offset (typed)
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(price, NULL::int8) IS NOT NULL
+);
+
+-- 2-arg PREV/NEXT: NULL offset (untyped)
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(price, NULL) IS NOT NULL
+);
+
+-- 2-arg PREV/NEXT: host variable negative and NULL
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price > PREV(price, $1)
+);
+EXECUTE test_prev_offset(-1);
+EXECUTE test_prev_offset(NULL);
+DEALLOCATE test_prev_offset;
+
+-- 2-arg PREV/NEXT: host variable with expression (0 + $1)
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS price > PREV(price, 0 + $1)
+);
+EXECUTE test_prev_offset(-1);
+EXECUTE test_prev_offset(NULL);
+DEALLOCATE test_prev_offset;
+
+-- 2-arg: two PREV with different offsets in same DEFINE clause
+-- B: price exceeds both 1-back and 2-back values
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS price > PREV(price, 1) AND price > PREV(price, 2)
+);
+
+-- 2-arg: PREV and NEXT with explicit offsets in same DEFINE clause
+-- A: price exceeds 1-back and is below 1-ahead (ascending interior point)
+SELECT company, tdate, price,
+       first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+)
+    DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
+);
+
 --
 -- SKIP TO / Backtracking / Frame boundary
 --
@@ -671,7 +960,7 @@ count(*) OVER w
 );
 
 -- ReScan test: LATERAL join forces WindowAgg rescan with RPR
--- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+-- Tests ExecReScanWindowAgg clearing nav_slot
 SELECT g.x, sub.*
 FROM generate_series(1, 2) g(x),
 LATERAL (
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 93e06b0cbdf..c2cbe2edd59 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2088,9 +2088,65 @@ WINDOW w AS (
         D AS v < PREV(v)
 );');
 
--- Using NULL comparisons
+-- Using 1-arg PREV (implicit offset 1)
 CREATE VIEW rpr_ev83 AS
 SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 1-arg NEXT (implicit offset 1)
+CREATE VIEW rpr_ev84 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 2-arg PREV (explicit offset)
+CREATE VIEW rpr_ev85 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 2-arg NEXT (explicit offset)
+CREATE VIEW rpr_ev86 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using NULL comparisons
+CREATE VIEW rpr_ev87 AS
+SELECT count(*) OVER w
 FROM (
     SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
     FROM generate_series(1, 30) v
@@ -2101,7 +2157,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev87'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2121,7 +2177,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- 500 rows - verify statistics scale correctly
-CREATE VIEW rpr_ev84 AS
+CREATE VIEW rpr_ev88 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2130,7 +2186,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev88'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2143,7 +2199,7 @@ WINDOW w AS (
 );');
 
 -- High match count scenario
-CREATE VIEW rpr_ev85 AS
+CREATE VIEW rpr_ev89 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2152,7 +2208,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev89'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2165,7 +2221,7 @@ WINDOW w AS (
 );');
 
 -- High skip count scenario
-CREATE VIEW rpr_ev86 AS
+CREATE VIEW rpr_ev90 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2179,7 +2235,7 @@ WINDOW w AS (
         D AS v % 100 = 4,
         E AS v % 100 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev90'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2207,7 +2263,7 @@ WINDOW w AS (
 --
 
 -- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
-CREATE VIEW rpr_ev87 AS
+CREATE VIEW rpr_ev91 AS
 SELECT row_number() OVER w
 FROM generate_series(1, 10) AS s(v)
 WINDOW w AS (
@@ -2215,10 +2271,10 @@ WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 );
 
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev91;
 
 -- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-CREATE VIEW rpr_ev88 AS
+CREATE VIEW rpr_ev92 AS
 SELECT row_number() OVER w
 FROM generate_series(1, 10) AS s(v)
 WINDOW w AS (
@@ -2230,13 +2286,13 @@ WINDOW w AS (
         B AS v > PREV(v)
 );
 
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev92;
 
 --
 -- Planner optimization: non-RPR and RPR windows that share the same base frame
 -- after frame optimization are kept as separate WindowAgg nodes.
 --
-CREATE VIEW rpr_ev89 AS
+CREATE VIEW rpr_ev93 AS
 SELECT
     row_number() OVER w_normal AS rn_normal,
     row_number() OVER w_rpr AS rn_rpr
@@ -2250,7 +2306,7 @@ WINDOW
         DEFINE A AS v > 1
     );
 
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev89;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev93;
 
 --
 -- Planner optimization: find_window_run_conditions must not push down
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 36091f56418..ca9faabc7e3 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1775,7 +1775,6 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
-NavigationInfo
 NestLoop
 NestLoopParam
 NestLoopState
@@ -2441,6 +2440,8 @@ QuerySource
 QueueBackendStatus
 QueuePosition
 QuitSignalReason
+RPRNavExpr
+RPRNavKind
 RBTNode
 RBTOrderControl
 RBTree
-- 
2.50.1 (Apple Git-155)

---

From: Henson Choi <[email-masked]>
Date: Sat, 21 Mar 2026 21:42:52 +0900

Hi Tatsuo,

> So, if RPR is active, WinSetMarkPosition() is not called at all?  That
> seems too strong limitation.  Can't we set mark at frameheadpos even
> if RPR is active? It seems safe since the only allowed franme start
> option is ROWS BETWEEN CURRENT ROW and we never step back to the rows
> before frameheadpos.

You raise a valid point. Setting mark at frameheadpos would be safe
for the window function's own read pointer, since we only allow
ROWS BETWEEN CURRENT ROW.

However, the difficulty is with the PREV/NEXT navigation. In the
experimental patch, PREV accepts an optional offset argument --
e.g. PREV(price, N) -- and this offset can be a runtime expression
whose value is not known until evaluation time. If the mark is
advanced to frameheadpos, PREV(price, N) with a large N could try
to access rows that the tuplestore has already truncated.

This is why the experimental patch takes a different approach: it
creates a separate nav_winobj with its own mark pointer pinned at
position 0, so that the tuplestore never truncates rows that PREV
might need. The window function's own mark management is left
unchanged.

The mark=0 approach is safe but conservative -- it prevents the
tuplestore from releasing any rows within the partition. The SQL
standard (ISO/IEC 19075-5, Subclause 5.6.2) requires the offset
argument to be a "runtime constant" -- meaning it cannot reference
columns or row pattern variables. So in practice, the maximum
offset is always known at plan time, which opens the door for a
future optimization: advance the mark to (currentpos - max_offset)
when all PREV offsets in the DEFINE clause are constant.

Since the experimental patch redesigns how mark works for PREV/NEXT
navigation, I think we should drop 0004 from the current series
and revisit the set_mark question together with the experimental
patch. What do you think?

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Sat, 21 Mar 2026 23:57:06 +0900

2026년 3월 21일 (토) 23:16, Tatsuo Ishii <[email-masked]>님이 작성:

> Agreed. So I just want to make sure I create v46 using 0001-0003 and
> 0005-0015. Am I correct?

Yes, that’s right!

---

From: Henson Choi <[email-masked]>
Date: Sun, 29 Mar 2026 21:13:47 +0900

Hi Tatsuo,

Attached are 6 incremental patches on top of v46.

0001-0005 are small independent fixes. 0006 is the main
PREV/NEXT navigation redesign that was discussed as the
"experimental" implementation in prior messages.

Here is a summary of each patch:

  0001: Remove unused regex/regex.h include from nodeWindowAgg.c
        The regex header was left over from an earlier implementation
        that used the regex engine for pattern matching. The current
        NFA engine in execRPR.c does not use it.

  0002: Add CHECK_FOR_INTERRUPTS() to nfa_add_state_unique()
        This function iterates through a linked list of NFA states
        to check for duplicates. In state explosion scenarios
        (complex patterns with alternations and quantifiers), this
        list can grow significantly. Without an interrupt check,
        the loop is unresponsive to query cancellation.

  0003: Add CHECK_FOR_INTERRUPTS() to nfa_try_absorb_context()
        Similar to 0002. The absorption loop iterates through the
        context chain, which can grow unbounded with streaming
        window patterns. Each iteration also calls
        nfa_states_covered() which has its own loop.

  0004: Fix in-place modification of defineClause TargetEntry
        In set_upper_references(), the defineClause TargetEntry
        was modified in-place by fix_upper_expr() without making
        a copy first. This follows the same flatCopyTargetEntry()
        pattern already used for the main targetlist in the same
        function.

  0005: Fix mark handling for last_value() under RPR
        This is a revised version of the v45 0004 patch that you
        commented on [1]. You pointed out that suppressing
        WinSetMarkPosition() entirely under RPR was too strong a
        limitation, and we agreed to drop it and revisit together
        with the experimental PREV/NEXT patch.

        The RPR executor patch changed set_mark from true to
        false in window_last_value() to avoid mark advancement
        problems under RPR. But this also penalizes non-RPR
        queries by preventing tuplestore memory reclamation.

        The revised approach: restore set_mark=true (upstream
        default), and add a targeted guard in
        WinGetFuncArgInFrame() that only suppresses mark
        advancement for SEEK_TAIL under RPR -- not for all
        seek types as in v45 0004. Only SEEK_TAIL needs
        suppression because advancing the mark from the tail
        could prevent revisiting earlier rows that still fall
        within a future row's reduced frame.

        [1]
https://www.postgresql.org/message-id/20260321.140232.1947157589839395257.ishii%40postgresql.org

  0006: Implement 1-slot PREV/NEXT navigation for RPR
        This is the main patch. It redesigns PREV/NEXT navigation
        from the 3-slot model (outer/scan/inner with varno
        rewriting) to a 1-slot model using expression opcodes.

        Key changes:

        - RPRNavExpr: a new expression node type that replaces
          the previous approach of identifying PREV/NEXT by
          funcid. The parser transforms PREV/NEXT function calls
          into RPRNavExpr nodes in ParseFuncOrColumn().

        - EEOP_RPR_NAV_SET/RESTORE: two new opcodes that
          temporarily swap ecxt_outertuple to the target row
          during expression evaluation. The argument expression
          evaluates against the swapped slot, then the original
          slot is restored. This eliminates varno rewriting and
          naturally supports arbitrary offsets.

        - 2-argument form: PREV(value, offset) and
          NEXT(value, offset) are now supported. The offset
          defaults to 1 if omitted; offset=0 refers to the
          current row. The offset must be a run-time constant
          per the SQL standard.

        - nav_winobj: a dedicated WindowObject with its own
          tuplestore read pointer, separate from aggregate
          processing. A mark pointer pinned at position 0
          prevents tuplestore truncation so that PREV(expr, N)
          can reach any prior row. This is conservative -- it
          retains the entire partition -- but since the standard
          requires offsets to be run-time constants, we could
          advance the mark to (currentpos - max_offset) in a
          future optimization. I'd prefer to defer that until
          the basic navigation is stable.

        - Documentation and tests updated.

Changes from the experimental version of 0006:

- NULL/negative offset error rationale changed from "matching
  Oracle behavior" to the SQL standard (ISO/IEC 9075-2,
  Subclause 5.6.2: "There is an exception if the value of
  the offset is negative or null"). The behavior is the same;
  only the comment was corrected.

- RPRNavKind changed from -1/+1 values to plain enum values.
  The previous version used kind as an arithmetic multiplier
  (offset * kind), but this pattern cannot extend to FIRST/LAST
  which have different position semantics. The new version uses
  a switch statement with pg_sub/add_s64_overflow instead.

- Parser validation walkers consolidated from three separate
  walkers into a single NavCheckResult struct + nav_check_walker,
  reducing tree traversals from up to 4 per RPRNavExpr to 1-2.

- JIT changed from attempting to compile to explicit interpreter
  fallback. See item 2 below for the rationale.

- Additional test cases for subquery offset (caught by
  DEFINE-level restriction), first-arg subquery, and first-arg
  volatile function (allowed per standard).

I've reviewed and revised 0006 since the experimental version.
I think it is now ready to be included in the next version of
the patch set. It passes all existing regression tests (rpr,
rpr_base, rpr_explain, rpr_nfa) and adds comprehensive tests
for the new functionality.

Two items I'd like your opinion on:

1. 0005 (mark handling): The revised approach only suppresses
   mark advancement for SEEK_TAIL under RPR, unlike v45 0004
   which suppressed it for all seek types. Does narrowing to
   SEEK_TAIL seem reasonable to you, or do you see cases where
   other seek types could also be affected?

2. LLVM JIT fallback (in 0006): The mid-expression slot swap
   conflicts with how JIT caches tuple data pointers. The
   experimental version produced wrong results under
   jit_above_cost=0, and I have not found a clean fix within
   the current JIT framework. For now, DEFINE expressions
   containing PREV/NEXT fall back to the interpreter; other
   expressions in the same query are still JIT-compiled.
   I'd like to keep this as-is for now and look for a proper
   JIT solution over time. Does that sound reasonable?

Best regards,
Henson
From da0ce590d45e35be96ddf74f3eb9b657bfca4a81 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 24 Mar 2026 19:04:19 +0900
Subject: [PATCH 1/6] Remove unused regex/regex.h include from nodeWindowAgg.c

---
 src/backend/executor/nodeWindowAgg.c | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4f882b877b1..185d7a0d5ae 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -50,7 +50,6 @@
 #include "optimizer/rpr.h"
 #include "parser/parse_agg.h"
 #include "parser/parse_coerce.h"
-#include "regex/regex.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
-- 
2.50.1 (Apple Git-155)

From 8ab2e3b303422c36185210042bbe6831d50009cc Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:25:40 +0900
Subject: [PATCH 2/6] Add CHECK_FOR_INTERRUPTS() to nfa_add_state_unique() for
 state explosion patterns

---
 src/backend/executor/execRPR.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index bab5257f68f..cf54e0c76c3 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1763,6 +1763,8 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	/* Check for duplicate and find tail */
 	for (s = ctx->states; s != NULL; s = s->next)
 	{
+		CHECK_FOR_INTERRUPTS();
+
 		if (nfa_states_equal(winstate, s, state))
 		{
 			/*
-- 
2.50.1 (Apple Git-155)

From dd30851691ade32ea18b50501b06da72839687e3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 11:03:39 +0900
Subject: [PATCH 3/6] Add CHECK_FOR_INTERRUPTS() to nfa_try_absorb_context()
 loop

---
 src/backend/executor/execRPR.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index cf54e0c76c3..58f9da0b814 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -2084,6 +2084,8 @@ nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx)
 
 	for (older = ctx->prev; older != NULL; older = older->prev)
 	{
+		CHECK_FOR_INTERRUPTS();
+
 		/*
 		 * By invariant: ctx->prev chain is in creation order (oldest first),
 		 * and each row creates at most one context. So all contexts in this
-- 
2.50.1 (Apple Git-155)

From fb824d76d6635e624da5cdee1a481d61707e73bd Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:20:05 +0900
Subject: [PATCH 4/6] Fix in-place modification of defineClause TargetEntry in
 setrefs.c

---
 src/backend/optimizer/plan/setrefs.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 69cd1861e9b..9cdb217a047 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2646,6 +2646,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 			{
 				TargetEntry *tle = (TargetEntry *) lfirst(l);
 
+				tle = flatCopyTargetEntry(tle);
 				tle->expr = (Expr *)
 					fix_upper_expr(root,
 								   (Node *) tle->expr,
@@ -2654,6 +2655,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 								   rtoffset,
 								   NRM_EQUAL,
 								   NUM_EXEC_QUAL(plan));
+				lfirst(l) = tle;
 			}
 		}
 	}
-- 
2.50.1 (Apple Git-155)

From 344fbb8a5d39a53f7ce6ff924a06d4e4d3ec2b57 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:37:50 +0900
Subject: [PATCH 5/6] Fix mark handling for last_value() under RPR

Enable mark advancement in window_last_value() for
better tuplestore memory usage in non-RPR cases, while
adding a guard in WinGetFuncArgInFrame to suppress it
for RPR SEEK_TAIL to prevent position invalidation
from reduced frame shifts.
---
 src/backend/executor/nodeWindowAgg.c | 8 ++++++++
 src/backend/utils/adt/windowfuncs.c  | 2 +-
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 185d7a0d5ae..6d696af6fd4 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4703,6 +4703,14 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 
 	Assert(WindowObjectIsValid(winobj));
 	winstate = winobj->winstate;
+
+	/*
+	 * RPR reduced frames may shift; don't advance mark from tail seeks to
+	 * avoid invalidating tuplestore read positions.
+	 */
+	if (set_mark && winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
+		set_mark = false;
+
 	econtext = winstate->ss.ps.ps_ExprContext;
 	slot = winstate->temp_slot_1;

From faff160b05eb81d0f419a396e564a2323e535c43 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 23:52:42 +0900
Subject: [PATCH 6/6] Implement 1-slot PREV/NEXT navigation for RPR

Add PREV(value [, offset]) and NEXT(value [, offset]) navigation
functions for use in the DEFINE clause of row pattern recognition.

These functions return the column value at a row offset rows
before/after the current row within the partition, returning NULL
if the target row is outside the partition. The offset defaults
to 1 if omitted; offset=0 refers to the current row itself;
NULL or negative offset raises an error.

Key design: instead of the previous 3-slot model (outer/scan/inner),
a single-slot swap model is used. EEOP_RPR_NAV_SET temporarily
replaces ecxt_outertuple with the target row, the argument
expression evaluates against it, and EEOP_RPR_NAV_RESTORE restores
the original slot. This eliminates varno rewriting and naturally
supports arbitrary offsets.

A dedicated nav_winobj with its own tuplestore read pointer avoids
interference with aggregate processing. A mark pointer pinned at
position 0 prevents tuplestore truncation so that PREV(expr, N)
can reach any prior row.

RPRNavExpr is a new expression node that replaces the previous
approach of identifying PREV/NEXT by funcid. The parser transforms
PREV/NEXT function calls into RPRNavExpr nodes in ParseFuncOrColumn().
Validation in parse_rpr.c rejects nested PREV/NEXT, requires at
least one column reference in the first argument, and ensures
the offset is a run-time constant.

RPRNavKind uses plain enum values (not -1/+1) so that FIRST/LAST
can be added later without arithmetic tricks.

LLVM JIT falls back to the interpreter for expressions containing
RPR navigation opcodes, because JIT code caches the outertuple's
tts_values/tts_isnull pointers in the entry block and the
mid-expression slot swap leaves them stale. Only DEFINE clause
expressions with PREV/NEXT are affected; other expressions in
the same query are still JIT-compiled normally.
---
 doc/src/sgml/func/func-window.sgml        |  22 +-
 src/backend/executor/execExpr.c           |  56 +++
 src/backend/executor/execExprInterp.c     | 110 +++++
 src/backend/executor/nodeWindowAgg.c      | 243 ++++------
 src/backend/jit/llvm/llvmjit_expr.c       |  40 ++
 src/backend/jit/llvm/llvmjit_types.c      |   2 +
 src/backend/nodes/nodeFuncs.c             |  33 ++
 src/backend/parser/parse_func.c           |  30 +-
 src/backend/parser/parse_rpr.c            |  87 ++++
 src/backend/utils/adt/ruleutils.c         |  16 +
 src/backend/utils/adt/windowfuncs.c       |  52 +-
 src/include/catalog/pg_proc.dat           |   6 +
 src/include/executor/execExpr.h           |  18 +
 src/include/executor/nodeWindowAgg.h      |   3 +
 src/include/nodes/execnodes.h             |  10 +-
 src/include/nodes/primnodes.h             |  31 ++
 src/test/regress/expected/rpr.out         | 551 +++++++++++++++++++++-
 src/test/regress/expected/rpr_explain.out | 114 ++++-
 src/test/regress/sql/rpr.sql              | 322 ++++++++++++-
 src/test/regress/sql/rpr_explain.sql      |  84 +++-
 src/tools/pgindent/typedefs.list          |   3 +-
 21 files changed, 1638 insertions(+), 195 deletions(-)

index ae36e0f3135..1b9b993a817 100644
+        <parameter>offset</parameter> must not be NULL.
        </para></entry>
       </row>
 
@@ -318,12 +323,17 @@
         <indexterm>
          <primary>next</primary>
         </indexterm>
+        <parameter>offset</parameter> must not be NULL.
        </para></entry>
       </row>
 
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 77229141b38..dbed4f48a0f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1222,6 +1222,62 @@ ExecInitExprRec(Expr *node, ExprState *state,
 				break;
 			}
 
+		case T_RPRNavExpr:
+			{
+				/*
+				scratch.d.rpr_nav.kind = nav->kind;
index 43116431edf..39db11301c4 100644
@@ -56,12 +56,14 @@
  */
 #include "postgres.h"
 
+#include "common/int.h"
 #include "access/tupconvert.h"
@@ -578,6 +580,8 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
@@ -2005,6 +2009,24 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
@@ -5918,3 +5940,91 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
 
+	 * Determine the unsigned offset.  For 2-arg PREV/NEXT the offset
+	 * expression has already been evaluated into offset_value.  NULL or
+	 * negative offsets are errors per the SQL standard (ISO/IEC 9075-2,
+	 * Subclause 5.6.2).
+		offset = DatumGetInt64(*op->d.rpr_nav.offset_value);
+		if (offset < 0)
+		offset = 1;
+	 * Calculate target position based on navigation direction.  On overflow,
+	 * use -1 so that ExecRPRNavGetSlot treats it as out of range.
+	switch (op->d.rpr_nav.kind)
+		case RPR_NAV_PREV:
+			if (pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos))
+		case RPR_NAV_NEXT:
index 6d696af6fd4..084c745e6ca 100644
@@ -177,14 +176,6 @@ typedef struct WindowStatePerAggData
 
-/*
 
@@ -243,9 +234,6 @@ static uint8 get_notnull_info(WindowObject winobj,
 
@@ -256,9 +244,6 @@ static void register_reduced_frame_map(WindowAggState *winstate, int64 pos,
 									   int val);
 static void update_reduced_frame(WindowObject winobj, int64 pos);

@@ -1302,6 +1287,25 @@ prepare_tuplestore(WindowAggState *winstate)
 		}
 	}
 
+	/* Create read/mark pointers for RPR navigation if needed */
+	if (winstate->nav_winobj)
+	{
+		/*
+		 * Allocate a mark pointer pinned at position 0 so that the tuplestore
+		 * never truncates rows that a PREV(expr, N) might need.
+		 *
+		 * XXX This retains the entire partition in the tuplestore.  If the
+		 * DEFINE clause only uses PREV/NEXT with small constant offsets, we
+		 * could advance the mark to (currentpos - max_offset) instead.
+		 */
+		winstate->nav_winobj->markptr =
+			tuplestore_alloc_read_pointer(winstate->buffer, 0);
+		winstate->nav_winobj->readptr =
+			tuplestore_alloc_read_pointer(winstate->buffer,
+										  EXEC_FLAG_BACKWARD);
+		winstate->nav_winobj->markpos = 0;
+	}
+
 	/*
 	 * If we are in RANGE or GROUPS mode, then determining frame boundaries
 	 * requires physical access to the frame endpoint rows, except in certain
@@ -1412,6 +1416,13 @@ begin_partition(WindowAggState *winstate)
 		winstate->aggregatedupto = 0;
 	}
 
@@ -2757,15 +2768,18 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)

 
 
 	/*
 	 * create frame head and tail slots only if needed (must create slots in
@@ -2935,6 +2949,23 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->agg_winobj = agg_winobj;
 	}
 
+	/*
 
@@ -2975,7 +3006,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
@@ -2992,7 +3025,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
@@ -3027,107 +3059,38 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 }
 
 /*
 
-static bool
 
-	if (node == NULL)
-		return false;
+	/*

-/*

@@ -3188,8 +3151,8 @@ ExecReScanWindowAgg(WindowAggState *node)
@@ -4278,6 +4241,10 @@ register_result:
@@ -4288,37 +4255,25 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	ListCell   *lc;
 	int			varIdx = 0;
 	TupleTableSlot *slot;
+	int64		saved_pos;
 
-	/*

 
 	foreach(lc, winstate->defineClauseList)
 	{
@@ -4336,6 +4291,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 			break;
 	}

diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 650f1d42a93..d158e37e7b5 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -296,6 +296,40 @@ llvm_compile_expr(ExprState *state)
 								   FIELDNO_EXPRCONTEXT_AGGNULLS,
 								   "v.econtext.aggnulls");
 
+	/*
+	 * RPR navigation opcodes (PREV/NEXT) swap ecxt_outertuple to a different
+	 * row mid-expression.  The JIT code loads v_outervalues and v_outernulls
+	 * once in the entry block and reuses them for all EEOP_OUTER_VAR steps.
+	 * After a slot swap, these pointers become stale because the new slot has
+	 * its own tts_values/tts_isnull arrays.  Fall back to the interpreter for
+	 * these expressions.
+	 *
+	 * XXX To JIT-compile these expressions properly, the NAV_SET and
+	 * NAV_RESTORE handlers would need to reload the tts_values and tts_isnull
+	 * pointers from the new slot.  However, LLVM uses SSA (Static Single
+	 * Assignment) form where each value is defined exactly once.  When
+	 * different basic blocks produce different values for the same pointer,
+	 * LLVM requires PHI nodes at the merge point to select the correct one.
+	 * Without that plumbing, OUTER_VAR steps after a slot swap would read
+	 * from the wrong pointer.
+	 */
+	if (parent && IsA(parent, WindowAggState) &&
+		((WindowAgg *) parent->plan)->rpPattern != NULL)
+	{
+		for (int opno = 0; opno < state->steps_len; opno++)
+		{
+			ExprEvalOp	opcode = ExecEvalStepOp(state, &state->steps[opno]);
+
+			if (opcode == EEOP_RPR_NAV_SET ||
+				opcode == EEOP_RPR_NAV_RESTORE)
+			{
+				LLVMDeleteFunction(eval_fn);
+				LLVMDisposeBuilder(b);
+				return false;
+			}
+		}
+	}
+
 	/* allocate blocks for each op upfront, so we can do jumps easily */
 	opblocks = palloc_array(LLVMBasicBlockRef, state->steps_len);
 	for (int opno = 0; opno < state->steps_len; opno++)
@@ -2432,6 +2466,12 @@ llvm_compile_expr(ExprState *state)
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
 
+			case EEOP_RPR_NAV_SET:
+				/* unreachable: filtered out by the pre-scan above */
+				Assert(false);
index 1adda7c5d84..d2f19584070 100644
@@ -853,6 +856,9 @@ exprCollation(const Node *expr)
@@ -1154,6 +1160,9 @@ exprSetCollation(Node *expr, Oid collation)
@@ -1426,6 +1435,9 @@ exprLocation(const Node *expr)
@@ -2187,6 +2199,16 @@ expression_tree_walker_impl(Node *node,
@@ -3116,6 +3138,17 @@ expression_tree_mutator_impl(Node *node,
index 23b02fb3bc0..e14ff4dc494 100644

index 55283ab4bbe..1355989e66c 100644
@@ -41,6 +42,8 @@ static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+static void check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate);
 
 /*
  * transformRPR
@@ -400,6 +403,10 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	foreach_ptr(TargetEntry, te, defineClause)
 		te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");

@@ -408,3 +415,83 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 	return defineClause;
 }
+
+/*
+ * check_rpr_nav_expr
+ *		Validate a single RPRNavExpr node by walking its arg and offset_arg
+ *		subtrees in a single pass each.  Checks for nested PREV/NEXT, missing
+ *		column references, and non-constant offset expressions.
+ */
+typedef struct
+{
+	bool		has_nav;		/* RPRNavExpr found (nesting) */
+	bool		has_column_ref; /* Var found */
+}			NavCheckResult;
+
+static bool
+nav_check_walker(Node *node, void *context)
+{
+	NavCheckResult *result = (NavCheckResult *) context;
+
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+		result->has_nav = true;
+	if (IsA(node, Var))
+		result->has_column_ref = true;
+
+	return expression_tree_walker(node, nav_check_walker, context);
+}
+
+static void
+check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
+{
+	NavCheckResult result;
+
+	/* Check arg subtree: nesting + column reference in one walk */
+	memset(&result, 0, sizeof(result));
+	(void) nav_check_walker((Node *) nav->arg, &result);
+
+	if (result.has_nav)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("PREV and NEXT cannot be nested"),
+				 parser_errposition(pstate, nav->location)));
+	if (!result.has_column_ref)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("argument of row pattern navigation operation must include at least one column reference"),
+				 parser_errposition(pstate, nav->location)));
+
+	/* Check offset_arg: column ref + volatile in one walk */
+	if (nav->offset_arg != NULL)
+	{
+		memset(&result, 0, sizeof(result));
+		(void) nav_check_walker((Node *) nav->offset_arg, &result);
+
+		if (result.has_column_ref ||
+			contain_volatile_functions((Node *) nav->offset_arg))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("PREV/NEXT offset must be a run-time constant"),
+					 parser_errposition(pstate, nav->location)));
+	}
+}
+
+/*
+ * check_rpr_nav_nesting_walker
+ *		Walk the DEFINE clause expression tree and validate each RPRNavExpr.
+ */
+static bool
+check_rpr_nav_nesting_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+	{
+		check_rpr_nav_expr((RPRNavExpr *) node, (ParseState *) context);
+		/* don't recurse into arg; nesting already checked above */
+		return false;
+	}
+	return expression_tree_walker(node, check_rpr_nav_nesting_walker, context);
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 25bc7dd7779..72f4f263533 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10106,6 +10106,22 @@ get_rule_expr(Node *node, deparse_context *context,
 			get_func_expr((FuncExpr *) node, context, showimplicit);
 			break;
 
+		case T_RPRNavExpr:
+			{
 
 /*
  * prev
 
 /*
  * next
index 55749d3e517..34e4b375a62 100644
@@ -10975,9 +10975,15 @@
 
index aa9b361fa31..fac37c96896 100644
 
+	/* row pattern navigation (RPR PREV/NEXT) */
+	EEOP_RPR_NAV_SET,
+	EEOP_RPR_NAV_RESTORE,
+
 	/* aggregation related nodes */
 	EEOP_AGG_STRICT_DESERIALIZE,
 	EEOP_AGG_DESERIALIZE,
@@ -691,6 +695,16 @@ typedef struct ExprEvalStep
 			SubPlanState *sstate;
 		}			subplan;
 
@@ -898,6 +912,10 @@ extern void ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
 
index 33028c3f10b..30ee769e45d 100644
@@ -2689,10 +2689,12 @@ typedef struct WindowAggState

 	/*
index f5b6b45664a..94723a3b909 100644
 
+/*
+ * RPRNavExpr
+ *
+ * kind:       RPR_NAV_PREV or RPR_NAV_NEXT
+	RPR_NAV_PREV,
+	RPR_NAV_NEXT,
index e72171050c7..d586e17e0a1 100644
@@ -1018,6 +1018,555 @@ WINDOW w AS (
   5 |     0.1 |     0
 (5 rows)
 
+--
+-- Non-constant offset: subquery as offset
+    DEFINE A AS PREV(price, (SELECT 1)) > 0
+ERROR:  cannot use subquery in DEFINE expression
+LINE 7:     DEFINE A AS PREV(price, (SELECT 1)) > 0
+-- First arg: subquery (caught by DEFINE-level subquery restriction)
+    DEFINE A AS PREV(price + (SELECT 1)) > 0
+ERROR:  cannot use subquery in DEFINE expression
+LINE 7:     DEFINE A AS PREV(price + (SELECT 1)) > 0
+-- First arg: volatile function is allowed (evaluated on target row)
+    DEFINE A AS PREV(price + random() * 0) >= 0
+ company1 | 07-02-2023 |   200 |         200 |        130 |     9
+ company2 | 07-02-2023 |  2000 |        2000 |       1300 |     9
@@ -1479,7 +2028,7 @@ count(*) OVER w
 (14 rows)

 

index 95794d409e1..504476a2b02 100644
@@ -440,6 +440,326 @@ WINDOW w AS (
 
+--
+-- Non-constant offset: subquery as offset
+    DEFINE A AS PREV(price, (SELECT 1)) > 0
+-- First arg: subquery (caught by DEFINE-level subquery restriction)
+    DEFINE A AS PREV(price + (SELECT 1)) > 0
+-- First arg: volatile function is allowed (evaluated on target row)
+    DEFINE A AS PREV(price + random() * 0) >= 0
@@ -671,7 +991,7 @@ count(*) OVER w
 );

 

 

 

 
 --
 -- Planner optimization: find_window_run_conditions must not push down
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 71ea925d77a..24856cf8478 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1799,7 +1799,6 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
-NavigationInfo
 NestLoop
 NestLoopParam
 NestLoopState
@@ -2476,6 +2475,8 @@ QuerySource
 QueueBackendStatus
 QueuePosition
 QuitSignalReason
+RPRNavExpr
+RPRNavKind
 RBTNode
 RBTOrderControl
 RBTree
-- 
2.50.1 (Apple Git-155)

---

From: Henson Choi <[email-masked]>
Date: Sun, 29 Mar 2026 23:06:11 +0900

Hi Tatsuo,

Separately from the PREV/NEXT patches, I have been analyzing
how FIRST and LAST navigation would interact with the current
NFA engine -- particularly context absorption and DEFINE
evaluation sharing. This is not a proposal to implement them
now; rather, I wanted to document the design constraints
while the analysis is fresh, so we have a reference when we
are ready to add them.

Design Note: FIRST/LAST Navigation for RPR
===========================================

Scope: This note covers only patterns without quantifiers, where
each pattern variable consumes exactly one row.

1. Navigation Function Classification
--------------------------------------

The SQL standard (ISO/IEC 19075-5:2021) defines four navigation
functions in two categories:

PREV/NEXT -- physical navigation:
  - Scope: entire row pattern partition
  - Offset: physical (row position within partition)
  - Base row: the row denoted by the variable under running
    semantics (in our no-qualifier scope: current eval row)
  - Default offset: 1
  - Can reach rows outside the match (standard 5.6.2)
  - Always RUNNING semantics only

FIRST/LAST -- logical navigation:
  - Standard scope: rows mapped to a specific variable
  - Our scope (no quantifier): match_start to currentpos range
  - Offset: logical (in standard), but equivalent to physical
    when each variable maps to at most one row
  - Base: match_start (FIRST) or currentpos (LAST)
  - Default offset: 0 (unlike PREV/NEXT which default to 1)
  - RUNNING semantics in DEFINE; FINAL available in MEASURES

FIRST/LAST results depend on the match start position
(match_start). Each current row in window RPR triggers an
independent match attempt with its own match_start. FIRST
always depends on match_start. LAST(price) depends only on
currentpos and is match_start-independent, but LAST(price, N)
depends on match_start because the NULL boundary check
(currentpos - N < match_start) varies with match_start.

Example (PATTERN (A B C), match_start = 3):

    FIRST(price)    = R3.price   (match_start row)
    FIRST(price, 1) = R4.price   (match_start + 1)
    LAST(price)     = R5.price   (currentpos row)
    LAST(price, 1)  = R4.price   (currentpos - 1)
    LAST(price, 3)  = NULL       (before match_start)

    PREV(price, 2)  = R3.price   (pos - 2, physical)
    NEXT(price, 2)  = R7.price   (pos + 2, beyond match)

PREV/NEXT can reach any row in the partition regardless of
match boundaries. FIRST/LAST are valid only within the
match_start to currentpos range.

2. match_start and NFA Contexts
-------------------------------

Each match_start defines one match attempt, which corresponds
to one NFA context. Different contexts have different
match_start values. Context absorption merges contexts when
one context's pattern states are covered by another's. If the
merged contexts have different match_start values, FIRST/LAST
would produce incorrect results because the surviving context's
match_start would be used for both.

3. Compound Navigation (PREV/NEXT wrapping FIRST/LAST)
------------------------------------------------------

The standard (5.6.4) permits nesting FIRST/LAST inside
PREV/NEXT. Using the standard's notation with variable
qualifiers (which our no-quantifier scope does not yet
require):

    PREV(LAST(A.Price + A.Tax, 1), 3)

Evaluation proceeds in three steps:
  1. Inner FIRST/LAST: determines target row position only
     (does not evaluate the expression)
  2. Outer PREV/NEXT: applies physical offset from that row
  3. Expression evaluation: at the final destination row only

The reverse nesting (FIRST/LAST wrapping PREV/NEXT) and
same-kind nesting (PREV inside PREV, etc.) are prohibited.

Since the inner function only determines a position, the
compound operation can be flattened into a single RPRNavExpr
node internally, requiring only one slot swap.

4. Context Absorption Rules
----------------------------

Absorption safety depends on whether the navigation function
depends on match_start:

  DEFINE content                  | match_start dep. | absorption
  --------------------------------|------------------|----------
  No navigation                   | none             | safe
  PREV/NEXT only                  | none             | safe
  LAST (no offset)                | none             | safe
  LAST (with offset)              | boundary check   | unsafe
  FIRST (any)                     | direct           | unsafe
  Compound (inner FIRST)          | direct           | unsafe
  Compound (inner LAST, no off.)  | none             | safe
  Compound (inner LAST, with off.)| boundary check   | unsafe

The criterion: if the expression depends on match_start in
any way, absorption is unsafe. Comparing match_start values
at runtime is impractical, so we treat any dependency as
unconditionally unsafe.

5. DEFINE Clause Evaluation Rules
---------------------------------

Currently, nfa_evaluate_row(pos) evaluates all DEFINE
variables once per row and shares the results across all NFA
contexts.

  DEFINE content                  | evaluation
  --------------------------------|--------------------------
  No navigation                   | shared (once per row)
  PREV/NEXT only                  | shared (once per row)
  LAST (no offset)                | shared (once per row)
  LAST (with offset)              | per-context
  FIRST (any)                     | per-context
  Compound (inner FIRST)          | per-context
  Compound (inner LAST, no off.)  | shared (once per row)
  Compound (inner LAST, with off.)| per-context

6. Slot Swap Elision
--------------------

When the final target position resolves to currentpos, the
slot swap can be elided. This applies to:
  - PREV(price, 0), NEXT(price, 0): offset 0
  - LAST(price), LAST(price, 0): always currentpos
  - Compound cases where offsets cancel, e.g.,
    NEXT(LAST(price, 1), 1)

The PREV/NEXT infrastructure (RPRNavExpr, EEOP_RPR_NAV_SET/
RESTORE, nav_winobj) can be reused directly for FIRST/LAST --
it would mainly require adding two RPRNavKind values and
match_start arithmetic in the ExecEvalRPRNavSet switch.
No variable qualifiers, match history tracking, or CLASSIFIER
infrastructure would be needed for this no-quantifier scope.

I do think CLASSIFIER, MEASURES, and match history tracking
should stay out of the initial patch set -- they add
significant complexity to NFA contexts and affect absorption
in ways that are hard to validate incrementally. Variable
qualifiers and quantifier support for navigation functions
are also tied to match history infrastructure, so they should
be deferred as well. Better to get the current feature set
stable first.

With PREV/NEXT now resolved (0006), the next major milestone
would be match history infrastructure (needed for CLASSIFIER,
MEASURES, variable qualifiers, and quantifier-aware
navigation). Before that larger step, there are a few
remaining items that can be done within the current
architecture:

Optimization:
  - PREFIX pattern absorption (e.g., A (B+) C) [1][2]
  - Fixed-length iteration group absorption
    (e.g., (A B{2})+ ) [3]

Feature:
  - FIRST/LAST navigation (this note)
  - LLVM JIT proper support for slot swap (0006 uses
    interpreter fallback) [4][5]

[1]
https://www.postgresql.org/message-id/CAAAe_zAEg7sVM%3DWDwXMyE-odGmQyXSVi5ZzWgye6SupSjdMKpg%40mail.gmail.com
[2]
https://www.postgresql.org/message-id/CAAAe_zDq7R8CaDfmh8C%2BH3_639Y5LtJD%2B2Z%3D1txDt%3DvaOr90rQ%40mail.gmail.com
[3]
https://www.postgresql.org/message-id/CAAAe_zBY%2BEqk_DQpS0cy1Eb67H9v92tyaf%2BU%2BSKKuLGUs_z%2BEA%40mail.gmail.com
[4]
https://www.postgresql.org/message-id/CAAAe_zBbrnx2fjK2s%2BJgx6TSOdnKAPawXbHeX49WqmX9ji%2BHdg%40mail.gmail.com
[5]
https://www.postgresql.org/message-id/CAAAe_zBCF3dwSjStmG0kJqw_y1z8QD73Rf1G58QTKEvd9tScwA%40mail.gmail.com

The question is how much of this to complete before the
current patches are committed. The patch set is already
substantial, and the next CommitFest is in July. What do
you think -- should we try to include some of these items
in the current round, or focus on getting what we have
committed first? Also, do you see any blockers that might
prevent the current patch set from being committed?

Best regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 12:51:05 +0900

Hi Tatsuo,

Thank you for the review and the attached patch for 0005.
I appreciate you taking the time to look at each patch
carefully.

Attached are 8 incremental patches on top of v46 (replacing
the previous 6-patch set). 0001-0003 are unchanged. 0004 and
0005 incorporate your feedback below. 0006 is a new planner
fix. Previous 0006 becomes 0007. 0008 is new.

> Probably we want to modify the comment above since it implies an
> in-place modification? What about something like this? (Modifies ->
> Replace)
>
>         /*
>          * Replace an expression tree in each DEFINE clause so that all Var
>          * nodes's varno refers to OUTER_VAR.
>          */
>

Good point, thank you. Applied in the updated 0004.

> I think instead we can set a mark at frameheadpos when seek type is
> SEEK_TAIL and RPR is enabled. See attached patch.
>

Thank you for the patch. Your approach is cleaner -- setting
the mark at frameheadpos is more direct than suppressing
advancement. I've adopted it in the updated 0005.

> Excellent! I will take a look at it. (it will take for a while).
>

No rush at all. In the meantime, while testing the PREV/NEXT
patch in various query patterns, I found a planner issue.
I've also added JIT support. Here is a summary of the new
patches:

  0006: Prevent removal of RPR window functions in unused
        subquery outputs

        Wrapping an RPR window query in a subquery with an
        outer aggregate causes a crash:

          SELECT count(*) FROM (
            SELECT count(*) OVER w FROM ...
            WINDOW w AS (... DEFINE A AS i > PREV(i))
          ) t;

        remove_unused_subquery_outputs() replaces unused subquery
        target entries with NULL constants. When an RPR window
        function's result is not referenced by the outer query,
        this replacement eliminates all active window functions
        for the WindowClause, causing the planner to omit the
        WindowAgg node. DEFINE clause expressions containing
        RPRNavExpr (PREV/NEXT) then lose their execution context,
        leading to a crash.

        The fix skips the NULL replacement for window functions
        whose WindowClause has a defineClause. Even when the
        window function result is unused, RPR pattern matching
        (frame reduction) must still execute -- the WindowAgg
        node must be preserved.

        An alternative would be to let the planner remove the
        window function but teach it to still generate the
        WindowAgg node when defineClause is present, even with
        no active window functions. That would be a more
        targeted optimization but requires deeper planner
        changes. Do you think the current approach is
        sufficient, or would you prefer a different strategy?

  0007: Implement 1-slot PREV/NEXT navigation for RPR
        (unchanged from previous 0006)

> I am not an expert of JIT, but for me, it sounds reasonable.  We can
> enhance it later on.
>

  0008: Add JIT compilation support for RPR PREV/NEXT navigation

        EEOP_OUTER_VAR normally uses slot pointers cached in the
        entry block, but the mid-expression slot swap in
        NAV_SET/RESTORE invalidates them. To avoid penalizing
        existing expressions, the reload path is only generated
        when RPR navigation opcodes are present in the
        expression (compile-time decision). Expressions without
        PREV/NEXT produce identical machine code as before.
        NAV_SET/RESTORE use the standard build_EvalXFunc pattern.

        A 100K-row PREV/NEXT test case that runs under
        jit_above_cost=0 is included in rpr.sql.

I would appreciate your thoughts on these when you have time.

Best regards,
Henson
From b26e6ee5e1335f1e0f976f6a6531eeed019ec835 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 24 Mar 2026 19:04:19 +0900
Subject: [PATCH 1/8] Remove unused regex/regex.h include from nodeWindowAgg.c

---

From cfdb656b416c67c9b9a9a33cc6e5955a0c5781e8 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:25:40 +0900
Subject: [PATCH 2/8] Add CHECK_FOR_INTERRUPTS() to nfa_add_state_unique() for
 state explosion patterns

---

From 987bfaa4180af27a194427566d5d090c44ead894 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 11:03:39 +0900
Subject: [PATCH 3/8] Add CHECK_FOR_INTERRUPTS() to nfa_try_absorb_context()
 loop

---

From 038bad56be0ed38734490f8853eefdb0e71ad75f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:20:05 +0900
Subject: [PATCH 4/8] Fix in-place modification of defineClause TargetEntry in
 setrefs.c

---
 src/backend/optimizer/plan/setrefs.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 69cd1861e9b..813a326bd78 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2633,7 +2633,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 					   NUM_EXEC_QUAL(plan));
 
 	/*
-	 * Modifies an expression tree in each DEFINE clause so that all Var
+	 * Replace an expression tree in each DEFINE clause so that all Var
 	 * nodes's varno refers to OUTER_VAR.
 	 */
 	if (IsA(plan, WindowAgg))
@@ -2646,6 +2646,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 			{
 				TargetEntry *tle = (TargetEntry *) lfirst(l);

From 876c0b8fb52c5bde2e1a0c3e9dd9d1ac50396496 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:37:50 +0900
Subject: [PATCH 5/8] Fix mark handling for last_value() under RPR

Enable mark advancement in window_last_value() for
better tuplestore memory usage in non-RPR cases, while
adding a guard in WinGetFuncArgInFrame to suppress it
for RPR SEEK_TAIL to prevent position invalidation
from reduced frame shifts.
---
 src/backend/executor/nodeWindowAgg.c | 10 ++++++++++
 src/backend/utils/adt/windowfuncs.c  |  2 +-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 185d7a0d5ae..aed7cbef99a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4932,7 +4932,17 @@ WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
 	if (isout)
 		*isout = false;
 	if (set_mark)
+	{
+		/*
+		 * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the
+		 * mark position unconditionally to frameheadpos. In this case the
+		 * frame always starts at CURRENT_ROW and never goes back, thus
+		 * setting the mark at the position is safe.
+		 */
+		if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
+			mark_pos = winstate->frameheadpos;
 		WinSetMarkPosition(winobj, mark_pos);
+	}
 	return 0;
 
 out_of_frame:

From 2d48907d52e3af8738b09df69d3c016b6224d619 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 11:55:02 +0900
Subject: [PATCH 6/8] Prevent removal of RPR window functions in unused
 subquery outputs

remove_unused_subquery_outputs() replaces unused subquery target
entries with NULL constants. When an RPR window function's result
is not referenced by the outer query, this replacement eliminates
all active window functions for the WindowClause, causing the
planner to omit the WindowAgg node. DEFINE clause expressions
containing RPRNavExpr (PREV/NEXT) then lose their execution
context, leading to an Assert failure in execExpr.c.

Skip the NULL replacement for window functions whose WindowClause
has a defineClause, so the WindowAgg node is preserved and RPR
pattern matching executes correctly.
---
 src/backend/optimizer/path/allpaths.c | 28 +++++++++++++++++++++++
 src/test/regress/expected/rpr.out     | 33 +++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql          | 26 +++++++++++++++++++++
 3 files changed, 87 insertions(+)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f42a2bae14a..9d3af43a72e 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4750,6 +4750,34 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		if (contain_volatile_functions(texpr))
 			continue;
 
+		/*
+		 * If it's a window function referencing a window clause with RPR (Row
+		 * Pattern Recognition), don't remove it.  Even when the window
+		 * function result is unused by the outer query, the RPR pattern
+		 * matching (frame reduction via DEFINE/PATTERN) must still execute.
+		 * Replacing this with NULL would leave no active window functions for
+		 * the WindowClause, causing the planner to omit the WindowAgg node
+		 * entirely.
+		 */
+		if (IsA(texpr, WindowFunc))
+		{
+			WindowFunc *wfunc = (WindowFunc *) texpr;
+			ListCell   *wlc;
+
+			foreach(wlc, subquery->windowClause)
+			{
+				WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+				if (wc->winref == wfunc->winref &&
+					wc->defineClause != NIL)
+				{
+					break;
+				}
+			}
+			if (wlc != NULL)
+				continue;
+		}
+
 		/*
 		 * OK, we don't need it.  Replace the expression with a NULL constant.
 		 * Preserve the exposed type of the expression, in case something
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index e72171050c7..54a6857bdb8 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1604,6 +1604,39 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
            0 |      99998 |     99999
 (1 row)
 
+--
+-- Subquery wrapping: RPR window inside outer aggregate.
+-- Tests that WindowAgg is not removed by remove_unused_subquery_outputs()
+-- when DEFINE clause contains PREV/NEXT.
+--
+-- PREV in DEFINE + outer aggregate
+SELECT count(*) FROM (
+  SELECT count(*) OVER w FROM generate_series(1,10) i
+  WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS i > PREV(i)
+  )
+) t;
+ count 
+-------
+    10
+(1 row)
+
+-- DEFINE without PREV + outer aggregate (WindowAgg must still be preserved)
+SELECT count(*), sum(c) FROM (
+  SELECT count(*) OVER w AS c FROM generate_series(1,10) i
+  WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS TRUE
+  )
+) t;
+ count | sum 
+-------+-----
+    10 |  10
+(1 row)
+
 --
 -- IGNORE NULLS
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 95794d409e1..996135634fd 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -764,6 +764,32 @@ result AS (
 -- Should match: A (33333 rows) + B (33333 rows) + C (33333 rows) = 99999 rows
 SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
 
+--
+-- Subquery wrapping: RPR window inside outer aggregate.
+-- Tests that WindowAgg is not removed by remove_unused_subquery_outputs()
+-- when DEFINE clause contains PREV/NEXT.
+--
+
+-- PREV in DEFINE + outer aggregate
+SELECT count(*) FROM (
+  SELECT count(*) OVER w FROM generate_series(1,10) i
+  WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS i > PREV(i)
+  )
+) t;
+
+-- DEFINE without PREV + outer aggregate (WindowAgg must still be preserved)
+SELECT count(*), sum(c) FROM (
+  SELECT count(*) OVER w AS c FROM generate_series(1,10) i
+  WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS TRUE
+  )
+) t;
+
 --
 -- IGNORE NULLS
 --
-- 
2.50.1 (Apple Git-155)

From dfdfff46d47f2f6b0657e89fda2e4aa38a186d84 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 23:52:42 +0900
Subject: [PATCH 7/8] Implement 1-slot PREV/NEXT navigation for RPR

 
@@ -318,12 +323,17 @@
         <indexterm>
          <primary>next</primary>
         </indexterm>

+		case T_RPRNavExpr:
+			{
+				/*
 
+#include "common/int.h"

index aed7cbef99a..0e4effea5c2 100644
 
-/*

 

 

 
 
 	/*
 
+	/*

 /*
 
-static bool
 
-	if (node == NULL)
-		return false;
+	/*

-/*

-	/*

 

 
+	/*
 
+			case EEOP_RPR_NAV_SET:

 
 /*
  * transformRPR

 
 	return defineClause;
 }
+
+/*
+ * check_rpr_nav_expr
 
+		case T_RPRNavExpr:
+			{
 
 /*
  * prev
 
 /*
  * next

 

 
 	/*
 
+/*
+ * RPRNavExpr
+ *
index 54a6857bdb8..b005c7e07a1 100644
 
+--

 

index 996135634fd..49dac932d96 100644
 
+--

 

 

 

 

From ab356c4f081417ef69517e6ef8449db143ef29e1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 10:54:30 +0900
Subject: [PATCH 8/8] Add JIT compilation support for RPR PREV/NEXT navigation

---
 src/backend/jit/llvm/llvmjit_expr.c | 72 +++++++++++++++++++++--------
 src/test/regress/expected/rpr.out   | 31 +++++++++++++
 src/test/regress/sql/rpr.sql        | 27 +++++++++++
 3 files changed, 111 insertions(+), 19 deletions(-)

diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index d158e37e7b5..4901b2a7ff4 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -127,6 +127,9 @@ llvm_compile_expr(ExprState *state)
 	LLVMValueRef v_aggvalues;
 	LLVMValueRef v_aggnulls;
 
+	/* RPR navigation: when true, EEOP_OUTER_VAR reloads from econtext */
+	bool		has_rpr_nav;
+
 	instr_time	starttime;
 	instr_time	deform_starttime;
 	instr_time	endtime;
@@ -300,19 +303,16 @@ llvm_compile_expr(ExprState *state)
 	 * RPR navigation opcodes (PREV/NEXT) swap ecxt_outertuple to a different
 	 * row mid-expression.  The JIT code loads v_outervalues and v_outernulls
 	 * once in the entry block and reuses them for all EEOP_OUTER_VAR steps.
-	 * After a slot swap, these pointers become stale because the new slot has
-	 * its own tts_values/tts_isnull arrays.  Fall back to the interpreter for
-	 * these expressions.
+	 * After a slot swap, these cached pointers become stale because the new
+	 * slot has its own tts_values/tts_isnull arrays.
 	 *
-	 * XXX To JIT-compile these expressions properly, the NAV_SET and
-	 * NAV_RESTORE handlers would need to reload the tts_values and tts_isnull
-	 * pointers from the new slot.  However, LLVM uses SSA (Static Single
-	 * Assignment) form where each value is defined exactly once.  When
-	 * different basic blocks produce different values for the same pointer,
-	 * LLVM requires PHI nodes at the merge point to select the correct one.
-	 * Without that plumbing, OUTER_VAR steps after a slot swap would read
-	 * from the wrong pointer.
+	 * When RPR navigation opcodes are present, EEOP_OUTER_VAR reloads the
+	 * slot pointer from econtext->ecxt_outertuple on every access instead of
+	 * using the cached entry-block values.  This avoids the SSA/PHI
+	 * complexity while keeping the rest of the expression JIT-compiled.
+	 * Expressions without RPR navigation use the cached values as before.
 	 */
+	has_rpr_nav = false;
 	if (parent && IsA(parent, WindowAggState) &&
 		((WindowAgg *) parent->plan)->rpPattern != NULL)
 	{
@@ -323,9 +323,8 @@ llvm_compile_expr(ExprState *state)
 			if (opcode == EEOP_RPR_NAV_SET ||
 				opcode == EEOP_RPR_NAV_RESTORE)
 			{
-				LLVMDeleteFunction(eval_fn);
-				LLVMDisposeBuilder(b);
-				return false;
+				has_rpr_nav = true;
+				break;
 			}
 		}
 	}
@@ -492,8 +491,37 @@ llvm_compile_expr(ExprState *state)
 					}
 					else if (opcode == EEOP_OUTER_VAR)
 					{
-						v_values = v_outervalues;
-						v_nulls = v_outernulls;
+						if (has_rpr_nav)
+						{
+							/*
+							 * RPR navigation swaps ecxt_outertuple
+							 * mid-expression.  Reload slot pointer from
+							 * econtext on every access so we read from the
+							 * current (possibly swapped) slot.
+							 */
+							LLVMValueRef v_tmpslot;
+
+							v_tmpslot = l_load_struct_gep(b,
+														  StructExprContext,
+														  v_econtext,
+														  FIELDNO_EXPRCONTEXT_OUTERTUPLE,
+														  "v_outerslot_reload");
+							v_values = l_load_struct_gep(b,
+														 StructTupleTableSlot,
+														 v_tmpslot,
+														 FIELDNO_TUPLETABLESLOT_VALUES,
+														 "v_outervalues_reload");
+							v_nulls = l_load_struct_gep(b,
+														StructTupleTableSlot,
+														v_tmpslot,
+														FIELDNO_TUPLETABLESLOT_ISNULL,
+														"v_outernulls_reload");
+						}
+						else
+						{
+							v_values = v_outervalues;
+							v_nulls = v_outernulls;
+						}
 					}
 					else if (opcode == EEOP_SCAN_VAR)
 					{
@@ -2467,10 +2495,16 @@ llvm_compile_expr(ExprState *state)
 				break;
 
 			case EEOP_RPR_NAV_SET:
+				build_EvalXFunc(b, mod, "ExecEvalRPRNavSet",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_RPR_NAV_RESTORE:
-				/* unreachable: filtered out by the pre-scan above */
-				Assert(false);
-				return false;
+				build_EvalXFunc(b, mod, "ExecEvalRPRNavRestore",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
 
 			case EEOP_AGG_STRICT_DESERIALIZE:
 			case EEOP_AGG_DESERIALIZE:
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index b005c7e07a1..541e6bf8f98 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2153,6 +2153,37 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
            0 |      99998 |     99999
 (1 row)
 
+-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
+-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
+-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit_above_cost = 0;
+WITH data AS (
+ SELECT i, abs(50000 - i) AS price
+ FROM generate_series(1, 100000) i
+),
+result AS (
+ SELECT i, price,
+        count(*) OVER w AS match_len,
+        first_value(price) OVER w AS match_first
+ FROM data
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN (DOWN+ UP+)
+  DEFINE
+   DOWN AS price < PREV(price),
+   UP AS price > PREV(price)
+ )
+)
+SELECT count(*) AS matched_rows, max(match_len) AS longest_match
+FROM result WHERE match_len > 0;
+ matched_rows | longest_match 
+--------------+---------------
+            1 |         99999
+(1 row)
+
+RESET jit_above_cost;
 --
 -- Subquery wrapping: RPR window inside outer aggregate.
 -- Tests that WindowAgg is not removed by remove_unused_subquery_outputs()
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 49dac932d96..cc8daae481e 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1084,6 +1084,33 @@ result AS (
 -- Should match: A (33333 rows) + B (33333 rows) + C (33333 rows) = 99999 rows
 SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
 
+-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
+-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
+-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit_above_cost = 0;
+WITH data AS (
+ SELECT i, abs(50000 - i) AS price
+ FROM generate_series(1, 100000) i
+),
+result AS (
+ SELECT i, price,
+        count(*) OVER w AS match_len,
+        first_value(price) OVER w AS match_first
+ FROM data
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN (DOWN+ UP+)
+  DEFINE
+   DOWN AS price < PREV(price),
+   UP AS price > PREV(price)
+ )
+)
+SELECT count(*) AS matched_rows, max(match_len) AS longest_match
+FROM result WHERE match_len > 0;
+RESET jit_above_cost;
+
 --
 -- Subquery wrapping: RPR window inside outer aggregate.
 -- Tests that WindowAgg is not removed by remove_unused_subquery_outputs()
-- 
2.50.1 (Apple Git-155)

---

From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 16:31:13 +0900

Hi hackers,

I'd like to propose an extension to the context absorption
optimization for Row Pattern Recognition in the window clause:
fixed-length iteration group absorption. This builds on the
two-flag absorption design [1] and three-phase execution
framework [2], and is a sibling optimization to PREFIX pattern
absorption [3].

The existing framework requires group children to have exactly
{1,1} quantifiers. This relaxes the constraint to accept any
fixed-length quantifier (min == max), allowing patterns like
(A B{2})+ and (A (B{2} C){4})+ to benefit from absorption
with no runtime changes.

PREFIX absorption [3] handles fixed-length elements before
the unbounded repetition; this handles fixed-length elements
inside the unbounded group.

[1] Context absorption design - two-flag approach
https://www.postgresql.org/message-id/CAAAe_zCmjDRf9u19xRRbtzq%2B-7ujqadZk0izz0mkef2j%2BWpBBA%40mail.gmail.com
[2] Three-phase absorption framework (Match -> Absorb -> Advance)
https://www.postgresql.org/message-id/CAAAe_zAGLOF_qporKahnL8ajSu_eXZYsJN2M1cEeF5n5GWSNdQ%40mail.gmail.com
[3] PREFIX pattern absorption
https://www.postgresql.org/message-id/CAAAe_zDq7R8CaDfmh8C%2BH3_639Y5LtJD%2B2Z%3D1txDt%3DvaOr90rQ%40mail.gmail.com
[4] FIRST/LAST navigation design note
https://www.postgresql.org/message-id/CAAAe_zCUrKGBgZdaazdO_v9QWHsS_1DXuP%3DrLeNhO3iwsHwwbg%40mail.gmail.com

Design Note: Fixed-Length Iteration Group Absorption
=====================================================

1. Problem Definition
=====================

Context absorption currently requires that children of an unbounded
group have exactly {1,1} quantifiers. This means only simple patterns
like (A B)+ qualify for absorption.

Patterns like (A B{2})+ or (A (B{2} C){4})+ are excluded even though
each iteration consumes a fixed number of rows.

1.2 The Fixed-Length Constraint

The existing {1,1} check is overly restrictive. The actual requirement
for absorption is that each iteration of the group consumes a fixed
number of rows, so that the count-dominance property holds: an older
context always has a repeat count >= any newer context at the same
element.

Any group where all children have min == max (recursively) satisfies
this property, regardless of nesting depth or quantifier values.

The Match -> Absorb -> Advance model [2] is preserved as-is. This
optimization only changes compile-time eligibility analysis in the
two-flag framework [1]; the Absorb phase logic is unchanged.

2.2 Prerequisites

1. Greedy quantifier -- the outer group must be greedy.
2. SKIP TO PAST LAST ROW -- prior matches suppress subsequent
   start points.
3. UNBOUNDED FOLLOWING -- all contexts see the same future rows.
4. Shared DEFINE evaluation -- all contexts produce the same
   DEFINE result for a given row [4].

The existing dominance condition is unchanged:

    C_old and C_new are in the absorbable region of the same
    element, and C_old.counts >= C_new.counts.

The only change is which patterns are recognized as having an
absorbable region. The runtime count comparison works identically
regardless of the step size per iteration.

3. Correctness Argument
========================

A fixed-length quantifier (min == max) is semantically equivalent
to repeating the element that many times. Therefore any pattern
with fixed-length children can be conceptually unrolled to {1,1}
elements:

  (A B{2})+           -> (A B B)+
  (A (B{2} C){4})+    -> (A B B C B B C B B C B B C)+
  ((A{2} B{3}){2})+   -> (A A B B B A A B B B)+

The unrolled form contains only {1,1} VARs inside the unbounded
group, which is exactly the existing Case 2 that is already proven
correct for absorption.

This optimization recognizes fixed-length groups at compile time
without actually unrolling them. The runtime behavior is identical
to the unrolled form: each iteration consumes a fixed number of
rows, count-dominance holds, and absorption proceeds unchanged.

4. Compile-Time Analysis

4.1 Change to isUnboundedStart()

Currently, Case 2 in isUnboundedStart() requires all children of
an unbounded group to be simple {1,1} VARs. The relaxation:

- VAR children: accept min == max (any value)
- BEGIN children (nested subgroups): recursively verify that all
  descendants have min == max, including the subgroup's own END
  quantifier
- ALT children: reject (unchanged)

The check follows the existing depth-based traversal of the flat
element array.

4.2 Flat Array Example

Pattern: (A (B{2} C){4})+

  [0] BEGIN depth=0  min=1 max=INF   <- outer group
  [1] A     depth=1  min=1 max=1     <- 1==1 OK
  [2] BEGIN depth=1  min=4 max=4     <- subgroup
  [3] B     depth=2  min=2 max=2     <- 2==2 OK
  [4] C     depth=2  min=1 max=1     <- 1==1 OK
  [5] END   depth=1  min=4 max=4     <- 4==4 OK
  [6] END   depth=0  min=1 max=INF   <- unbounded, greedy

Verification traverses [1]-[5] confirming min==max at every
level. [6] is the outer END: unbounded and greedy, so absorption
applies.

4.3 Runtime: No Changes

The runtime absorption logic (nfa_try_absorb_context,
nfa_states_covered) and flag assignment (RPR_ELEM_ABSORBABLE,
RPR_ELEM_ABSORBABLE_BRANCH) require no changes.

5. Worked Example
==================

Pattern: (A B{2})+
Step size = 1 + 2 = 3.

Data: rows match in repeating A, B, B cycle.
Contexts that fail A at B-rows die immediately (omitted).

Row  Data  C_old             C_new            Active
r0   A     A                 -                1
r1   B     B(cnt=1)          -                1
r2   B     B(cnt=2)->END     -                1
           count=1, loops
r3   A     A                 A                2
r4   B     B(cnt=1)          B(cnt=1)         2
r5   B     B(cnt=2)->END     B(cnt=2)->END    2
           count=2            count=1
           absorb: 2 >= 1 -> C_new absorbed
           C_old loops                        1
r6   A     A                 A                2
r7   B     B(cnt=1)          B(cnt=1)         2
r8   B     B(cnt=2)->END     B(cnt=2)->END    2
           count=3            count=1
           absorb: 3 >= 1 -> C_new absorbed
           C_old loops                        1

Absorption occurs at END (the ABSORBABLE judgment point),
where both contexts synchronize after completing one full
iteration. The older context has a strictly higher group
count and absorbs the newer one.

Between END points, both contexts progress in lockstep
at the same pattern positions. Maximum concurrent contexts
during this phase = 2. Overall complexity remains O(n).

6. Non-Qualifying Patterns
===========================

Patterns where any child has min != max do NOT qualify:

  (A B+ C)+    -- B+ has min=1, max=INF -> NOT fixed
  (A B{2,5})+  -- B has min=2, max=5 -> NOT fixed
  (A B?)+      -- B? has min=0, max=1 -> NOT fixed

These patterns have variable step sizes per iteration, so
count-dominance is not guaranteed. They remain non-absorbable
under this optimization.

---

From: Henson Choi <[email-masked]>
Date: Sun, 5 Apr 2026 21:50:53 +0900

Hi Tatsuo,

Attached are 15 incremental patches on top of v46 (replacing
the previous 8-patch set). Here is a summary of the changes
from the previous version:

  0001-0005: Unchanged from the previous set.

  0006: Fix DEFINE expression handling in RPR window
        planning (revised)

        Integration tests in 0007 revealed two crashes:
        (1) subquery wrapping with outer aggregate causes
        WindowAgg removal when RPR window function output
        is unused, (2) RPR and non-RPR windows coexisting
        causes SIGSEGV from RPRNavExpr propagating to the
        wrong WindowAgg. This version extends the fix to
        extract only Var nodes via pull_var_clause()
        instead of adding the whole DEFINE expression to
        the targetlist. The allpaths.c guard is extended
        to also preserve columns referenced by DEFINE
        clauses.

  0007: Add RPR planner integration tests (new)

        Planner optimization interactions with RPR windows
        have been revealing crashes, so this is a dedicated
        test file: window dedup, subquery flattening,
        DEFINE propagation, LATERAL, recursive CTE,
        etc. Will be expanded further.

  0008: Replace reduced frame map with single match result
        (new)

        RPR processes rows sequentially and maintains at
        most one active match per start position. This
        replaces the per-row boolean frame map array with
        four scalar fields (valid, matched, start, length),
        simplifying nodeWindowAgg.c.

  0009: Add fixed-length group absorption for RPR (new)

        Extends context absorption to accept fixed-length
        quantifiers (min == max) inside unbounded groups.
        The NFA compiler optimizes (A B B)+ into
        (A B{2})+, which previously lost absorption
        eligibility because only {1,1} children
        qualified. Now (A B{2})+ and ((A{2} B{3}){2})+
        also qualify with no runtime changes. Also fixes
        two NFA bugs: (1) incorrect absorption at
        non-judgment points -- added ABSORBABLE check
        in nfa_states_covered and fixed isAbsorbable
        propagation in advance exit paths, (2) VAR
        visited bitmap moved to nfa_add_state_unique
        to prevent false cycle detection on new-iteration
        loop-back caused by generalized END chain
        traversal. Design note posted separately [1].

  0010: Rename rpr_explain test views to descriptive
        names (new)

        Replaces sequential view names (rpr_ev83, rpr_ev84)
        with descriptive names (rpr_ev_nav_prev1, etc.) for
        maintainability when adding new tests.

  0011: Implement 1-slot PREV/NEXT navigation for RPR
        (was 0007, rebased)

        Context changes only: optimizer.h include moved to
        0006, test view names updated by 0010.

  0012: Add JIT compilation support for RPR PREV/NEXT
        (was 0008, unchanged)

        Note: master recently disabled JIT by default
        [3]. A quick benchmark with RPR did not show
        measurable improvement from JIT. This patch is
        retained for correctness (the new opcodes must
        be handled when JIT is enabled), but the
        practical benefit may be limited.

  0013: Add tuplestore trim optimization for RPR PREV
        navigation (new)

        The planner extracts max_offset from all PREV
        calls in DEFINE clauses and sets mark = currentpos
        - max_offset at runtime, so only max_offset rows
        are retained. Constant expressions are folded by
        the planner; non-constant offsets (parameters,
        etc.) are evaluated by the executor at init time
        via RPR_NAV_OFFSET_NEEDS_EVAL.

  0014: Update RPR code comments to reflect 1-slot
        navigation model (new)

        Updates stale comments in execRPR.c and
        parse_rpr.c that still referenced the old 3-slot
        design.

  0015: Implement FIRST/LAST navigation for RPR
        (work in progress)

        Adds FIRST(column) and LAST(column) navigation
        functions per SQL standard. Parser, planner,
        executor, and tests are included. See the design
        note [2] for the interaction with context
        absorption.

        Not yet done: compound navigation (e.g.
        PREV(FIRST(col))), tests for FIRST/LAST offset
        variants, tuplestore mark handling for FIRST,
        and additional edge-case tests.

In the FIRST/LAST design note [2], I listed remaining
items that can be done within the current architecture.
With this patch set, all items except PREFIX pattern
absorption are now covered: fixed-length group absorption
(0009), JIT proper support (0012), and FIRST/LAST
navigation (0015). PREFIX absorption is planned
separately, but stabilizing the base patch takes
priority. If it is ready in time, would it make sense
to decide then whether to include it in this round?
Apart from bug fixes, are there any features you
consider essential before the patch can be committed?

[1] Fixed-length group absorption design note
https://www.postgresql.org/message-id/CAAAe_zAKAGKpK9iHx3ZSuG59sP93r5dfootqv5tCfaMt%3Dw6wzA%40mail.gmail.com
[2] FIRST/LAST navigation design note
https://www.postgresql.org/message-id/CAAAe_zCUrKGBgZdaazdO_v9QWHsS_1DXuP%3DrLeNhO3iwsHwwbg%40mail.gmail.com
[3] jit: Change the default to off
https://github.com/postgres/postgres/commit/7f8c88c2b872

Best regards,
Henson

> I would appreciate your thoughts on these when you have time.
From f79d4358bc0033bc4fe8b4f8c9e32904d3df6a93 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 24 Mar 2026 19:04:19 +0900
Subject: [PATCH] Remove unused regex/regex.h include from nodeWindowAgg.c

---

From 31e07dcbd5391b7ff9ef8293fcb090cf8f845c71 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:25:40 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS() to nfa_add_state_unique() for
 state explosion patterns

---

From 0f15fdabc01fc1503f2a13253df65844ece4c86d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 11:03:39 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS() to nfa_try_absorb_context() loop

---

From 6601dda3d297ee8928bbe1c035102d683c78251f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:20:05 +0900
Subject: [PATCH] Fix in-place modification of defineClause TargetEntry in
 setrefs.c

---

 	/*

From 31e7dacb8b0fa6ead63ff92c19aa5dfb0cde76a1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:37:50 +0900
Subject: [PATCH] Fix mark handling for last_value() under RPR

 
 out_of_frame:

From 7dd896a8b279a1f02e8a5d9f241d9a59a32ee588 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 11:55:02 +0900
Subject: [PATCH] Fix DEFINE expression handling in RPR window planning

transformDefineClause() added full DEFINE expressions including
RPRNavExpr (PREV/NEXT) nodes to the query targetlist.  These
propagated to upper WindowAgg nodes that lack RPR navigation state,
causing a SIGSEGV when RPR and non-RPR windows coexist in the same
query.

Add only the Var nodes referenced by DEFINE expressions to the
targetlist, and protect those Vars from removal by
remove_unused_subquery_outputs() so they remain available in the
tuplestore slot for pattern matching evaluation.

Move the subquery wrapping tests from rpr.sql to rpr_integration.sql.
---
 src/backend/optimizer/path/allpaths.c |  68 +++++++++++++++++
 src/backend/parser/parse_rpr.c        | 106 ++++++++++++++------------
 2 files changed, 126 insertions(+), 48 deletions(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f42a2bae14a..f0ef4e289a6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4750,6 +4750,74 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		if (contain_volatile_functions(texpr))
 			continue;
 
+		/*
+		 * If any RPR window clause references this column in its DEFINE
+		 * clause, don't remove it.  The DEFINE expression needs these columns
+		 * in the tuplestore slot for pattern matching evaluation, even if the
+		 * outer query doesn't reference them.
+		 */
+		if (IsA(texpr, Var))
+		{
+			Var		   *var = (Var *) texpr;
+			ListCell   *wlc;
+			bool		needed_by_define = false;
+
+			foreach(wlc, subquery->windowClause)
+			{
+				WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+				if (wc->defineClause != NIL)
+				{
+					List	   *vars = pull_var_clause((Node *) wc->defineClause, 0);
+					ListCell   *vlc;
+
+					foreach(vlc, vars)
+					{
+						Var		   *dvar = (Var *) lfirst(vlc);
+
+						if (dvar->varattno == var->varattno)
+						{
+							needed_by_define = true;
+							break;
+						}
+					}
+					list_free(vars);
+					if (needed_by_define)
+						break;
+				}
+			}
+			if (needed_by_define)
+				continue;
+		}
+
+		/*
+		 * If it's a window function referencing a window clause with RPR (Row
+		 * Pattern Recognition), don't remove it.  Even when the window
+		 * function result is unused by the outer query, the RPR pattern
+		 * matching (frame reduction via DEFINE/PATTERN) must still execute.
+		 * Replacing this with NULL would leave no active window functions for
+		 * the WindowClause, causing the planner to omit the WindowAgg node
+		 * entirely.
+		 */
+		if (IsA(texpr, WindowFunc))
+		{
+			WindowFunc *wfunc = (WindowFunc *) texpr;
+			ListCell   *wlc;
+
+			foreach(wlc, subquery->windowClause)
+			{
+				WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+				if (wc->winref == wfunc->winref &&
+					wc->defineClause != NIL)
+				{
+					break;
+				}
+			}
+			if (wlc != NULL)
+				continue;
+		}
+
 		/*
 		 * OK, we don't need it.  Replace the expression with a NULL constant.
 		 * Preserve the exposed type of the expression, in case something
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 55283ab4bbe..db1309ca311 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -28,6 +28,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
 #include "optimizer/rpr.h"
 #include "parser/parse_clause.h"
 #include "parser/parse_collate.h"
@@ -310,9 +311,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	restargets = NIL;
 	foreach(lc, windef->rpCommonSyntax->rpDefs)
 	{
-		TargetEntry *te,
-				   *teDefine;
-		int			defineExprLocation;
+		TargetEntry *teDefine;
 
 		restarget = (ResTarget *) lfirst(lc);
 		name = restarget->name;
@@ -335,57 +334,68 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		restargets = lappend(restargets, restarget);
 
 		/*
-		 * Transform the DEFINE expression (restarget->val) and add it to the
-		 * targetlist as a TargetEntry if not already present, so the planner
-		 * can propagate the referenced columns to the outer plan's
-		 * targetlist.
+		 * Transform the DEFINE expression.  We must NOT add the whole
+		 * expression to the query targetlist, because it may contain
+		 * RPRNavExpr nodes (PREV/NEXT) that can only be evaluated inside the
+		 * owning WindowAgg.
 		 *
-		 * Note: findTargetlistEntrySQL99 transforms and clobbers
-		 * restarget->val.
+		 * Instead, we transform the expression directly and only ensure that
+		 * the individual Var nodes it references are present in the
+		 * targetlist, so the planner can propagate the referenced columns.
 		 */
+		{
+			Node	   *expr;
+			List	   *vars;
+			ListCell   *lc2;
 
-		/*
-		 * Save the original expression location before transformation.
-		 * findTargetlistEntrySQL99 may return an existing TargetEntry whose
-		 * location points to where it was originally created (e.g., ORDER
-		 * BY), not the DEFINE clause. We need to preserve the DEFINE location
-		 * for accurate error reporting.
-		 */
-		defineExprLocation = exprLocation(restarget->val);
+			expr = transformExpr(pstate, restarget->val,
+								 EXPR_KIND_RPR_DEFINE);
+
+			/*
+			 * Pull out Var nodes from the transformed expression and ensure
+			 * each one is present in the targetlist.  This is needed so the
+			 * planner propagates the referenced columns through the plan
+			 * tree, making them available to the WindowAgg's DEFINE
+			 * evaluation.
+			 */
+			vars = pull_var_clause(expr, 0);
+			foreach(lc2, vars)
+			{
+				Var		   *var = (Var *) lfirst(lc2);
+				bool		found = false;
+				ListCell   *tl;
 
-		te = findTargetlistEntrySQL99(pstate, restarget->val,
-									  targetlist, EXPR_KIND_RPR_DEFINE);
+				foreach(tl, *targetlist)
+				{
+					TargetEntry *tle = (TargetEntry *) lfirst(tl);
 
-		/* -------------------
-		 * Copy the TargetEntry for defineClause and always set the pattern
-		 * variable name. We use copyObject so the original targetlist entry
-		 * is not modified.
-		 *
-		 * Note: We must always set resname to the pattern variable name.
-		 * findTargetlistEntrySQL99 creates new TEs with resname = NULL
-		 * (resjunk entries), but returns existing TEs unchanged when the
-		 * expression already exists in targetlist.
-		 *
-		 * Example: "SELECT id, flag, ... WINDOW w AS (... DEFINE T AS flag)"
-		 *
-		 * 1. SELECT list processing creates: TE{resname="flag", expr=flag}
-		 * 2. DEFINE T AS flag: findTargetlistEntrySQL99 finds existing TE
-		 * 3. te->resname is "flag" (from SELECT), not NULL
-		 * 4. Without unconditionally setting resname, teDefine->resname
-		 *    would remain "flag" instead of pattern variable name "T"
-		 * 5. buildRPRPattern builds defineVariableList from resname, so
-		 *    it would contain ["flag"] instead of ["T"]
-		 * 6. Pattern variable "T" not found -> Assert failure crash
-		 */
-		teDefine = copyObject(te);
-		teDefine->resname = pstrdup(name);
+					if (IsA(tle->expr, Var) &&
+						((Var *) tle->expr)->varno == var->varno &&
+						((Var *) tle->expr)->varattno == var->varattno)
+					{
+						found = true;
+						break;
+					}
+				}
+				if (!found)
+				{
+					TargetEntry *newtle;
 
-		/*
-		 * Update the expression location to point to the DEFINE clause. This
-		 * ensures error messages reference the correct source location.
-		 */
-		if (defineExprLocation >= 0 && IsA(teDefine->expr, Var))
-			((Var *) teDefine->expr)->location = defineExprLocation;
+					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);
-- 
2.50.1 (Apple Git-155)

From 4141670cfe47e9aa2ceac717dba0d09015fa89c1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 14:09:53 +0900
Subject: [PATCH] Add RPR planner integration tests

Add rpr_integration.sql covering planner optimization interactions
with RPR windows: frame optimization, run condition pushdown, window
deduplication, unused window removal, inverse transition, cost
estimation, subquery flattening, DEFINE expression propagation, and
LIMIT.  Also covers integration scenarios with partitioned tables,
LATERAL, recursive CTEs, incremental sort, volatile functions, and
correlated subqueries.
---
 src/test/regress/expected/rpr_integration.out | 885 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/rpr_integration.sql      | 537 +++++++++++
 3 files changed, 1423 insertions(+), 1 deletion(-)
 create mode 100644 src/test/regress/expected/rpr_integration.out
 create mode 100644 src/test/regress/sql/rpr_integration.sql

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
new file mode 100644
index 00000000000..a21ac5a8588
--- /dev/null
+++ b/src/test/regress/expected/rpr_integration.out
@@ -0,0 +1,885 @@
+-- ============================================================
+-- RPR Integration Tests
+-- Planner optimization interaction tests for Row Pattern Recognition
+-- ============================================================
+--
+-- Verifies that each planner optimization correctly handles RPR windows.
+-- Even if individual optimizations are tested elsewhere, this file
+-- provides a single checkpoint for all planner/RPR interactions.
+--
+-- A. Planner Optimization Protection Tests
+--    A1. Frame optimization bypass
+--    A2. Run condition pushdown bypass
+--    A3. Window dedup prevention (planner)
+--    A4. Window dedup prevention (parser)
+--    A5. Unused window removal prevention
+--    A6. Inverse transition bypass
+--    A7. Cost estimation RPR awareness
+--    A8. Subquery flattening prevention
+--    A9. DEFINE expression non-propagation
+--    A10. RPR + LIMIT
+--
+-- B. Integration Scenario Tests
+--    B1. RPR + CTE
+--    B2. RPR + JOIN
+--    B3. RPR + Set operations
+--    B4. RPR + Prepared statements
+--    B5. RPR + Partitioned table
+--    B6. RPR + LATERAL
+--    B7. RPR + Recursive CTE
+--    B8. RPR + Incremental sort
+--    B9. RPR + Volatile function in DEFINE
+--    B10. RPR + Correlated subquery
+--
+CREATE TABLE rpr_integ (id INT, val INT);
+INSERT INTO rpr_integ VALUES
+    (1, 10), (2, 20), (3, 15), (4, 25), (5, 5),
+    (6, 30), (7, 35), (8, 20), (9, 40), (10, 45);
+-- ============================================================
+-- A1. Frame optimization bypass
+-- ============================================================
+-- optimize_window_clauses() must not apply frame optimization to RPR windows.
+-- Non-RPR: frame is optimized (RANGE -> ROWS conversion, etc.)
+-- RPR: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+-- Non-RPR window with default frame -> frame optimization applied
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id);
+            QUERY PLAN             
+-----------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id)
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(5 rows)
+
+-- RPR window -> frame optimization must NOT change the frame
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(6 rows)
+
+-- ============================================================
+-- A2. Run condition pushdown bypass
+-- ============================================================
+-- find_window_run_conditions() must not push monotonic filters as Run Conditions
+-- for RPR windows, because RPR results are match-dependent, not monotonic.
+-- Non-RPR: count(*) > 0 pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+) t WHERE cnt > 0;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on t
+   ->  WindowAgg
+         Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Run Condition: (count(*) OVER w > 0)
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(7 rows)
+
+-- RPR: count(*) > 0 must NOT be pushed down (appears as Filter, not Run Condition)
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on t
+   Filter: (t.cnt > 0)
+   ->  WindowAgg
+         Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(8 rows)
+
+-- Verify results are correct
+SELECT * FROM (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  3 |  15 |   2
+  5 |   5 |   3
+  8 |  20 |   3
+(4 rows)
+
+-- ============================================================
+-- A3. Window dedup prevention (planner)
+-- ============================================================
+-- optimize_window_clauses() must not merge RPR and non-RPR windows
+-- even if they share the same base frame.
+-- Two windows with same ORDER BY: one RPR, one non-RPR -> separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ WindowAgg
+   Window: w_normal AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   ->  WindowAgg
+         Window: w_rpr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: id
+               ->  Seq Scan on rpr_integ
+(8 rows)
+
+-- Verify both produce different results
+SELECT
+    id, val,
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY id;
+ id | val | rpr_cnt | normal_cnt 
+----+-----+---------+------------
+  1 |  10 |       2 |         10
+  2 |  20 |       0 |          9
+  3 |  15 |       2 |          8
+  4 |  25 |       0 |          7
+  5 |   5 |       3 |          6
+  6 |  30 |       0 |          5
+  7 |  35 |       0 |          4
+  8 |  20 |       3 |          3
+  9 |  40 |       0 |          2
+ 10 |  45 |       0 |          1
+(10 rows)
+
+-- ============================================================
+-- A4. Window dedup prevention (parser)
+-- ============================================================
+-- transformWindowFuncCall() must not merge two RPR windows with different DEFINE.
+-- Two RPR windows: same PATTERN structure, different DEFINE conditions.
+-- EXPLAIN: must show two separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER w1 AS cnt_up,
+    count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+    w1 AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w2 AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val < PREV(val));
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ WindowAgg
+   Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  WindowAgg
+         Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: id
+               ->  Seq Scan on rpr_integ
+(9 rows)
+
+-- Verify results differ
+SELECT
+    id, val,
+    count(*) OVER w1 AS cnt_up,
+    count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+    w1 AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w2 AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val < PREV(val))
+ORDER BY id;
+ id | val | cnt_up | cnt_down 
+----+-----+--------+----------
+  1 |  10 |      2 |        0
+  2 |  20 |      0 |        2
+  3 |  15 |      2 |        0
+  4 |  25 |      0 |        2
+  5 |   5 |      3 |        0
+  6 |  30 |      0 |        0
+  7 |  35 |      0 |        2
+  8 |  20 |      3 |        0
+  9 |  40 |      0 |        0
+ 10 |  45 |      0 |        0
+(10 rows)
+
+-- ============================================================
+-- A5. Unused window removal prevention
+-- ============================================================
+-- remove_unused_subquery_outputs() must not remove RPR window functions
+-- even when their output columns are not referenced by the outer query.
+-- RPR window output not directly referenced, but pattern matching must still execute
+SELECT count(*) FROM (
+    SELECT count(*) OVER w FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+ count 
+-------
+    10
+(1 row)
+
+-- With PREV in DEFINE: window must be preserved
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+ count | sum 
+-------+-----
+    10 |   6
+(1 row)
+
+-- Without PREV but with DEFINE: window must still be preserved
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS TRUE)
+) t;
+ count | sum 
+-------+-----
+    10 |  10
+(1 row)
+
+-- ============================================================
+-- A6. Inverse transition bypass
+-- ============================================================
+-- RPR windows must not use moving aggregate (inverse transition) optimization.
+-- Moving aggregates recalculate by subtracting leaving rows, but RPR's
+-- reduced frames make this invalid.
+-- sum() supports inverse transition. With RPR, it must not be used.
+-- Verify correctness: sum with RPR pattern matching
+SELECT id, val,
+    sum(val) OVER w AS pattern_sum
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | pattern_sum 
+----+-----+-------------
+  1 |  10 |          30
+  2 |  20 |            
+  3 |  15 |          40
+  4 |  25 |            
+  5 |   5 |          70
+  6 |  30 |            
+  7 |  35 |            
+  8 |  20 |         105
+  9 |  40 |            
+ 10 |  45 |            
+(10 rows)
+
+-- ============================================================
+-- A7. Cost estimation RPR awareness
+-- ============================================================
+-- cost_windowagg() must account for DEFINE expression evaluation cost.
+-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost.
+CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$
+DECLARE
+    plan json;
+    cost numeric;
+BEGIN
+    EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan;
+    cost := (plan->0->'Plan'->>'Total Cost')::numeric;
+    RETURN cost;
+END;
+$$ LANGUAGE plpgsql;
+SELECT get_windowagg_cost(
+    'SELECT count(*) OVER w FROM rpr_integ
+     WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                  PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))')
+    >
+    get_windowagg_cost(
+    'SELECT count(*) OVER w FROM rpr_integ
+     WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)')
+    AS rpr_cost_is_higher;
+ rpr_cost_is_higher 
+--------------------
+ t
+(1 row)
+
+DROP FUNCTION get_windowagg_cost(text);
+-- ============================================================
+-- A8. Subquery flattening prevention
+-- ============================================================
+-- is_simple_subquery() prevents pullup of subqueries with window functions.
+-- RPR subquery must not be flattened into the outer query.
+-- EXPLAIN must show Subquery Scan (not flattened).
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) sub
+WHERE cnt > 0;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on sub
+   Filter: (sub.cnt > 0)
+   ->  WindowAgg
+         Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(8 rows)
+
+-- ============================================================
+-- A9. DEFINE expression non-propagation
+-- ============================================================
+-- DEFINE expressions must not appear in upper WindowAgg's targetlist.
+-- Only Var nodes should be propagated, not the full DEFINE expression.
+-- EXPLAIN VERBOSE: outer WindowAgg output must NOT contain DEFINE expression.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+                                            QUERY PLAN                                             
+---------------------------------------------------------------------------------------------------
+ WindowAgg
+   Output: (count(*) OVER w_rpr), count(*) OVER w_normal, id, val
+   Window: w_normal AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   ->  WindowAgg
+         Output: id, val, count(*) OVER w_rpr
+         Window: w_rpr AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Output: id, val
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on public.rpr_integ
+                     Output: id, val
+(12 rows)
+
+-- ============================================================
+-- A10. RPR + LIMIT
+-- ============================================================
+-- LIMIT must not interfere with RPR pattern matching.
+-- The result should be a prefix of the unlimited query.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id
+LIMIT 5;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+(5 rows)
+
+-- ============================================================
+-- B1. RPR + CTE
+-- ============================================================
+-- CTE with RPR, outer query aggregates
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT count(*) AS total_rows,
+       count(*) FILTER (WHERE cnt > 0) AS matched_rows,
+       max(cnt) AS max_match_len
+FROM rpr_result;
+ total_rows | matched_rows | max_match_len 
+------------+--------------+---------------
+         10 |            4 |             3
+(1 row)
+
+-- Multiple CTE references
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+ id | cnt 
+----+-----
+  1 |   2
+  3 |   2
+  5 |   3
+  8 |   3
+(4 rows)
+
+-- ============================================================
+-- B2. RPR + JOIN
+-- ============================================================
+CREATE TABLE rpr_integ2 (id INT, label TEXT);
+INSERT INTO rpr_integ2 VALUES
+    (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'),
+    (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j');
+-- RPR subquery joined with another table
+SELECT r.id, r.cnt, j.label
+FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+ id | cnt | label 
+----+-----+-------
+  1 |   2 | a
+  3 |   2 | c
+  5 |   3 | e
+  8 |   3 | h
+(4 rows)
+
+-- ============================================================
+-- B3. RPR + Set operations
+-- ============================================================
+-- UNION of RPR and non-RPR results
+SELECT id, cnt, 'rpr' AS source FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+ id | cnt | source 
+----+-----+--------
+  1 |   1 | normal
+  2 |   2 | normal
+  3 |   3 | normal
+  4 |   4 | normal
+  5 |   5 | normal
+  6 |   6 | normal
+  7 |   7 | normal
+  8 |   8 | normal
+  9 |   9 | normal
+ 10 |  10 | normal
+  1 |   2 | rpr
+  3 |   2 | rpr
+  5 |   3 | rpr
+  8 |   3 | rpr
+(14 rows)
+
+-- ============================================================
+-- B4. RPR + Prepared statements
+-- ============================================================
+-- Plan caching must correctly serialize/deserialize RPR fields
+PREPARE rpr_prep AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+-- First execution (custom plan)
+EXECUTE rpr_prep;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- Subsequent executions (may use cached plan)
+EXECUTE rpr_prep;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+EXECUTE rpr_prep;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+EXECUTE rpr_prep;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+EXECUTE rpr_prep;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+EXECUTE rpr_prep;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- Verify cached plan produces same result
+EXECUTE rpr_prep;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+DEALLOCATE rpr_prep;
+-- ============================================================
+-- B5. RPR + Partitioned table
+-- ============================================================
+-- RPR pattern matching must work correctly across partitions.
+CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id);
+CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6);
+CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11);
+INSERT INTO rpr_part SELECT id, val FROM rpr_integ;
+-- Same query on partitioned vs non-partitioned should produce identical results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- EXPLAIN: Append under Sort under WindowAgg
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_part
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+                                       QUERY PLAN                                       
+----------------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY rpr_part.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Sort
+         Sort Key: rpr_part.id
+         ->  Append
+               ->  Seq Scan on rpr_part_1
+               ->  Seq Scan on rpr_part_2
+(8 rows)
+
+DROP TABLE rpr_part;
+-- ============================================================
+-- B6. RPR + LATERAL
+-- ============================================================
+-- RPR inside a LATERAL subquery.  Qualified column references from the
+-- outer query are not yet supported in DEFINE, so this tests the basic
+-- case where LATERAL provides filtering but DEFINE uses only local columns.
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WHERE id <= o.id
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+ outer_id | id | cnt 
+----------+----+-----
+        5 |  1 |   2
+        5 |  3 |   2
+       10 |  1 |   2
+       10 |  3 |   2
+       10 |  5 |   3
+       10 |  8 |   3
+(6 rows)
+
+-- ============================================================
+-- B7. RPR + Recursive CTE
+-- ============================================================
+-- RPR applied to results of a recursive CTE.
+WITH RECURSIVE seq AS (
+    SELECT 1 AS id, 10 AS val
+    UNION ALL
+    SELECT id + 1, val + (CASE WHEN id % 3 = 0 THEN -15 ELSE 10 END)
+    FROM seq WHERE id < 10
+)
+SELECT id, val, count(*) OVER w AS cnt
+FROM seq
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  15 |   3
+  5 |  25 |   0
+  6 |  35 |   0
+  7 |  20 |   3
+  8 |  30 |   0
+  9 |  40 |   0
+ 10 |  25 |   0
+(10 rows)
+
+-- ============================================================
+-- B8. RPR + Incremental sort
+-- ============================================================
+-- Incremental sort may be used when data is partially sorted.
+CREATE INDEX rpr_integ_id_idx ON rpr_integ (id);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Index Scan using rpr_integ_id_idx on rpr_integ
+(4 rows)
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+RESET enable_seqscan;
+DROP INDEX rpr_integ_id_idx;
+-- ============================================================
+-- B9. RPR + Volatile function in DEFINE
+-- ============================================================
+-- Volatile functions must be evaluated per-row, not optimized away.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val) AND random() >= 0.0)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- ============================================================
+-- B10. RPR + Correlated subquery in WHERE
+-- ============================================================
+-- RPR window in a correlated subquery.  DEFINE references only local
+-- columns (no qualified refs), while correlation is in WHERE.
+SELECT o.id, o.val,
+    (SELECT count(*) OVER w
+     FROM rpr_integ i
+     WHERE i.id <= o.id
+     WINDOW w AS (ORDER BY id
+         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+         PATTERN (A B+)
+         DEFINE B AS val > PREV(val))
+     ORDER BY id
+     LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+ id | val | first_cnt 
+----+-----+-----------
+  1 |  10 |         0
+  2 |  20 |         2
+  3 |  15 |         2
+  4 |  25 |         2
+  5 |   5 |         2
+  6 |  30 |         2
+  7 |  35 |         2
+  8 |  20 |         2
+  9 |  40 |         2
+ 10 |  45 |         2
+(10 rows)
+
+-- Cleanup
+DROP TABLE rpr_integ;
+DROP TABLE rpr_integ2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 6a2d19d4410..dbfa3e8f2cc 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -107,7 +107,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Row Pattern Recognition tests
 # ----------
-test: rpr rpr_base rpr_explain rpr_nfa
+test: rpr rpr_base rpr_explain rpr_nfa rpr_integration
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
new file mode 100644
index 00000000000..2ae42c92a27
--- /dev/null
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -0,0 +1,537 @@
+-- ============================================================
+-- RPR Integration Tests
+-- Planner optimization interaction tests for Row Pattern Recognition
+-- ============================================================
+--
+-- Verifies that each planner optimization correctly handles RPR windows.
+-- Even if individual optimizations are tested elsewhere, this file
+-- provides a single checkpoint for all planner/RPR interactions.
+--
+-- A. Planner Optimization Protection Tests
+--    A1. Frame optimization bypass
+--    A2. Run condition pushdown bypass
+--    A3. Window dedup prevention (planner)
+--    A4. Window dedup prevention (parser)
+--    A5. Unused window removal prevention
+--    A6. Inverse transition bypass
+--    A7. Cost estimation RPR awareness
+--    A8. Subquery flattening prevention
+--    A9. DEFINE expression non-propagation
+--    A10. RPR + LIMIT
+--
+-- B. Integration Scenario Tests
+--    B1. RPR + CTE
+--    B2. RPR + JOIN
+--    B3. RPR + Set operations
+--    B4. RPR + Prepared statements
+--    B5. RPR + Partitioned table
+--    B6. RPR + LATERAL
+--    B7. RPR + Recursive CTE
+--    B8. RPR + Incremental sort
+--    B9. RPR + Volatile function in DEFINE
+--    B10. RPR + Correlated subquery
+--
+
+CREATE TABLE rpr_integ (id INT, val INT);
+INSERT INTO rpr_integ VALUES
+    (1, 10), (2, 20), (3, 15), (4, 25), (5, 5),
+    (6, 30), (7, 35), (8, 20), (9, 40), (10, 45);
+
+-- ============================================================
+-- A1. Frame optimization bypass
+-- ============================================================
+-- optimize_window_clauses() must not apply frame optimization to RPR windows.
+-- Non-RPR: frame is optimized (RANGE -> ROWS conversion, etc.)
+-- RPR: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+
+-- Non-RPR window with default frame -> frame optimization applied
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id);
+
+-- RPR window -> frame optimization must NOT change the frame
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+
+-- ============================================================
+-- A2. Run condition pushdown bypass
+-- ============================================================
+-- find_window_run_conditions() must not push monotonic filters as Run Conditions
+-- for RPR windows, because RPR results are match-dependent, not monotonic.
+
+-- Non-RPR: count(*) > 0 pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+) t WHERE cnt > 0;
+
+-- RPR: count(*) > 0 must NOT be pushed down (appears as Filter, not Run Condition)
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0;
+
+-- Verify results are correct
+SELECT * FROM (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+ORDER BY id;
+
+-- ============================================================
+-- A3. Window dedup prevention (planner)
+-- ============================================================
+-- optimize_window_clauses() must not merge RPR and non-RPR windows
+-- even if they share the same base frame.
+
+-- Two windows with same ORDER BY: one RPR, one non-RPR -> separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+
+-- Verify both produce different results
+SELECT
+    id, val,
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY id;
+
+-- ============================================================
+-- A4. Window dedup prevention (parser)
+-- ============================================================
+-- transformWindowFuncCall() must not merge two RPR windows with different DEFINE.
+-- Two RPR windows: same PATTERN structure, different DEFINE conditions.
+
+-- EXPLAIN: must show two separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER w1 AS cnt_up,
+    count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+    w1 AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w2 AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val < PREV(val));
+
+-- Verify results differ
+SELECT
+    id, val,
+    count(*) OVER w1 AS cnt_up,
+    count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+    w1 AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w2 AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val < PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- A5. Unused window removal prevention
+-- ============================================================
+-- remove_unused_subquery_outputs() must not remove RPR window functions
+-- even when their output columns are not referenced by the outer query.
+
+-- RPR window output not directly referenced, but pattern matching must still execute
+SELECT count(*) FROM (
+    SELECT count(*) OVER w FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+
+-- With PREV in DEFINE: window must be preserved
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+
+-- Without PREV but with DEFINE: window must still be preserved
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS TRUE)
+) t;
+
+-- ============================================================
+-- A6. Inverse transition bypass
+-- ============================================================
+-- RPR windows must not use moving aggregate (inverse transition) optimization.
+-- Moving aggregates recalculate by subtracting leaving rows, but RPR's
+-- reduced frames make this invalid.
+
+-- sum() supports inverse transition. With RPR, it must not be used.
+-- Verify correctness: sum with RPR pattern matching
+SELECT id, val,
+    sum(val) OVER w AS pattern_sum
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- A7. Cost estimation RPR awareness
+-- ============================================================
+-- cost_windowagg() must account for DEFINE expression evaluation cost.
+-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost.
+
+CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$
+DECLARE
+    plan json;
+    cost numeric;
+BEGIN
+    EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan;
+    cost := (plan->0->'Plan'->>'Total Cost')::numeric;
+    RETURN cost;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT get_windowagg_cost(
+    'SELECT count(*) OVER w FROM rpr_integ
+     WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                  PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))')
+    >
+    get_windowagg_cost(
+    'SELECT count(*) OVER w FROM rpr_integ
+     WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)')
+    AS rpr_cost_is_higher;
+
+DROP FUNCTION get_windowagg_cost(text);
+
+-- ============================================================
+-- A8. Subquery flattening prevention
+-- ============================================================
+-- is_simple_subquery() prevents pullup of subqueries with window functions.
+-- RPR subquery must not be flattened into the outer query.
+-- EXPLAIN must show Subquery Scan (not flattened).
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) sub
+WHERE cnt > 0;
+
+-- ============================================================
+-- A9. DEFINE expression non-propagation
+-- ============================================================
+-- DEFINE expressions must not appear in upper WindowAgg's targetlist.
+-- Only Var nodes should be propagated, not the full DEFINE expression.
+-- EXPLAIN VERBOSE: outer WindowAgg output must NOT contain DEFINE expression.
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+
+-- ============================================================
+-- A10. RPR + LIMIT
+-- ============================================================
+-- LIMIT must not interfere with RPR pattern matching.
+-- The result should be a prefix of the unlimited query.
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id
+LIMIT 5;
+
+-- ============================================================
+-- B1. RPR + CTE
+-- ============================================================
+
+-- CTE with RPR, outer query aggregates
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT count(*) AS total_rows,
+       count(*) FILTER (WHERE cnt > 0) AS matched_rows,
+       max(cnt) AS max_match_len
+FROM rpr_result;
+
+-- Multiple CTE references
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+
+-- ============================================================
+-- B2. RPR + JOIN
+-- ============================================================
+
+CREATE TABLE rpr_integ2 (id INT, label TEXT);
+INSERT INTO rpr_integ2 VALUES
+    (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'),
+    (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j');
+
+-- RPR subquery joined with another table
+SELECT r.id, r.cnt, j.label
+FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+
+-- ============================================================
+-- B3. RPR + Set operations
+-- ============================================================
+
+-- UNION of RPR and non-RPR results
+SELECT id, cnt, 'rpr' AS source FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+
+-- ============================================================
+-- B4. RPR + Prepared statements
+-- ============================================================
+
+-- Plan caching must correctly serialize/deserialize RPR fields
+PREPARE rpr_prep AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- First execution (custom plan)
+EXECUTE rpr_prep;
+
+-- Subsequent executions (may use cached plan)
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+
+-- Verify cached plan produces same result
+EXECUTE rpr_prep;
+
+DEALLOCATE rpr_prep;
+
+-- ============================================================
+-- B5. RPR + Partitioned table
+-- ============================================================
+-- RPR pattern matching must work correctly across partitions.
+
+CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id);
+CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6);
+CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11);
+INSERT INTO rpr_part SELECT id, val FROM rpr_integ;
+
+-- Same query on partitioned vs non-partitioned should produce identical results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- EXPLAIN: Append under Sort under WindowAgg
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_part
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+
+DROP TABLE rpr_part;
+
+-- ============================================================
+-- B6. RPR + LATERAL
+-- ============================================================
+-- RPR inside a LATERAL subquery.  Qualified column references from the
+-- outer query are not yet supported in DEFINE, so this tests the basic
+-- case where LATERAL provides filtering but DEFINE uses only local columns.
+
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WHERE id <= o.id
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+
+-- ============================================================
+-- B7. RPR + Recursive CTE
+-- ============================================================
+-- RPR applied to results of a recursive CTE.
+
+WITH RECURSIVE seq AS (
+    SELECT 1 AS id, 10 AS val
+    UNION ALL
+    SELECT id + 1, val + (CASE WHEN id % 3 = 0 THEN -15 ELSE 10 END)
+    FROM seq WHERE id < 10
+)
+SELECT id, val, count(*) OVER w AS cnt
+FROM seq
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- B8. RPR + Incremental sort
+-- ============================================================
+-- Incremental sort may be used when data is partially sorted.
+
+CREATE INDEX rpr_integ_id_idx ON rpr_integ (id);
+SET enable_seqscan = off;
+
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+RESET enable_seqscan;
+DROP INDEX rpr_integ_id_idx;
+
+-- ============================================================
+-- B9. RPR + Volatile function in DEFINE
+-- ============================================================
+-- Volatile functions must be evaluated per-row, not optimized away.
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val) AND random() >= 0.0)
+ORDER BY id;
+
+-- ============================================================
+-- B10. RPR + Correlated subquery in WHERE
+-- ============================================================
+-- RPR window in a correlated subquery.  DEFINE references only local
+-- columns (no qualified refs), while correlation is in WHERE.
+
+SELECT o.id, o.val,
+    (SELECT count(*) OVER w
+     FROM rpr_integ i
+     WHERE i.id <= o.id
+     WINDOW w AS (ORDER BY id
+         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+         PATTERN (A B+)
+         DEFINE B AS val > PREV(val))
+     ORDER BY id
+     LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+
+-- Cleanup
+DROP TABLE rpr_integ;
+DROP TABLE rpr_integ2;
-- 
2.50.1 (Apple Git-155)

From 24a19226ef1fe100fc1680b178103e997c0c48bb Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 14:09:12 +0900
Subject: [PATCH] Replace reduced frame map with single match result

The reduced frame map was a per-row byte array tracking match status.
Since rows are processed sequentially and only one match is active
at a time, replace it with four scalar fields: valid, matched,
start, and length.

Also distinguish empty matches (FIN reached with zero rows consumed)
from unmatched rows via RF_EMPTY_MATCH, counted as matched in NFA
statistics.
---
 src/backend/executor/execRPR.c            |  56 +++---
 src/backend/executor/nodeWindowAgg.c      | 221 ++++++++--------------
 src/include/nodes/execnodes.h             |  21 +-
 src/test/regress/expected/rpr_explain.out |   8 +-
 4 files changed, 126 insertions(+), 180 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 58f9da0b814..1cbe8e14780 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -549,7 +549,7 @@
  *
  *   (1) Find or create a context for the target row
  *   (2) Enter the row processing loop
- *   (3) After the loop ends, record the result in reduced_frame_map
+ *   (3) After the loop ends, record the match result
  *
  * Pseudocode of the row processing loop:
  *
@@ -923,18 +923,19 @@
  * Chapter X  Match Result Processing
  * ============================================================================
  *
- * X-1. Reduced Frame Map
+ * X-1. Match Result
  *
- * RPR match results are recorded in a byte array called reduced_frame_map.
- * One byte is allocated per row, and the value is one of the following:
+ * RPR tracks the current match result as a single entry in WindowAggState
+ * with four fields: rpr_match_valid, rpr_match_matched, rpr_match_start,
+ * and rpr_match_length.  When valid is true, the entry describes the
+ * match result for the position at rpr_match_start: matched indicates
+ * success or failure, and length gives the number of rows consumed.
+ * A match with length 0 represents an empty match (pattern matched but
+ * consumed no rows).  When valid is false, the position has not been
+ * evaluated yet (RF_NOT_DETERMINED).
  *
- *   RF_NOT_DETERMINED (0)  Not yet processed
- *   RF_FRAME_HEAD     (1)  Start row of the match
- *   RF_SKIPPED        (2)  Interior row of the match (skipped in frame)
- *   RF_UNMATCHED      (3)  Match failure
- *
- * The window function references this map to determine frame inclusion for
- * each row.
+ * The window function calls get_reduced_frame_status() to look up a
+ * row's status against the current match result.
  *
  * X-2. AFTER MATCH SKIP
  *
@@ -1028,8 +1029,7 @@
  *     Phase 3 (Advance): skipped (no states)
  *
  *   C0.states is empty, so the loop terminates.
- *   matchEndRow < matchStartRow -> RF_UNMATCHED.
- *   register_reduced_frame_map(0, RF_UNMATCHED).
+ *   matchEndRow < matchStartRow -> unmatched.
  *
  * --- Row 1 (price=110) ---
  *
@@ -1113,9 +1113,7 @@
  *
  *   C1.states is empty and matchEndRow=3 >= matchStartRow=1 -> match succeeds.
  *
- *   register_reduced_frame_map(1, RF_FRAME_HEAD)
- *   register_reduced_frame_map(2, RF_SKIPPED)
- *   register_reduced_frame_map(3, RF_SKIPPED)
+ *   rpr_match_start = 1, rpr_match_length = 3
  *
  * --- Row 4 (price=130) ---
  *
@@ -1128,15 +1126,15 @@
  *     B: 130 < PREV(115) -> false
  *
  *   ... No subsequent rows, so ExecRPRFinalizeAllContexts() is called.
- *   Match incomplete -> RF_UNMATCHED.
+ *   Match incomplete -> unmatched.
  *
  * XI-5. Final Result
  *
- *   Row 0: RF_UNMATCHED  -> frame = the row itself
- *   Row 1: RF_FRAME_HEAD -> frame = rows 1 through 3
- *   Row 2: RF_SKIPPED    -> inside row 1's match
- *   Row 3: RF_SKIPPED    -> inside row 1's match
- *   Row 4: RF_UNMATCHED  -> frame = the row itself
+ *   Row 0: unmatched     -> frame = the row itself
+ *   Row 1: match head    -> frame = rows 1 through 3
+ *   Row 2: inside match  -> skipped
+ *   Row 3: inside match  -> skipped
+ *   Row 4: unmatched     -> frame = the row itself
  *
  * Chapter XII  Summary of Key Design Decisions
  * ============================================================================
@@ -1579,12 +1577,14 @@ static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
  *
  * - Empty match handling: The initial advance uses currentPos =
  *   startPos - 1 (before any row is consumed). If FIN is reached via
- *   epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow,
- *   resulting in UNMATCHED. For reluctant min=0 patterns (A*?, A??),
- *   the skip path reaches FIN first and early termination prunes enter
- *   paths, yielding an immediate empty (unmatched) result. For
- *   greedy patterns (A*), the enter path adds VAR states first, then
- *   the skip FIN is recorded but VAR states survive for later matching.
+ *   epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow.
+ *   If matchedState is set (FIN was reached), this is an empty match
+ *   (RF_EMPTY_MATCH); otherwise it is unmatched (RF_UNMATCHED).
+ *   For reluctant min=0 patterns (A*?, A??), the skip path reaches
+ *   FIN first and early termination prunes enter paths, yielding an
+ *   immediate empty match result. For greedy patterns (A*), the enter
+ *   path adds VAR states first, then the skip FIN is recorded but VAR
+ *   states survive for later matching.
  *
  * Context Absorption Runtime:
  * ---------------------------
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index aed7cbef99a..1c925e0221d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -249,11 +249,8 @@ static bool attno_map_walker(Node *node, void *context);
 static bool rpr_is_defined(WindowAggState *winstate);
 static int	row_is_in_reduced_frame(WindowObject winobj, int64 pos);
 
-static void create_reduced_frame_map(WindowAggState *winstate);
-static void clear_reduced_frame_map(WindowAggState *winstate);
-static int	get_reduced_frame_map(WindowAggState *winstate, int64 pos);
-static void register_reduced_frame_map(WindowAggState *winstate, int64 pos,
-									   int val);
+static void clear_reduced_frame(WindowAggState *winstate);
+static int	get_reduced_frame_status(WindowAggState *winstate, int64 pos);
 static void update_reduced_frame(WindowObject winobj, int64 pos);
 
 static void check_rpr_navigation(Node *node, bool is_prev);
@@ -1037,12 +1034,6 @@ eval_windowaggregates(WindowAggState *winstate)
 	{
 		int			ret;
 
-#ifdef RPR_DEBUG
-		printf("===== loop in frame starts: aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
-			   winstate->aggregatedupto,
-			   winstate->aggregatedbase);
-#endif
-
 		/* Fetch next row if we didn't already */
 		if (TupIsNull(agg_row_slot))
 		{
@@ -1065,27 +1056,18 @@ eval_windowaggregates(WindowAggState *winstate)
 
 		if (rpr_is_defined(winstate))
 		{
-#ifdef RPR_DEBUG
-			printf("reduced_frame_map: %d aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
-				   get_reduced_frame_map(winstate,
-										 winstate->aggregatedupto),
-				   winstate->aggregatedupto,
-				   winstate->aggregatedbase);
-#endif
-
 			/*
-			 * If the row status at currentpos is already decided and current
-			 * row status is not decided yet, it means we passed the last
-			 * reduced frame. Time to break the loop.
+			 * If currentpos is already decided but aggregatedupto is not yet
+			 * determined, we've passed the last reduced frame.
 			 */
-			if (get_reduced_frame_map(winstate, winstate->currentpos)
+			if (get_reduced_frame_status(winstate, winstate->currentpos)
 				!= RF_NOT_DETERMINED &&
-				get_reduced_frame_map(winstate, winstate->aggregatedupto)
+				get_reduced_frame_status(winstate, winstate->aggregatedupto)
 				== RF_NOT_DETERMINED)
 				break;
 
 			/*
-			 * Otherwise we need to calculate the reduced frame.
+			 * Calculate the reduced frame for aggregatedupto.
 			 */
 			ret = row_is_in_reduced_frame(winstate->agg_winobj,
 										  winstate->aggregatedupto);
@@ -1093,17 +1075,13 @@ eval_windowaggregates(WindowAggState *winstate)
 				break;
 
 			/*
-			 * Check if current row needs to be skipped due to no match.
+			 * Check if current row is inside a match but not the head
+			 * (skipped), and it's the base row for aggregation.
 			 */
-			if (get_reduced_frame_map(winstate,
-									  winstate->aggregatedupto) == RF_SKIPPED &&
+			if (get_reduced_frame_status(winstate,
+										 winstate->aggregatedupto) == RF_SKIPPED &&
 				winstate->aggregatedupto == winstate->aggregatedbase)
-			{
-#ifdef RPR_DEBUG
-				printf("skip current row for aggregation\n");
-#endif
 				break;
-			}
 		}
 
 		/* Set tuple context for evaluation of aggregate arguments */
@@ -1358,7 +1336,8 @@ begin_partition(WindowAggState *winstate)
 	winstate->framehead_valid = false;
 	winstate->frametail_valid = false;
 	winstate->grouptail_valid = false;
-	create_reduced_frame_map(winstate);
+	if (rpr_is_defined(winstate))
+		clear_reduced_frame(winstate);
 	winstate->spooled_rows = 0;
 	winstate->currentpos = 0;
 	winstate->frameheadpos = 0;
@@ -1581,9 +1560,8 @@ release_partition(WindowAggState *winstate)
 	winstate->partition_spooled = false;
 	winstate->next_partition = true;
 
-	/* Reset RPR reduced frame map */
-	winstate->reduced_frame_map = NULL;
-	winstate->alloc_sz = 0;
+	/* Reset RPR match results */
+	clear_reduced_frame(winstate);
 
 	/* Reset NFA state for new partition */
 	winstate->nfaContext = NULL;
@@ -2366,11 +2344,6 @@ ExecWindowAgg(PlanState *pstate)
 
 	CHECK_FOR_INTERRUPTS();
 
-#ifdef RPR_DEBUG
-	printf("ExecWindowAgg called. pos: " INT64_FORMAT "\n",
-		   winstate->currentpos);
-#endif
-
 	if (winstate->status == WINDOWAGG_DONE)
 		return NULL;
 
@@ -2480,14 +2453,13 @@ ExecWindowAgg(PlanState *pstate)
 		if (winstate->status == WINDOWAGG_RUN)
 		{
 			/*
-			 * If RPR is defined and skip mode is next row, we need to clear
-			 * existing reduced frame info so that we newly calculate the info
-			 * starting from current row.
+			 * If RPR is defined and skip mode is next row, clear the current
+			 * match so the next row triggers re-evaluation.
 			 */
 			if (rpr_is_defined(winstate))
 			{
 				if (winstate->rpSkipTo == ST_NEXT_ROW)
-					clear_reduced_frame_map(winstate);
+					clear_reduced_frame(winstate);
 			}
 
 			/*
@@ -2986,9 +2958,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 			name = te->resname;
 			expr = te->expr;
 
-#ifdef RPR_DEBUG
-			printf("defineVariable name: %s\n", name);
-#endif
 			winstate->defineVariableList =
 				lappend(winstate->defineVariableList,
 						makeString(pstrdup(name)));
@@ -3974,8 +3943,6 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 	WindowAggState *winstate = winobj->winstate;
 	int			state;
 	int			rtn;
-	int64		i;
-	int			num_reduced_rows;
 
 	if (!rpr_is_defined(winstate))
 	{
@@ -3984,14 +3951,10 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 		 * window frame.
 		 */
 		rtn = 0;
-#ifdef RPR_DEBUG
-		printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
-			   rtn, pos);
-#endif
 		return rtn;
 	}
 
-	state = get_reduced_frame_map(winstate, pos);
+	state = get_reduced_frame_status(winstate, pos);
 
 	if (state == RF_NOT_DETERMINED)
 	{
@@ -3999,16 +3962,12 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 		update_reduced_frame(winobj, pos);
 	}
 
-	state = get_reduced_frame_map(winstate, pos);
+	state = get_reduced_frame_status(winstate, pos);
 
 	switch (state)
 	{
 		case RF_FRAME_HEAD:
-			num_reduced_rows = 1;
-			for (i = pos + 1;
-				 get_reduced_frame_map(winstate, i) == RF_SKIPPED; i++)
-				num_reduced_rows++;
-			rtn = num_reduced_rows;
+			rtn = winstate->rpr_match_length;
 			break;
 
 		case RF_SKIPPED:
@@ -4016,6 +3975,7 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 			break;
 
 		case RF_UNMATCHED:
+		case RF_EMPTY_MATCH:
 			rtn = -1;
 			break;
 
@@ -4025,91 +3985,56 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 			break;
 	}
 
-#ifdef RPR_DEBUG
 	return rtn;
 }
 
-#define REDUCED_FRAME_MAP_INIT_SIZE	1024L
-
-/*
- * create_reduced_frame_map
- * Create reduced frame map
- */
-static void
-create_reduced_frame_map(WindowAggState *winstate)
-{
-	winstate->reduced_frame_map =
-		MemoryContextAlloc(winstate->partcontext,
-						   REDUCED_FRAME_MAP_INIT_SIZE);
-	winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE;
-	clear_reduced_frame_map(winstate);
-}
-
 /*
- * clear_reduced_frame_map
- * Clear reduced frame map
+ * clear_reduced_frame
+ * Clear reduced frame status
  */
 static void
-clear_reduced_frame_map(WindowAggState *winstate)
+clear_reduced_frame(WindowAggState *winstate)
 {
-	Assert(winstate->reduced_frame_map != NULL);
-	MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
-		   winstate->alloc_sz);
+	winstate->rpr_match_valid = false;
+	winstate->rpr_match_matched = false;
+	winstate->rpr_match_start = -1;
+	winstate->rpr_match_length = 0;
 }
 
 /*
- * get_reduced_frame_map
- * Get reduced frame map specified by pos
+ * get_reduced_frame_status
+ *		Look up a position against the current match.
+ *
+ * Returns one of the RF_* constants:
+ *   RF_NOT_DETERMINED  pos has not been processed yet
+ *   RF_FRAME_HEAD      pos is the start of the current match
+ *   RF_SKIPPED         pos is inside the current match but not the start
+ *   RF_UNMATCHED       pos is processed but not part of any match
  */
 static int
-get_reduced_frame_map(WindowAggState *winstate, int64 pos)
+get_reduced_frame_status(WindowAggState *winstate, int64 pos)
 {
-	Assert(winstate->reduced_frame_map != NULL);
-	Assert(pos >= 0);
+	int64		start = winstate->rpr_match_start;
+	int64		length = winstate->rpr_match_length;
 
-	/*
-	 * If pos is not in the reduced frame map, it means that any info
-	 * regarding the pos has not been registered yet. So we return
-	 * RF_NOT_DETERMINED.
-	 */
-	if (pos >= winstate->alloc_sz)
+	if (!winstate->rpr_match_valid)
 		return RF_NOT_DETERMINED;
 
-	return winstate->reduced_frame_map[pos];
-}
-
-/*
- * register_reduced_frame_map
- * Add/replace reduced frame map member at pos.
- * If there's no enough space, expand the map.
- */
-static void
-register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val)
-{
-	int64		realloc_sz;
-
-	Assert(winstate->reduced_frame_map != NULL);
+	/* Empty match: covers only the start position */
+	if (pos == start && winstate->rpr_match_matched && length == 0)
+		return RF_EMPTY_MATCH;
 
-	if (pos < 0)
-		elog(ERROR, "wrong pos: " INT64_FORMAT, pos);
-
-	while (pos > winstate->alloc_sz - 1)
-	{
-		realloc_sz = winstate->alloc_sz * 2;
-
-		winstate->reduced_frame_map =
-			repalloc(winstate->reduced_frame_map, realloc_sz);
+	/* Outside the result range */
+	if (pos < start || pos >= start + length)
+		return RF_NOT_DETERMINED;
 
-		MemSet(winstate->reduced_frame_map + winstate->alloc_sz,
-			   RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz);
+	if (!winstate->rpr_match_matched)
+		return RF_UNMATCHED;
 
+	if (pos == start)
+		return RF_FRAME_HEAD;
 
+	return RF_SKIPPED;
 }
 
 /*
@@ -4156,7 +4081,11 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	if (winstate->nfaContext != NULL &&
 		pos < winstate->nfaContext->matchStartRow)
 	{
-		register_reduced_frame_map(winstate, pos, RF_UNMATCHED);
+		/* already processed, unmatched */
+		winstate->rpr_match_valid = true;
+		winstate->rpr_match_matched = false;
+		winstate->rpr_match_start = pos;
+		winstate->rpr_match_length = 1;
 		return;
 	}
 
@@ -4173,7 +4102,11 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		 */
 		if (pos <= winstate->nfaLastProcessedRow)
 		{
-			register_reduced_frame_map(winstate, pos, RF_UNMATCHED);
+			/* already processed, unmatched */
+			winstate->rpr_match_valid = true;
+			winstate->rpr_match_matched = false;
+			winstate->rpr_match_start = pos;
+			winstate->rpr_match_length = 1;
 			return;
 		}
 		/* Not yet processed - create new context and start fresh */
@@ -4245,26 +4178,38 @@ register_result:
 	Assert(pos == targetCtx->matchStartRow);
 
 	/*
-	 * Register reduced frame map based on match result.
+	 * Record match result.
 	 */
+	winstate->rpr_match_valid = true;
+	winstate->rpr_match_start = targetCtx->matchStartRow;
+
 	if (targetCtx->matchEndRow < targetCtx->matchStartRow)
 	{
 		matchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1;
 
-		register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_UNMATCHED);
-		ExecRPRRecordContextFailure(winstate, matchLen);
+		if (targetCtx->matchedState != NULL)
+		{
+			/* Empty match: FIN reached but 0 rows consumed */
+			winstate->rpr_match_matched = true;
+			winstate->rpr_match_length = 0;
+			ExecRPRRecordContextSuccess(winstate, 0);
+		}
+		else
+		{
+			/* No match */
+			winstate->rpr_match_matched = false;
+			winstate->rpr_match_length = 1;
+			ExecRPRRecordContextFailure(winstate, matchLen);
+		}
 		ExecRPRFreeContext(winstate, targetCtx);
 		return;
 	}
 
-	/* Match succeeded - register frame map and record statistics */
+	/* Match succeeded */
 	matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1;
 
-	register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD);
-	for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++)
-	{
-		register_reduced_frame_map(winstate, i, RF_SKIPPED);
-	}
+	winstate->rpr_match_matched = true;
+	winstate->rpr_match_length = matchLen;
 	ExecRPRRecordContextSuccess(winstate, matchLen);
 
 	/* Remove the matched context */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 33028c3f10b..c672d29f35b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2499,10 +2499,12 @@ typedef enum WindowAggStatus
 									 * tuples during spool */
 } WindowAggStatus;
 
-#define	RF_NOT_DETERMINED	0
-#define	RF_FRAME_HEAD		1
-#define	RF_SKIPPED			2
-#define	RF_UNMATCHED		3
+/* RPR reduced frame states returned by get_reduced_frame_status() */
+#define	RF_NOT_DETERMINED	0	/* not yet processed */
+#define	RF_FRAME_HEAD		1	/* start row of a match */
+#define	RF_SKIPPED			2	/* interior row of a match */
+#define	RF_UNMATCHED		3	/* no match at this row */
+#define	RF_EMPTY_MATCH		4	/* empty match (0 rows); treated as unmatched */
 
 /*
  * RPRNFAState - single NFA state for pattern matching
@@ -2694,12 +2696,11 @@ typedef struct WindowAggState
 	TupleTableSlot *next_slot;	/* NEXT row navigation operator */
 	TupleTableSlot *null_slot;	/* all NULL slot */
 
-	/*
-	 * Each byte corresponds to a row positioned at absolute its pos in
-	 * partition.  See above definition for RF_*. Used for RPR.
-	 */
-	char	   *reduced_frame_map;
-	int64		alloc_sz;		/* size of the map */
+	/* RPR current match result */
+	bool		rpr_match_valid;	/* true if a match result is set */
+	bool		rpr_match_matched;	/* true if the result was a match */
+	int64		rpr_match_start;	/* start position of the match result */
+	int64		rpr_match_length;	/* number of rows matched (0 = empty) */
 } WindowAggState;
 
 /* ----------------
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index bd345906133..79cbc246039 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3348,8 +3348,8 @@ WINDOW w AS (
    Pattern: ((a' b')+" c)*
    Storage: Memory  Maximum Storage: NkB
    NFA States: 9 peak, 178 total, 0 merged
-   NFA Contexts: 4 peak, 61 total, 22 pruned
-   NFA: 1 matched (len 57/57/57.0), 0 mismatched
+   NFA Contexts: 4 peak, 61 total, 20 pruned
+   NFA: 3 matched (len 0/57/19.0), 0 mismatched
    NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
@@ -3385,8 +3385,8 @@ WINDOW w AS (
    Pattern: (a (b c)+)*
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 160 total, 0 merged
-   NFA Contexts: 4 peak, 61 total, 22 pruned
-   NFA: 1 matched (len 57/57/57.0), 0 mismatched
+   NFA Contexts: 4 peak, 61 total, 20 pruned
+   NFA: 3 matched (len 0/57/19.0), 0 mismatched
    NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
-- 
2.50.1 (Apple Git-155)

From 87986f98416ac0d145d654c65c5c8530ada78f3f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 21:47:01 +0900
Subject: [PATCH] Add fixed-length group absorption for RPR

Extend context absorption to unbounded groups with fixed-length
children (min == max, recursively).  Patterns like (A B{2})+ or
((A (B C){2}){2})+ are now absorbable, equivalent to unrolling to
{1,1} VARs at compile time without actually unrolling.

isFixedLengthChildren() recursively verifies min == max for all
children including nested subgroups, extending the existing Case 2
in isUnboundedStart().

Absorption comparison in nfa_states_covered requires states to be at
an ABSORBABLE judgment point, where count-dominance is guaranteed.
The inline advance in nfa_match is generalized to advance bounded
VARs within the absorbable region through END chains to reach the
judgment point.

Fix isAbsorbable propagation in nfa_advance_var and nfa_advance_end
exit paths, where reusing a state object skipped recomputation.

Mark VAR elements in the DFS visited bitmap at nfa_add_state_unique
instead of at nfa_advance_state entry, so that loop-back through ALT
to the same VAR is not incorrectly blocked by cycle detection.
---
 src/backend/executor/execRPR.c            | 110 ++++++--
 src/backend/optimizer/plan/rpr.c          | 136 +++++++--
 src/test/regress/expected/rpr_base.out    |  83 +++++-
 src/test/regress/expected/rpr_explain.out | 206 +++++++++++++-
 src/test/regress/expected/rpr_nfa.out     | 321 ++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql         |  36 +++
 src/test/regress/sql/rpr_explain.sql      | 114 ++++++++
 src/test/regress/sql/rpr_nfa.sql          | 168 +++++++++++
 8 files changed, 1111 insertions(+), 63 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1cbe8e14780..8f0457e2b3c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1760,6 +1760,10 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	RPRNFAState *s;
 	RPRNFAState *tail = NULL;
 
+	/* Mark VAR in visited before duplicate check to prevent DFS loops */
+	winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
+		((bitmapword) 1 << BITNUM(state->elemIdx));
+
 	/* Check for duplicate and find tail */
 	for (s = ctx->states; s != NULL; s = s->next)
 	{
@@ -2033,6 +2037,14 @@ nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *new
 		elem = &pattern->elements[newerState->elemIdx];
 		depth = elem->depth;
 
+		/*
+		 * Only compare at absorption judgment points (RPR_ELEM_ABSORBABLE).
+		 * Judgment points are where count-dominance guarantees the newer
+		 * context's future matches are a subset of the older's.
+		 */
+		if (!RPRElemIsAbsorbable(elem))
+			return false;
+
 		for (olderState = older->states; olderState != NULL; olderState = olderState->next)
 		{
 			CHECK_FOR_INTERRUPTS();
@@ -2175,9 +2187,10 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
  *   - not matched: remove state (exit alternatives already exist from
  *     previous advance when count >= min was satisfied)
  *
- * For simple VARs (min=max=1) followed by END:
- *   - Advance to END and update group count before absorb phase
- *   - This ensures absorption can compare states by group completion
+ * For VARs that reached max count followed by END:
+ *   - Advance through END chain to reach absorption judgment point
+ *   - Only deterministic exits (count >= max, max != INF) are handled
+ *   - Chains through END elements while count >= max (must-exit path)
  *
  * Non-VAR elements (ALT, END, FIN) are kept as-is for advance phase.
  */
@@ -2191,9 +2204,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 	RPRNFAState *nextState;
 
 	/*
-	 * Evaluate VAR elements against current row. For simple VARs with END
-	 * next, advance to END and update group count inline so absorb phase can
-	 * compare states properly.
+	 * Evaluate VAR elements against current row. For VARs that reach max
+	 * count with END next, advance through END chain inline so absorb phase
+	 * can compare states at judgment points.
 	 */
 	for (state = ctx->states; state != NULL; state = nextState)
 	{
@@ -2223,34 +2236,61 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 				state->counts[depth] = count;
 
 				/*
-				 * For simple VAR (min=max=1) with END next, advance to END
-				 * and update group count inline. This keeps state in place,
-				 * preserving lexical order.
+				 * For VAR at max count with END next, advance through END
+				 * chain to reach the absorption judgment point. Only
+				 * deterministic exits (count >= max, max finite) are handled;
+				 * unbounded VARs stay for advance phase.
 				 */
-				if (elem->min == 1 && elem->max == 1 &&
+				if (RPRElemIsAbsorbableBranch(elem) &&
+					!RPRElemIsAbsorbable(elem) &&
+					count >= elem->max &&
 					RPRElemIsEnd(&elements[elem->next]))
 				{
 					RPRPatternElement *endElem = &elements[elem->next];
 					int			endDepth = endElem->depth;
 					int32		endCount = state->counts[endDepth];
 
-					Assert(count == 1);
-
-					/* Increment group count with overflow protection */
+					/* Increment group count */
 					if (endCount < RPR_COUNT_MAX)
 						endCount++;
-
-					/*
-					 * END's max can never be exceeded here because
-					 * nfa_advance_end only loops when count < max, so
-					 * endCount entering inline advance is at most max-1, and
-					 * incrementing yields at most max.
-					 */
 					Assert(endElem->max == RPR_QUANTITY_INF ||
 						   endCount <= endElem->max);
 
 					state->elemIdx = elem->next;
 					state->counts[endDepth] = endCount;
+
+					/*
+					 * Chain through END elements within the absorbable region
+					 * (ABSORBABLE_BRANCH) until reaching the judgment point
+					 * (ABSORBABLE).  Continue only on must-exit path (count
+					 * >= max) with END next.
+					 */
+					while (RPRElemIsAbsorbableBranch(endElem) &&
+						   !RPRElemIsAbsorbable(endElem) &&
+						   endCount >= endElem->max &&
+						   RPRElemIsEnd(&elements[endElem->next]))
+					{
+						RPRPatternElement *outerEnd = &elements[endElem->next];
+						int			outerDepth = outerEnd->depth;
+						int32		outerCount = state->counts[outerDepth];
+
+						/* Reset exited group's count */
+						state->counts[endDepth] = 0;
+
+						/* Increment outer group count */
+						if (outerCount < RPR_COUNT_MAX)
+							outerCount++;
+						Assert(outerEnd->max == RPR_QUANTITY_INF ||
+							   outerCount <= outerEnd->max);
+
+						state->elemIdx = endElem->next;
+						state->counts[outerDepth] = outerCount;
+
+						/* Advance to next END in chain */
+						endElem = outerEnd;
+						endDepth = outerDepth;
+						endCount = outerCount;
+					}
 				}
 				/* else: stay at VAR for advance phase */
 			}
@@ -2468,6 +2508,10 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
 
+		/* Update isAbsorbable for target element (monotonic) */
+		state->isAbsorbable = state->isAbsorbable &&
+			RPRElemIsAbsorbableBranch(nextElem);
+
 		/* END->END: increment outer END's count */
 		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
 			state->counts[nextElem->depth]++;
@@ -2621,6 +2665,13 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			state->elemIdx = elem->next;
 			nextElem = &elements[state->elemIdx];
 
+			/*
+			 * Update isAbsorbable for target element (monotonic: AND
+			 * preserves false)
+			 */
+			state->isAbsorbable = state->isAbsorbable &&
+				RPRElemIsAbsorbableBranch(nextElem);
+
 			/*
 			 * When exiting directly to an outer END, increment its iteration
 			 * count.  Simple VARs (min=max=1) handle this via inline advance
@@ -2650,6 +2701,13 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
 
+		/*
+		 * Update isAbsorbable for target element (monotonic: AND preserves
+		 * false)
+		 */
+		state->isAbsorbable = state->isAbsorbable &&
+			RPRElemIsAbsorbableBranch(nextElem);
+
 		/* See comment above: increment outer END count for quantified VARs */
 		if (RPRElemIsEnd(nextElem))
 		{
@@ -2686,11 +2744,19 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
 		nfa_state_free(winstate, state);
 		return;
 	}
-	winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
-		((bitmapword) 1 << BITNUM(state->elemIdx));
 
 	elem = &pattern->elements[state->elemIdx];
 
+	/*
+	 * Mark epsilon elements (END, ALT, BEGIN, FIN) in visited to prevent
+	 * infinite epsilon cycles.  VAR elements are marked later when added to
+	 * the state list (nfa_add_state_unique), allowing legitimate loop-back to
+	 * the same VAR in a new iteration.
+	 */
+	if (!RPRElemIsVar(elem))
+		winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
+			((bitmapword) 1 << BITNUM(state->elemIdx));
+
 	switch (elem->varId)
 	{
 		case RPR_VARID_FIN:
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2728a0b9fca..c0e9d134aa9 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -92,6 +92,8 @@ static bool fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
 static void finalizeRPRPattern(RPRPattern *result);
 
 /* Forward declarations - context absorption */
+static bool isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx,
+								  RPRDepth scopeDepth);
 static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
 static void computeAbsorbabilityRecursive(RPRPattern *pattern,
 										  RPRElemIdx startIdx,
@@ -1524,6 +1526,70 @@ finalizeRPRPattern(RPRPattern *result)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * isFixedLengthChildren
+ *		Check if all children at scopeDepth have fixed-length quantifiers
+ *		(min == max), recursively for nested subgroups.
+ *
+ * A fixed-length group is semantically equivalent to unrolling each child
+ * to {1,1} copies, which is the existing Case 2 already proven correct
+ * for absorption.  This check recognizes fixed-length groups at compile
+ * time without actually unrolling them.
+ *
+ * Traverses the flat element array starting at idx.  For VAR elements,
+ * checks min == max.  For BEGIN elements (nested subgroups), recurses
+ * into the subgroup and also checks the subgroup's END quantifier.
+ * ALT elements are rejected (alternation inside absorbable group is
+ * not supported).
+ *
+ * Returns true if all children are fixed-length, stopping at the END
+ * element at scopeDepth - 1.
+ */
+static bool
+isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx, RPRDepth scopeDepth)
+{
+	RPRPatternElement *e = &pattern->elements[idx];
+
+	check_stack_depth();
+
+	while (e->depth == scopeDepth)
+	{
+		if (RPRElemIsVar(e))
+		{
+			if (e->min != e->max)
+				return false;
+		}
+		else if (RPRElemIsBegin(e))
+		{
+			RPRElemIdx	childIdx = e->next;
+
+			/* Recurse into subgroup children at scopeDepth + 1 */
+			if (!isFixedLengthChildren(pattern, childIdx, scopeDepth + 1))
+				return false;
+
+			/* Advance past the subgroup to its END element */
+			e = &pattern->elements[e->next];
+			while (e->depth > scopeDepth)
+				e = &pattern->elements[e->next];
+
+			/* e is now the END at scopeDepth; check its quantifier */
+			Assert(RPRElemIsEnd(e) && e->depth == scopeDepth);
+			if (e->min != e->max)
+				return false;
+		}
+		else
+		{
+			/* ALT inside group: not supported for absorption */
+			return false;
+		}
+
+		Assert(e->next != RPR_ELEMIDX_INVALID);
+		e = &pattern->elements[e->next];
+	}
+
+	return true;
+}
+
 /*
  * isUnboundedStart
  *		Check if the element at idx starts an unbounded greedy sequence.
@@ -1533,29 +1599,31 @@ finalizeRPRPattern(RPRPattern *result)
  *   - Greedy (not reluctant)
  *   - At the start of current scope
  *
- * Algorithm:
- *   - Traverse elements within current scope (parentDepth to startDepth)
- *   - For GROUP: must be unbounded greedy AND contain only simple {1,1} VARs
- *   - Sets ABSORBABLE and ABSORBABLE_BRANCH flags on matching elements
- *
  * Two cases are handled:
  *   1. Simple VAR: A+ B C - A has max=INF, gets both flags
- *   2. Group: (A B)+ C - END has max=INF, all children are {1,1} VARs
- *      A,B,END get ABSORBABLE_BRANCH, only END gets ABSORBABLE
+ *   2. Unbounded GROUP with fixed-length children: (A B{2})+ C
+ *      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).
+ *      Examples:
+ *        (A B{2})+ C          - B{2} has min==max, step=3
+ *        (A (B C){2} D)+ E    - nested {2} subgroup, step=6
+ *        ((A (B C){2}){2})+   - doubly nested {2}, step=10
+ *        (A ((B C{3}){2} D){2} E)+ F  - deep nesting, step=20
  *
  * Returns false for patterns where absorption cannot work:
  *   - A B+ (unbounded not at start)
  *   - A+? B (reluctant quantifier)
  *   - (A | B)+ (ALT inside group)
- *   - (A B+)+ (unbounded element inside group)
- *   - ((A B)+ C)+ (nested unbounded groups)
+ *   - (A B+)+ (variable-length element inside group)
+ *   - (A B{2,5})+ (min != max inside group)
  */
 static bool
 isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
 {
 	RPRPatternElement *elem = &pattern->elements[idx];
 	RPRDepth	startDepth = elem->depth;
-	RPRPatternElement *nextElem;
 	RPRPatternElement *e;
 
 	/* Case 1: Simple unbounded VAR at start (greedy only) */
@@ -1568,21 +1636,19 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
 	}
 
 	/*
-	 * Case 2: Unbounded GROUP - traverse siblings at startDepth and check if
-	 * they're all simple {1,1} VARs, then check if END at startDepth - 1 is
-	 * unbounded greedy.
+	 * Case 2: Unbounded GROUP with fixed-length children.  Each child must
+	 * have min == max (recursively for nested subgroups), ensuring a fixed
+	 * step size per iteration so that count-dominance holds.
 	 */
-	for (e = elem; e->depth == startDepth; e = nextElem)
-	{
-		/* Must be simple {1,1} VAR */
-		if (!RPRElemIsVar(e) || e->min != 1 || e->max != 1)
-			return false;
+	if (!isFixedLengthChildren(pattern, idx, startDepth))
+		return false;
 
-		Assert(e->next != RPR_ELEMIDX_INVALID);
-		nextElem = &pattern->elements[e->next];
-	}
+	/* Find the END element at startDepth - 1 */
+	e = &pattern->elements[idx];
+	while (e->depth >= startDepth)
+		e = &pattern->elements[e->next];
 
-	/* Now e should be END at startDepth - 1 */
+	/* END must be unbounded greedy */
 	if (e->depth == startDepth - 1 &&
 		RPRElemIsEnd(e) && e->max == RPR_QUANTITY_INF &&
 		!RPRElemIsReluctant(e))
@@ -1590,7 +1656,8 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
 		Assert(e->jump == idx); /* END points back to first child */
 
 		/* Set ABSORBABLE_BRANCH on all children, ABSORBABLE on END only */
-		for (e = elem; !RPRElemIsEnd(e); e = &pattern->elements[e->next])
+		for (e = elem; !RPRElemIsEnd(e) || e->depth >= startDepth;
+			 e = &pattern->elements[e->next])
 			e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
 		e->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
 		return true;
@@ -1654,12 +1721,25 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
 	}
 	else if (RPRElemIsBegin(elem))
 	{
-		/* BEGIN: skip to first child and check that */
-		computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
-
-		/* Mark BEGIN element if contents are absorbable */
-		if (*hasAbsorbable)
+		/*
+		 * BEGIN: first try to treat this BEGIN's children as an unbounded
+		 * group directly (handles nested fixed-length groups like ((A{2}
+		 * B{3}){2})+).  If that fails, skip to first child and recurse as
+		 * before.
+		 */
+		if (isUnboundedStart(pattern, elem->next))
+		{
+			*hasAbsorbable = true;
 			elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+		}
+		else
+		{
+			computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
+
+			/* Mark BEGIN element if contents are absorbable */
+			if (*hasAbsorbable)
+				elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+		}
 	}
 	else
 	{
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 3168468d0ae..7452cf1b3ab 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3312,7 +3312,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 -------------------------------------------------------------------------------
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a{2}){2,}
+   Pattern: (a{2}'){2,}"
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
@@ -4095,6 +4095,87 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
+-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}"
+-- All children have min == max, equivalent to unrolling to {1,1}
+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{2} B{3})+)
+             DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{2}' b{3}')+"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable
+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 (B C){2} D)+)
+             DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val > 60);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' (b' c'){2}' d')+"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable
+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{2} B{3}){2})+)
+             DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a{2}' b{3}'){2}')+"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max)
+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 B{2,5})+)
+             DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b{2,5})+
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max)
+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 B?)+)
+             DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b?)+
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
 -- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 79cbc246039..560f21f44c2 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -462,10 +462,10 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+"
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 2 peak, 61 total, 0 pruned
+   NFA States: 4 peak, 91 total, 0 merged
+   NFA Contexts: 3 peak, 61 total, 0 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
-   NFA: 29 absorbed (len 1/1/1.0), 30 skipped (len 1/1/1.0)
+   NFA: 29 absorbed (len 2/2/2.0), 30 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
@@ -904,6 +904,188 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
+-- Fixed-length group absorption: (A B B)+ C
+-- B B merged to B{2}; absorbable with fixed-length check
+-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
+CREATE VIEW rpr_ev20b AS
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B B)+ C)
+    DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line           
+-------------------------
+   PATTERN ((a b b)+ c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B B)+ C)
+    DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=70.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' b{2}')+" c
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 91 total, 0 merged
+   NFA Contexts: 4 peak, 71 total, 40 pruned
+   NFA: 10 matched (len 7/7/7.0), 0 mismatched
+   NFA: 10 absorbed (len 3/3/3.0), 10 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=70.00 loops=1)
+(9 rows)
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
+CREATE VIEW rpr_ev20c AS
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A (B C){2} D)+ E)
+    DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+           C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+           E AS v % 13 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line              
+--------------------------------
+   PATTERN ((a (b c){2} d)+ e) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A (B C){2} D)+ E)
+    DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+           C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+           E AS v % 13 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=65.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' (b' c'){2}' d')+" e
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 76 total, 0 merged
+   NFA Contexts: 4 peak, 66 total, 50 pruned
+   NFA: 5 matched (len 13/13/13.0), 0 mismatched
+   NFA: 5 absorbed (len 6/6/6.0), 5 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=65.00 loops=1)
+(9 rows)
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
+CREATE VIEW rpr_ev20d AS
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A ((B C C C){2} D){2} E)+ F)
+    DEFINE A AS v % 41 IN (1, 21),
+           B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+           C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+                           23,24,25, 27,28,29, 32,33,34, 36,37,38),
+           D AS v % 41 IN (10, 19, 30, 39),
+           E AS v % 41 IN (20, 40),
+           F AS v % 41 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                   line                    
+-------------------------------------------
+   PATTERN ((a ((b c c c){2} d){2} e)+ f) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A ((B C C C){2} D){2} E)+ F)
+    DEFINE A AS v % 41 IN (1, 21),
+           B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+           C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+                           23,24,25, 27,28,29, 32,33,34, 36,37,38),
+           D AS v % 41 IN (10, 19, 30, 39),
+           E AS v % 41 IN (20, 40),
+           F AS v % 41 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=82.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' ((b' c{3}'){2}' d'){2}' e')+" f
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 87 total, 0 merged
+   NFA Contexts: 4 peak, 83 total, 76 pruned
+   NFA: 2 matched (len 41/41/41.0), 0 mismatched
+   NFA: 2 absorbed (len 20/20/20.0), 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=82.00 loops=1)
+(9 rows)
+
+-- 3-level END chain absorption: ((A (B C){2}){2})+
+-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
+-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
+CREATE VIEW rpr_ev20e AS
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A (B C){2}){2})+)
+    DEFINE A AS v % 21 IN (1, 6, 11, 16),
+           B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+           C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line               
+---------------------------------
+   PATTERN (((a (b c){2}){2})+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A (B C){2}){2})+)
+    DEFINE A AS v % 21 IN (1, 6, 11, 16),
+           B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+           C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=42.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' (b' c'){2}'){2}')+"
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 47 total, 0 merged
+   NFA Contexts: 5 peak, 43 total, 30 pruned
+   NFA: 2 matched (len 20/20/20.0), 0 mismatched
+   NFA: 2 absorbed (len 10/10/10.0), 8 skipped (len 1/5/3.0)
+   ->  Function Scan on generate_series s (actual rows=42.00 loops=1)
+(9 rows)
+
 -- ============================================================
 -- Match Length Statistics Tests
 -- ============================================================
@@ -1894,10 +2076,10 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b' c')+"
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 3 peak, 81 total, 0 merged
-   NFA Contexts: 3 peak, 61 total, 20 pruned
+   NFA States: 4 peak, 81 total, 0 merged
+   NFA Contexts: 4 peak, 61 total, 20 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
-   NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
+   NFA: 19 absorbed (len 3/3/3.0), 20 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
@@ -2461,7 +2643,7 @@ WINDOW w AS (
    NFA States: 4 peak, 102 total, 0 merged
    NFA Contexts: 2 peak, 41 total, 10 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
-   NFA: 20 absorbed (len 1/1/1.0), 0 skipped
+   NFA: 10 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
@@ -3158,10 +3340,10 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+"
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 3 peak, 61 total, 0 merged
-   NFA Contexts: 2 peak, 41 total, 0 pruned
+   NFA States: 4 peak, 61 total, 0 merged
+   NFA Contexts: 3 peak, 41 total, 0 pruned
    NFA: 1 matched (len 40/40/40.0), 0 mismatched
-   NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
+   NFA: 19 absorbed (len 2/2/2.0), 20 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
@@ -3234,12 +3416,12 @@ WINDOW w AS (
 ----------------------------------------------------------------------
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: ((a b){2})+
+   Pattern: ((a' b'){2}')+"
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 76 total, 0 merged
    NFA Contexts: 4 peak, 61 total, 15 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
-   NFA: 0 absorbed, 44 skipped (len 1/4/2.3)
+   NFA: 14 absorbed (len 4/4/4.0), 30 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 7b5a17fb671..250f7f131b1 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -447,6 +447,327 @@ WINDOW w AS (
   7 | {X}   |             |          
 (7 rows)
 
+-- Fixed-length group absorption: (A B{2})+ C
+-- B{2} has min == max, equivalent to unrolling to (A B B)+ C
+WITH test_absorb_fixedlen AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),
+        (2,  ARRAY['B']),
+        (3,  ARRAY['B']),
+        (4,  ARRAY['A']),
+        (5,  ARRAY['B']),
+        (6,  ARRAY['B']),
+        (7,  ARRAY['A']),
+        (8,  ARRAY['B']),
+        (9,  ARRAY['B']),
+        (10, ARRAY['C']),
+        (11, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_fixedlen
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B{2})+ C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |        10
+  2 | {B}   |             |          
+  3 | {B}   |             |          
+  4 | {A}   |             |          
+  5 | {B}   |             |          
+  6 | {B}   |             |          
+  7 | {A}   |             |          
+  8 | {B}   |             |          
+  9 | {B}   |             |          
+ 10 | {C}   |             |          
+ 11 | {X}   |             |          
+(11 rows)
+
+-- Consecutive vars merged to fixed-length: (A B B)+ -> (A B{2})+
+-- mergeConsecutiveVars produces B{2}; now absorbable with fixed-length check
+WITH test_absorb_consecutive AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),
+        (2,  ARRAY['B']),
+        (3,  ARRAY['B']),
+        (4,  ARRAY['A']),
+        (5,  ARRAY['B']),
+        (6,  ARRAY['B']),
+        (7,  ARRAY['A']),
+        (8,  ARRAY['B']),
+        (9,  ARRAY['B']),
+        (10, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_consecutive
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B B)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         9
+  2 | {B}   |             |          
+  3 | {B}   |             |          
+  4 | {A}   |             |          
+  5 | {B}   |             |          
+  6 | {B}   |             |          
+  7 | {A}   |             |          
+  8 | {B}   |             |          
+  9 | {B}   |             |          
+ 10 | {X}   |             |          
+(10 rows)
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- Inner group {2} has min == max; absorbable via recursive check
+-- step_size = 1 + (1+1)*2 + 1 = 6
+WITH test_absorb_nested_fixedlen AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),
+        (2,  ARRAY['B']),
+        (3,  ARRAY['C']),
+        (4,  ARRAY['B']),
+        (5,  ARRAY['C']),
+        (6,  ARRAY['D']),
+        (7,  ARRAY['A']),
+        (8,  ARRAY['B']),
+        (9,  ARRAY['C']),
+        (10, ARRAY['B']),
+        (11, ARRAY['C']),
+        (12, ARRAY['D']),
+        (13, ARRAY['E']),
+        (14, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_nested_fixedlen
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A (B C){2} D)+ E)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |        13
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {B}   |             |          
+  5 | {C}   |             |          
+  6 | {D}   |             |          
+  7 | {A}   |             |          
+  8 | {B}   |             |          
+  9 | {C}   |             |          
+ 10 | {B}   |             |          
+ 11 | {C}   |             |          
+ 12 | {D}   |             |          
+ 13 | {E}   |             |          
+ 14 | {X}   |             |          
+(14 rows)
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; 2 iterations + F = 41 rows
+WITH test_absorb_doubly_nested AS (
+    SELECT v AS id, ARRAY[
+        CASE
+            WHEN v % 41 IN (1, 21)  THEN 'A'
+            WHEN v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35) THEN 'B'
+            WHEN v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+                            23,24,25, 27,28,29, 32,33,34, 36,37,38) THEN 'C'
+            WHEN v % 41 IN (10, 19, 30, 39) THEN 'D'
+            WHEN v % 41 IN (20, 40) THEN 'E'
+            WHEN v % 41 = 0 THEN 'F'
+            ELSE 'X'
+        END
+    ] AS flags
+    FROM generate_series(1, 82) AS s(v)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_doubly_nested
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A ((B C C C){2} D){2} E)+ F)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags),
+        F AS 'F' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |        41
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {C}   |             |          
+  5 | {C}   |             |          
+  6 | {B}   |             |          
+  7 | {C}   |             |          
+  8 | {C}   |             |          
+  9 | {C}   |             |          
+ 10 | {D}   |             |          
+ 11 | {B}   |             |          
+ 12 | {C}   |             |          
+ 13 | {C}   |             |          
+ 14 | {C}   |             |          
+ 15 | {B}   |             |          
+ 16 | {C}   |             |          
+ 17 | {C}   |             |          
+ 18 | {C}   |             |          
+ 19 | {D}   |             |          
+ 20 | {E}   |             |          
+ 21 | {A}   |             |          
+ 22 | {B}   |             |          
+ 23 | {C}   |             |          
+ 24 | {C}   |             |          
+ 25 | {C}   |             |          
+ 26 | {B}   |             |          
+ 27 | {C}   |             |          
+ 28 | {C}   |             |          
+ 29 | {C}   |             |          
+ 30 | {D}   |             |          
+ 31 | {B}   |             |          
+ 32 | {C}   |             |          
+ 33 | {C}   |             |          
+ 34 | {C}   |             |          
+ 35 | {B}   |             |          
+ 36 | {C}   |             |          
+ 37 | {C}   |             |          
+ 38 | {C}   |             |          
+ 39 | {D}   |             |          
+ 40 | {E}   |             |          
+ 41 | {F}   |             |          
+ 42 | {A}   |          42 |        82
+ 43 | {B}   |             |          
+ 44 | {C}   |             |          
+ 45 | {C}   |             |          
+ 46 | {C}   |             |          
+ 47 | {B}   |             |          
+ 48 | {C}   |             |          
+ 49 | {C}   |             |          
+ 50 | {C}   |             |          
+ 51 | {D}   |             |          
+ 52 | {B}   |             |          
+ 53 | {C}   |             |          
+ 54 | {C}   |             |          
+ 55 | {C}   |             |          
+ 56 | {B}   |             |          
+ 57 | {C}   |             |          
+ 58 | {C}   |             |          
+ 59 | {C}   |             |          
+ 60 | {D}   |             |          
+ 61 | {E}   |             |          
+ 62 | {A}   |             |          
+ 63 | {B}   |             |          
+ 64 | {C}   |             |          
+ 65 | {C}   |             |          
+ 66 | {C}   |             |          
+ 67 | {B}   |             |          
+ 68 | {C}   |             |          
+ 69 | {C}   |             |          
+ 70 | {C}   |             |          
+ 71 | {D}   |             |          
+ 72 | {B}   |             |          
+ 73 | {C}   |             |          
+ 74 | {C}   |             |          
+ 75 | {C}   |             |          
+ 76 | {B}   |             |          
+ 77 | {C}   |             |          
+ 78 | {C}   |             |          
+ 79 | {C}   |             |          
+ 80 | {D}   |             |          
+ 81 | {E}   |             |          
+ 82 | {F}   |             |          
+(82 rows)
+
+-- 3-level END chain: ((A (B C){2}){2})+
+-- Tests END(BC{2}) -> END(A..{2}) -> END(+) chaining
+-- 2 iterations of +, each 10 rows: (A B C B C)(A B C B C)
+WITH test_absorb_3level_end AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),  -- 1st + iter, 1st {2}, A
+        (2,  ARRAY['B']),
+        (3,  ARRAY['C']),
+        (4,  ARRAY['B']),
+        (5,  ARRAY['C']),  -- 1st (BC){2} done
+        (6,  ARRAY['A']),  -- 1st + iter, 2nd {2}, A
+        (7,  ARRAY['B']),
+        (8,  ARRAY['C']),
+        (9,  ARRAY['B']),
+        (10, ARRAY['C']),  -- 2nd (BC){2} done, 1st {2} done, 1st + iter done
+        (11, ARRAY['A']),  -- 2nd + iter, 1st {2}, A
+        (12, ARRAY['B']),
+        (13, ARRAY['C']),
+        (14, ARRAY['B']),
+        (15, ARRAY['C']),
+        (16, ARRAY['A']),  -- 2nd + iter, 2nd {2}, A
+        (17, ARRAY['B']),
+        (18, ARRAY['C']),
+        (19, ARRAY['B']),
+        (20, ARRAY['C']),  -- 2nd + iter done
+        (21, ARRAY['X'])   -- no match, + ends
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_3level_end
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A (B C){2}){2})+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |        20
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {B}   |             |          
+  5 | {C}   |             |          
+  6 | {A}   |             |          
+  7 | {B}   |             |          
+  8 | {C}   |             |          
+  9 | {B}   |             |          
+ 10 | {C}   |             |          
+ 11 | {A}   |             |          
+ 12 | {B}   |             |          
+ 13 | {C}   |             |          
+ 14 | {B}   |             |          
+ 15 | {C}   |             |          
+ 16 | {A}   |             |          
+ 17 | {B}   |             |          
+ 18 | {C}   |             |          
+ 19 | {B}   |             |          
+ 20 | {C}   |             |          
+ 21 | {X}   |             |          
+(21 rows)
+
 -- Multiple unbounded: A+ B+ (first element unbounded enables absorption)
 WITH test_multi_unbounded AS (
     SELECT * FROM (VALUES
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index cf6c062ae85..8c23c7598a3 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2600,6 +2600,42 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ B | A B)*)
              DEFINE A AS val <= 50, B AS val > 50);
 
+-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}"
+-- All children have min == max, equivalent to unrolling to {1,1}
+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{2} B{3})+)
+             DEFINE A AS val <= 50, B AS val > 50);
+
+-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable
+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 (B C){2} D)+)
+             DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val > 60);
+
+-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable
+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{2} B{3}){2})+)
+             DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max)
+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 B{2,5})+)
+             DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max)
+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 B?)+)
+             DEFINE A AS val <= 50, B AS val > 50);
+
 -- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 93e06b0cbdf..237f0366631 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -578,6 +578,120 @@ WINDOW w AS (
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
 
+-- Fixed-length group absorption: (A B B)+ C
+-- B B merged to B{2}; absorbable with fixed-length check
+-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
+CREATE VIEW rpr_ev20b AS
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B B)+ C)
+    DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B B)+ C)
+    DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);');
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
+CREATE VIEW rpr_ev20c AS
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A (B C){2} D)+ E)
+    DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+           C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+           E AS v % 13 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A (B C){2} D)+ E)
+    DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+           C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+           E AS v % 13 = 0
+);');
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
+CREATE VIEW rpr_ev20d AS
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A ((B C C C){2} D){2} E)+ F)
+    DEFINE A AS v % 41 IN (1, 21),
+           B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+           C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+                           23,24,25, 27,28,29, 32,33,34, 36,37,38),
+           D AS v % 41 IN (10, 19, 30, 39),
+           E AS v % 41 IN (20, 40),
+           F AS v % 41 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A ((B C C C){2} D){2} E)+ F)
+    DEFINE A AS v % 41 IN (1, 21),
+           B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+           C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+                           23,24,25, 27,28,29, 32,33,34, 36,37,38),
+           D AS v % 41 IN (10, 19, 30, 39),
+           E AS v % 41 IN (20, 40),
+           F AS v % 41 = 0
+);');
+
+-- 3-level END chain absorption: ((A (B C){2}){2})+
+-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
+-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
+CREATE VIEW rpr_ev20e AS
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A (B C){2}){2})+)
+    DEFINE A AS v % 21 IN (1, 6, 11, 16),
+           B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+           C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A (B C){2}){2})+)
+    DEFINE A AS v % 21 IN (1, 6, 11, 16),
+           B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+           C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);');
+
 -- ============================================================
 -- Match Length Statistics Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 5edcb3357e6..aaa7b44f789 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -346,6 +346,174 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Fixed-length group absorption: (A B{2})+ C
+-- B{2} has min == max, equivalent to unrolling to (A B B)+ C
+WITH test_absorb_fixedlen AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),
+        (2,  ARRAY['B']),
+        (3,  ARRAY['B']),
+        (4,  ARRAY['A']),
+        (5,  ARRAY['B']),
+        (6,  ARRAY['B']),
+        (7,  ARRAY['A']),
+        (8,  ARRAY['B']),
+        (9,  ARRAY['B']),
+        (10, ARRAY['C']),
+        (11, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_fixedlen
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B{2})+ C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- Consecutive vars merged to fixed-length: (A B B)+ -> (A B{2})+
+-- mergeConsecutiveVars produces B{2}; now absorbable with fixed-length check
+WITH test_absorb_consecutive AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),
+        (2,  ARRAY['B']),
+        (3,  ARRAY['B']),
+        (4,  ARRAY['A']),
+        (5,  ARRAY['B']),
+        (6,  ARRAY['B']),
+        (7,  ARRAY['A']),
+        (8,  ARRAY['B']),
+        (9,  ARRAY['B']),
+        (10, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_consecutive
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B B)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- Inner group {2} has min == max; absorbable via recursive check
+-- step_size = 1 + (1+1)*2 + 1 = 6
+WITH test_absorb_nested_fixedlen AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),
+        (2,  ARRAY['B']),
+        (3,  ARRAY['C']),
+        (4,  ARRAY['B']),
+        (5,  ARRAY['C']),
+        (6,  ARRAY['D']),
+        (7,  ARRAY['A']),
+        (8,  ARRAY['B']),
+        (9,  ARRAY['C']),
+        (10, ARRAY['B']),
+        (11, ARRAY['C']),
+        (12, ARRAY['D']),
+        (13, ARRAY['E']),
+        (14, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_nested_fixedlen
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A (B C){2} D)+ E)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags)
+);
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; 2 iterations + F = 41 rows
+WITH test_absorb_doubly_nested AS (
+    SELECT v AS id, ARRAY[
+        CASE
+            WHEN v % 41 IN (1, 21)  THEN 'A'
+            WHEN v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35) THEN 'B'
+            WHEN v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+                            23,24,25, 27,28,29, 32,33,34, 36,37,38) THEN 'C'
+            WHEN v % 41 IN (10, 19, 30, 39) THEN 'D'
+            WHEN v % 41 IN (20, 40) THEN 'E'
+            WHEN v % 41 = 0 THEN 'F'
+            ELSE 'X'
+        END
+    ] AS flags
+    FROM generate_series(1, 82) AS s(v)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_doubly_nested
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A ((B C C C){2} D){2} E)+ F)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags),
+        F AS 'F' = ANY(flags)
+);
+
+-- 3-level END chain: ((A (B C){2}){2})+
+-- Tests END(BC{2}) -> END(A..{2}) -> END(+) chaining
+-- 2 iterations of +, each 10 rows: (A B C B C)(A B C B C)
+WITH test_absorb_3level_end AS (
+    SELECT * FROM (VALUES
+        (1,  ARRAY['A']),  -- 1st + iter, 1st {2}, A
+        (2,  ARRAY['B']),
+        (3,  ARRAY['C']),
+        (4,  ARRAY['B']),
+        (5,  ARRAY['C']),  -- 1st (BC){2} done
+        (6,  ARRAY['A']),  -- 1st + iter, 2nd {2}, A
+        (7,  ARRAY['B']),
+        (8,  ARRAY['C']),
+        (9,  ARRAY['B']),
+        (10, ARRAY['C']),  -- 2nd (BC){2} done, 1st {2} done, 1st + iter done
+        (11, ARRAY['A']),  -- 2nd + iter, 1st {2}, A
+        (12, ARRAY['B']),
+        (13, ARRAY['C']),
+        (14, ARRAY['B']),
+        (15, ARRAY['C']),
+        (16, ARRAY['A']),  -- 2nd + iter, 2nd {2}, A
+        (17, ARRAY['B']),
+        (18, ARRAY['C']),
+        (19, ARRAY['B']),
+        (20, ARRAY['C']),  -- 2nd + iter done
+        (21, ARRAY['X'])   -- no match, + ends
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_3level_end
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A (B C){2}){2})+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
 -- Multiple unbounded: A+ B+ (first element unbounded enables absorption)
 WITH test_multi_unbounded AS (
     SELECT * FROM (VALUES
-- 
2.50.1 (Apple Git-155)

From aaff42b0eac54654d9cfd42ae4d5db4c1129764b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 22:00:50 +0900
Subject: [PATCH] Rename rpr_explain test views from sequential numbers to
 descriptive names

Replace rpr_ev01..rpr_ev89 with section-based names like
rpr_ev_ctx_absorb_group, rpr_ev_alt_nested_start, etc.
This avoids numbering conflicts when inserting new tests.
---
 src/test/regress/expected/rpr_explain.out | 378 +++++++++++-----------
 src/test/regress/sql/rpr_explain.sql      | 372 ++++++++++-----------
 2 files changed, 375 insertions(+), 375 deletions(-)

diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 560f21f44c2..f66caf8908e 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -111,7 +111,7 @@ VALUES
 -- Basic NFA Statistics Tests
 -- ============================================================
 -- Simple pattern - should show basic statistics
-CREATE VIEW rpr_ev01 AS
+CREATE VIEW rpr_ev_basic_simple AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -120,7 +120,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS cat = 'A', B AS cat = 'B'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev01'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -150,7 +150,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Pattern with no matches - 0 matched
-CREATE VIEW rpr_ev02 AS
+CREATE VIEW rpr_ev_basic_nomatch AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -159,7 +159,7 @@ WINDOW w AS (
     PATTERN (X Y Z)
     DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev02'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_nomatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (x y z) 
@@ -188,7 +188,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Pattern matching every row - high match count
-CREATE VIEW rpr_ev03 AS
+CREATE VIEW rpr_ev_basic_allrows AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -197,7 +197,7 @@ WINDOW w AS (
     PATTERN (R)
     DEFINE R AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev03'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_allrows'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line      
 ----------------
    PATTERN (r) 
@@ -227,7 +227,7 @@ WINDOW w AS (
 
 -- Regression test: Space before parenthesis in pattern deparse
 -- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
-CREATE VIEW rpr_ev04 AS
+CREATE VIEW rpr_ev_basic_deparse_space AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -235,7 +235,7 @@ WINDOW w AS (
     PATTERN (A (B | C))
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev04'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_space'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN (a (b | c)) 
@@ -266,7 +266,7 @@ WINDOW w AS (
 -- Regression test: Sequential alternations at same depth
 -- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
 -- Previously failed due to missing parentheses on ALT depth decrease
-CREATE VIEW rpr_ev05 AS
+CREATE VIEW rpr_ev_basic_deparse_seqalt AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -274,7 +274,7 @@ WINDOW w AS (
     PATTERN (A ((B | C) (D | E))*)
     DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev05'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_seqalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line                
 -----------------------------------
    PATTERN (a ((b | c) (d | e))*) 
@@ -305,7 +305,7 @@ WINDOW w AS (
 -- State Statistics Tests (peak, total, merged)
 -- ============================================================
 -- Simple quantifier pattern - A+ with short matches (no merging)
-CREATE VIEW rpr_ev06 AS
+CREATE VIEW rpr_ev_state_simple_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -314,7 +314,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev06'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_simple_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line       
 -----------------
    PATTERN (a+) 
@@ -343,7 +343,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Alternation pattern - multiple state branches
-CREATE VIEW rpr_ev07 AS
+CREATE VIEW rpr_ev_state_alt AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -354,7 +354,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev07'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line               
 ----------------------------------
    PATTERN ((a | b | c) (d | e)) 
@@ -386,7 +386,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Complex pattern with high state count
-CREATE VIEW rpr_ev08 AS
+CREATE VIEW rpr_ev_state_complex AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -398,7 +398,7 @@ WINDOW w AS (
         B AS v % 3 = 2,
         C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev08'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_complex'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (a+ b* c+) 
@@ -431,7 +431,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Grouped pattern with quantifier - state count with grouping
-CREATE VIEW rpr_ev09 AS
+CREATE VIEW rpr_ev_state_group_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -440,7 +440,7 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev09'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_group_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN ((a b)+) 
@@ -471,7 +471,7 @@ WINDOW w AS (
 
 -- State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
-CREATE VIEW rpr_ev10 AS
+CREATE VIEW rpr_ev_state_explosion AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -480,7 +480,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev10'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_explosion'), E'\n')) AS line WHERE line ~ 'PATTERN';
                                      line                                     
 ------------------------------------------------------------------------------
    PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b)) 
@@ -511,7 +511,7 @@ WINDOW w AS (
 
 -- Consecutive ALT merge followed by different ALT
 -- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
-CREATE VIEW rpr_ev11 AS
+CREATE VIEW rpr_ev_state_alt_merge_alt AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -520,7 +520,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev11'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
                  line                 
 --------------------------------------
    PATTERN ((a | b) (a | b) (c | d)) 
@@ -551,7 +551,7 @@ WINDOW w AS (
 
 -- Consecutive ALT merge followed by non-ALT element
 -- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
-CREATE VIEW rpr_ev12 AS
+CREATE VIEW rpr_ev_state_alt_merge_nonalt AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -560,7 +560,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev12'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_nonalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN ((a | b) (a | b) c) 
@@ -590,7 +590,7 @@ WINDOW w AS (
 (9 rows)
 
 -- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
-CREATE VIEW rpr_ev13 AS
+CREATE VIEW rpr_ev_state_alt_absorb_group AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -599,7 +599,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B)+ (A | B))
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev13'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
                  line                  
 ---------------------------------------
    PATTERN ((a | b) (a | b)+ (a | b)) 
@@ -629,7 +629,7 @@ WINDOW w AS (
 (9 rows)
 
 -- High state count - alternation with plus quantifier
-CREATE VIEW rpr_ev14 AS
+CREATE VIEW rpr_ev_state_alt_plus AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -638,7 +638,7 @@ WINDOW w AS (
     PATTERN ((A | B | C)+ D)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev14'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
             line             
 -----------------------------
    PATTERN ((a | b | c)+ d) 
@@ -669,7 +669,7 @@ WINDOW w AS (
 
 -- Early termination: first ALT branch (A) reaches FIN immediately,
 -- pruning second branch (A B+) before it can accumulate B repetitions.
-CREATE VIEW rpr_ev15 AS
+CREATE VIEW rpr_ev_state_alt_prune AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -678,7 +678,7 @@ WINDOW w AS (
     PATTERN ((A | A B)+)
     DEFINE A AS v = 1, B AS v > 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev15'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_prune'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a | a b)+) 
@@ -707,7 +707,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Nested quantifiers causing state growth
-CREATE VIEW rpr_ev16 AS
+CREATE VIEW rpr_ev_state_nested_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -716,7 +716,7 @@ WINDOW w AS (
     PATTERN (((A | B)+)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev16'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN (((a | b)+)+) 
@@ -749,7 +749,7 @@ WINDOW w AS (
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
 -- Context absorption with unbounded quantifier at start
-CREATE VIEW rpr_ev17 AS
+CREATE VIEW rpr_ev_ctx_absorb_unbounded AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -758,7 +758,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev17'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_unbounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -788,7 +788,7 @@ WINDOW w AS (
 (9 rows)
 
 -- No absorption - bounded quantifier
-CREATE VIEW rpr_ev18 AS
+CREATE VIEW rpr_ev_ctx_no_absorb AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -797,7 +797,7 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev18'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (a{2,4} b) 
@@ -827,7 +827,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Contexts skipped by SKIP PAST LAST ROW
-CREATE VIEW rpr_ev19 AS
+CREATE VIEW rpr_ev_ctx_skip AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -836,7 +836,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev19'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a b c) 
@@ -866,7 +866,7 @@ WINDOW w AS (
 (9 rows)
 
 -- High context absorption - unbounded group
-CREATE VIEW rpr_ev20 AS
+CREATE VIEW rpr_ev_ctx_absorb_group AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -875,7 +875,7 @@ WINDOW w AS (
     PATTERN ((A B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN ((a b)+ c) 
@@ -907,7 +907,7 @@ WINDOW w AS (
 -- Fixed-length group absorption: (A B B)+ C
 -- B B merged to B{2}; absorbable with fixed-length check
 -- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
-CREATE VIEW rpr_ev20b AS
+CREATE VIEW rpr_ev_ctx_absorb_fixedvar AS
 SELECT count(*) OVER w
 FROM generate_series(1, 70) AS s(v)
 WINDOW w AS (
@@ -916,7 +916,7 @@ WINDOW w AS (
     PATTERN ((A B B)+ C)
     DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixedvar'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a b b)+ c) 
@@ -947,7 +947,7 @@ WINDOW w AS (
 
 -- Nested fixed-length group absorption: (A (B C){2} D)+ E
 -- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
-CREATE VIEW rpr_ev20c AS
+CREATE VIEW rpr_ev_ctx_absorb_nested AS
 SELECT count(*) OVER w
 FROM generate_series(1, 65) AS s(v)
 WINDOW w AS (
@@ -958,7 +958,7 @@ WINDOW w AS (
            C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
            E AS v % 13 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN ((a (b c){2} d)+ e) 
@@ -991,7 +991,7 @@ WINDOW w AS (
 
 -- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
 -- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
-CREATE VIEW rpr_ev20d AS
+CREATE VIEW rpr_ev_ctx_absorb_deep AS
 SELECT count(*) OVER w
 FROM generate_series(1, 82) AS s(v)
 WINDOW w AS (
@@ -1006,7 +1006,7 @@ WINDOW w AS (
            E AS v % 41 IN (20, 40),
            F AS v % 41 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_deep'), E'\n')) AS line WHERE line ~ 'PATTERN';
                    line                    
 -------------------------------------------
    PATTERN ((a ((b c c c){2} d){2} e)+ f) 
@@ -1044,7 +1044,7 @@ WINDOW w AS (
 -- 3-level END chain absorption: ((A (B C){2}){2})+
 -- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
 -- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
-CREATE VIEW rpr_ev20e AS
+CREATE VIEW rpr_ev_ctx_absorb_endchain AS
 SELECT count(*) OVER w
 FROM generate_series(1, 42) AS s(v)
 WINDOW w AS (
@@ -1055,7 +1055,7 @@ WINDOW w AS (
            B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
            C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_endchain'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line               
 ---------------------------------
    PATTERN (((a (b c){2}){2})+) 
@@ -1090,7 +1090,7 @@ WINDOW w AS (
 -- Match Length Statistics Tests
 -- ============================================================
 -- Fixed length matches - all same length
-CREATE VIEW rpr_ev21 AS
+CREATE VIEW rpr_ev_mlen_fixed AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -1101,7 +1101,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev21'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_fixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN (a b c d e) 
@@ -1133,7 +1133,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Variable length matches - min/max/avg differ
-CREATE VIEW rpr_ev22 AS
+CREATE VIEW rpr_ev_mlen_variable AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1142,7 +1142,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev22'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_variable'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1172,7 +1172,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Very long matches
-CREATE VIEW rpr_ev23 AS
+CREATE VIEW rpr_ev_mlen_long AS
 SELECT count(*) OVER w
 FROM generate_series(1, 200) AS s(v)
 WINDOW w AS (
@@ -1181,7 +1181,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v <= 195, B AS v > 195
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev23'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1211,7 +1211,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Uniform match length with mismatches from gap rows (v%20 = 11..15)
-CREATE VIEW rpr_ev24 AS
+CREATE VIEW rpr_ev_mlen_with_mismatch AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1222,7 +1222,7 @@ WINDOW w AS (
         A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
         B AS v % 20 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev24'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_with_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1258,7 +1258,7 @@ WINDOW w AS (
 -- ============================================================
 -- Pattern with complete match every cycle: 0 mismatched
 -- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched
-CREATE VIEW rpr_ev25 AS
+CREATE VIEW rpr_ev_mlen_no_mismatch AS
 SELECT count(*) OVER w
 FROM (
     SELECT v,
@@ -1274,7 +1274,7 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev25'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_no_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line         
 ----------------------
    PATTERN (a+ b+ c) 
@@ -1311,7 +1311,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Long partial matches that fail
-CREATE VIEW rpr_ev26 AS
+CREATE VIEW rpr_ev_mlen_long_partial AS
 SELECT count(*) OVER w
 FROM (
     SELECT i AS v,
@@ -1332,7 +1332,7 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev26'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long_partial'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line         
 ----------------------
    PATTERN (a+ b+ c) 
@@ -1377,7 +1377,7 @@ WINDOW w AS (
 -- JSON Format Tests
 -- ============================================================
 -- JSON format output with all statistics
-CREATE VIEW rpr_ev27 AS
+CREATE VIEW rpr_ev_json_basic AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1386,7 +1386,7 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev27'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_basic'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a+ b+) 
@@ -1454,7 +1454,7 @@ WINDOW w AS (
 (1 row)
 
 -- JSON format with match length statistics
-CREATE VIEW rpr_ev28 AS
+CREATE VIEW rpr_ev_json_matchlen AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1463,7 +1463,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev28'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1535,7 +1535,7 @@ WINDOW w AS (
 
 -- JSON format with mismatch statistics
 -- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
-CREATE VIEW rpr_ev29 AS
+CREATE VIEW rpr_ev_json_mismatch AS
 SELECT count(*) OVER w
 FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
 WINDOW w AS (
@@ -1544,7 +1544,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v = 1, B AS v = 2, C AS v = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev29'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a b c) 
@@ -1615,7 +1615,7 @@ WINDOW w AS (
 
 -- JSON format with skipped context statistics
 -- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
-CREATE VIEW rpr_ev30 AS
+CREATE VIEW rpr_ev_json_skip AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1624,7 +1624,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev30'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
                                      line                                     
 ------------------------------------------------------------------------------
    PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b)) 
@@ -1698,7 +1698,7 @@ WINDOW w AS (
 -- XML Format Tests
 -- ============================================================
 -- XML format output
-CREATE VIEW rpr_ev31 AS
+CREATE VIEW rpr_ev_xml_basic AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -1707,7 +1707,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev31'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_xml_basic'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -1778,7 +1778,7 @@ WINDOW w AS (
 -- Multiple Partitions Tests
 -- ============================================================
 -- Statistics across multiple partitions
-CREATE VIEW rpr_ev32 AS
+CREATE VIEW rpr_ev_part_multi AS
 SELECT count(*) OVER w
 FROM (
     SELECT p, v
@@ -1792,7 +1792,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev32'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1832,7 +1832,7 @@ WINDOW w AS (
 (14 rows)
 
 -- Different pattern behavior per partition
-CREATE VIEW rpr_ev33 AS
+CREATE VIEW rpr_ev_part_diff AS
 SELECT count(*) OVER w
 FROM (
     SELECT
@@ -1847,7 +1847,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS val < 5, B AS val >= 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev33'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_diff'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -1889,7 +1889,7 @@ WINDOW w AS (
 -- Edge Cases
 -- ============================================================
 -- Empty result set
-CREATE VIEW rpr_ev34 AS
+CREATE VIEW rpr_ev_edge_empty AS
 SELECT count(*) OVER w
 FROM generate_series(1, 0) AS s(v)
 WINDOW w AS (
@@ -1898,7 +1898,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v = 1, B AS v = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev34'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -1923,7 +1923,7 @@ WINDOW w AS (
 (4 rows)
 
 -- Single row
-CREATE VIEW rpr_ev35 AS
+CREATE VIEW rpr_ev_edge_single_row AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1) AS s(v)
 WINDOW w AS (
@@ -1932,7 +1932,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev35'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_row'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line      
 ----------------
    PATTERN (a) 
@@ -1961,7 +1961,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Pattern longer than data
-CREATE VIEW rpr_ev36 AS
+CREATE VIEW rpr_ev_edge_pattern_longer AS
 SELECT count(*) OVER w
 FROM generate_series(1, 5) AS s(v)
 WINDOW w AS (
@@ -1972,7 +1972,7 @@ WINDOW w AS (
         A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
         F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev36'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_pattern_longer'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line               
 ----------------------------------
    PATTERN (a b c d e f g h i j) 
@@ -2003,7 +2003,7 @@ WINDOW w AS (
 (8 rows)
 
 -- All rows match as single match
-CREATE VIEW rpr_ev37 AS
+CREATE VIEW rpr_ev_edge_single_match AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2012,7 +2012,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev37'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_match'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line       
 -----------------
    PATTERN (a+) 
@@ -2045,7 +2045,7 @@ WINDOW w AS (
 -- Complex Pattern Tests
 -- ============================================================
 -- Nested groups
-CREATE VIEW rpr_ev38 AS
+CREATE VIEW rpr_ev_cpx_nested AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -2054,7 +2054,7 @@ WINDOW w AS (
     PATTERN (((A B) C)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev38'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN (((a b) c)+) 
@@ -2084,7 +2084,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Multiple alternations
-CREATE VIEW rpr_ev39 AS
+CREATE VIEW rpr_ev_cpx_multi_alt AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -2095,7 +2095,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev39'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_multi_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line               
 ----------------------------------
    PATTERN ((a | b) (c | d | e)) 
@@ -2127,7 +2127,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Optional elements
-CREATE VIEW rpr_ev40 AS
+CREATE VIEW rpr_ev_cpx_optional AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2136,7 +2136,7 @@ WINDOW w AS (
     PATTERN (A B? C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev40'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_optional'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a b? c) 
@@ -2166,7 +2166,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
+CREATE VIEW rpr_ev_cpx_bounded AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -2175,7 +2175,7 @@ WINDOW w AS (
     PATTERN (A{2,5} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev41'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (a{2,5} b) 
@@ -2205,7 +2205,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Star quantifier
-CREATE VIEW rpr_ev42 AS
+CREATE VIEW rpr_ev_cpx_star AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2214,7 +2214,7 @@ WINDOW w AS (
     PATTERN (A B* C)
     DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev42'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_star'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a b* c) 
@@ -2247,7 +2247,7 @@ WINDOW w AS (
 -- Real-world Pattern Examples
 -- ============================================================
 -- Stock price pattern - V-shape (down then up)
-CREATE VIEW rpr_ev43 AS
+CREATE VIEW rpr_ev_real_vshape AS
 SELECT count(*) OVER w
 FROM rpr_nfa_complex
 WINDOW w AS (
@@ -2256,7 +2256,7 @@ WINDOW w AS (
     PATTERN (D+ U+)
     DEFINE D AS trend = 'D', U AS trend = 'U'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev43'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_vshape'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (d+ u+) 
@@ -2286,7 +2286,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Stock price pattern - peak (up, stable, down)
-CREATE VIEW rpr_ev44 AS
+CREATE VIEW rpr_ev_real_peak AS
 SELECT count(*) OVER w
 FROM rpr_nfa_complex
 WINDOW w AS (
@@ -2295,7 +2295,7 @@ WINDOW w AS (
     PATTERN (U+ S* D+)
     DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev44'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_peak'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (u+ s* d+) 
@@ -2325,7 +2325,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Consecutive increasing values (using PREV)
-CREATE VIEW rpr_ev45 AS
+CREATE VIEW rpr_ev_real_increasing AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2334,7 +2334,7 @@ WINDOW w AS (
     PATTERN (A{3,})
     DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev45'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_increasing'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a{3,}) 
@@ -2367,7 +2367,7 @@ WINDOW w AS (
 -- Performance-oriented Tests
 -- ============================================================
 -- Large dataset with simple pattern
-CREATE VIEW rpr_ev46 AS
+CREATE VIEW rpr_ev_perf_large_simple AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -2376,7 +2376,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev46'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -2406,7 +2406,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Large dataset with absorption
-CREATE VIEW rpr_ev47 AS
+CREATE VIEW rpr_ev_perf_large_absorb AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -2415,7 +2415,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev47'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -2445,7 +2445,7 @@ WINDOW w AS (
 (9 rows)
 
 -- High state merge ratio
-CREATE VIEW rpr_ev48 AS
+CREATE VIEW rpr_ev_perf_high_merge AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2454,7 +2454,7 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev48'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_high_merge'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a | b)+ c) 
@@ -2487,7 +2487,7 @@ WINDOW w AS (
 -- INITIAL vs no INITIAL comparison
 -- ============================================================
 -- With INITIAL keyword
-CREATE VIEW rpr_ev49 AS
+CREATE VIEW rpr_ev_initial_with AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2497,7 +2497,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev49'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_with'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -2528,7 +2528,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Without INITIAL keyword (same behavior currently)
-CREATE VIEW rpr_ev50 AS
+CREATE VIEW rpr_ev_initial_without AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2537,7 +2537,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev50'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_without'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -2570,7 +2570,7 @@ WINDOW w AS (
 -- Quantifier Variations
 -- ============================================================
 -- Plus quantifier
-CREATE VIEW rpr_ev51 AS
+CREATE VIEW rpr_ev_quant_plus AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -2579,7 +2579,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 4 <> 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev51'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
       line       
 -----------------
    PATTERN (a+) 
@@ -2609,7 +2609,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Star quantifier (zero or more)
-CREATE VIEW rpr_ev52 AS
+CREATE VIEW rpr_ev_quant_star AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -2618,7 +2618,7 @@ WINDOW w AS (
     PATTERN (A* B)
     DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev52'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_star'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a* b) 
@@ -2648,7 +2648,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Question mark (zero or one)
-CREATE VIEW rpr_ev53 AS
+CREATE VIEW rpr_ev_quant_question AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -2657,7 +2657,7 @@ WINDOW w AS (
     PATTERN (A? B C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev53'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_question'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a? b c) 
@@ -2687,7 +2687,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Exact count {n}
-CREATE VIEW rpr_ev54 AS
+CREATE VIEW rpr_ev_quant_exact AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2696,7 +2696,7 @@ WINDOW w AS (
     PATTERN (A{3} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev54'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_exact'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a{3} b) 
@@ -2726,7 +2726,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Range {n,m}
-CREATE VIEW rpr_ev55 AS
+CREATE VIEW rpr_ev_quant_range AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2735,7 +2735,7 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev55'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_range'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line          
 -----------------------
    PATTERN (a{2,4} b) 
@@ -2765,7 +2765,7 @@ WINDOW w AS (
 (9 rows)
 
 -- At least {n,}
-CREATE VIEW rpr_ev56 AS
+CREATE VIEW rpr_ev_quant_atleast AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2774,7 +2774,7 @@ WINDOW w AS (
     PATTERN (A{3,} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev56'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_atleast'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line         
 ----------------------
    PATTERN (a{3,} b) 
@@ -2808,7 +2808,7 @@ WINDOW w AS (
 -- ============================================================
 -- Verify state count accuracy
 -- Pattern A+ B with 20 rows should show predictable state behavior
-CREATE VIEW rpr_ev57 AS
+CREATE VIEW rpr_ev_reg_state_count AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -2817,7 +2817,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev57'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_state_count'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -2847,7 +2847,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Verify context count with known absorption
-CREATE VIEW rpr_ev58 AS
+CREATE VIEW rpr_ev_reg_ctx_absorb AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2856,7 +2856,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev58'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_ctx_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a+ b c) 
@@ -2886,7 +2886,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Verify match length with fixed-length pattern
-CREATE VIEW rpr_ev59 AS
+CREATE VIEW rpr_ev_reg_matchlen AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2895,7 +2895,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev59'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line        
 --------------------
    PATTERN (a b c) 
@@ -2928,7 +2928,7 @@ WINDOW w AS (
 -- Alternation Pattern Tests
 -- ============================================================
 -- Simple alternation
-CREATE VIEW rpr_ev60 AS
+CREATE VIEW rpr_ev_alt_simple AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -2937,7 +2937,7 @@ WINDOW w AS (
     PATTERN ((A | B) C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev60'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN ((a | b) c) 
@@ -2967,7 +2967,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Multiple items in alternation
-CREATE VIEW rpr_ev61 AS
+CREATE VIEW rpr_ev_alt_multi_item AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -2978,7 +2978,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev61'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_multi_item'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN ((a | b | c | d) e) 
@@ -3010,7 +3010,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Alternation with quantifiers
-CREATE VIEW rpr_ev62 AS
+CREATE VIEW rpr_ev_alt_with_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -3019,7 +3019,7 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev62'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_with_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a | b)+ c) 
@@ -3049,7 +3049,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Multiple alternatives (4+)
-CREATE VIEW rpr_ev63 AS
+CREATE VIEW rpr_ev_alt_four_plus AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -3057,7 +3057,7 @@ WINDOW w AS (
     PATTERN (A | B | C | D | E)
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev63'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_four_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN (a | b | c | d | e) 
@@ -3085,7 +3085,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Alternation at start
-CREATE VIEW rpr_ev64 AS
+CREATE VIEW rpr_ev_alt_at_start AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3093,7 +3093,7 @@ WINDOW w AS (
     PATTERN ((A | B) C D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev64'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN ((a | b) c d) 
@@ -3122,7 +3122,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Multiple sequential alternations
-CREATE VIEW rpr_ev65 AS
+CREATE VIEW rpr_ev_alt_sequential AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -3130,7 +3130,7 @@ WINDOW w AS (
     PATTERN ((A | B) C (D | E) F)
     DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev65'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_sequential'), E'\n')) AS line WHERE line ~ 'PATTERN';
                line               
 ----------------------------------
    PATTERN ((a | b) c (d | e) f) 
@@ -3158,7 +3158,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Quantified alternatives
-CREATE VIEW rpr_ev66 AS
+CREATE VIEW rpr_ev_alt_quantified AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3166,7 +3166,7 @@ WINDOW w AS (
     PATTERN ((A+ | B+) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev66'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_quantified'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN ((a+ | b+) c) 
@@ -3195,7 +3195,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Alternation at end
-CREATE VIEW rpr_ev67 AS
+CREATE VIEW rpr_ev_alt_at_end AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3203,7 +3203,7 @@ WINDOW w AS (
     PATTERN (A B (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev67'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_end'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN (a b (c | d)) 
@@ -3233,7 +3233,7 @@ WINDOW w AS (
 
 -- Nested ALT at start of branch inside outer ALT
 -- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
-CREATE VIEW rpr_ev68 AS
+CREATE VIEW rpr_ev_alt_nested_start AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -3241,7 +3241,7 @@ WINDOW w AS (
     PATTERN (A ((B | C) D | E))
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev68'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
               line              
 --------------------------------
    PATTERN (a ((b | c) d | e)) 
@@ -3270,7 +3270,7 @@ WINDOW w AS (
 
 -- Nested ALT at end of branch inside outer ALT
 -- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
-CREATE VIEW rpr_ev69 AS
+CREATE VIEW rpr_ev_alt_nested_end AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -3278,7 +3278,7 @@ WINDOW w AS (
     PATTERN (C (A | B) | D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev69'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_end'), E'\n')) AS line WHERE line ~ 'PATTERN';
             line            
 ----------------------------
    PATTERN (c (a | b) | d) 
@@ -3309,7 +3309,7 @@ WINDOW w AS (
 -- Group Pattern Tests
 -- ============================================================
 -- Simple group
-CREATE VIEW rpr_ev70 AS
+CREATE VIEW rpr_ev_grp_simple AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -3318,7 +3318,7 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev70'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN ((a b)+) 
@@ -3348,7 +3348,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Group with bounded quantifier
-CREATE VIEW rpr_ev71 AS
+CREATE VIEW rpr_ev_grp_bounded AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -3357,7 +3357,7 @@ WINDOW w AS (
     PATTERN ((A B){2,4})
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev71'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line           
 -------------------------
    PATTERN ((a b){2,4}) 
@@ -3387,7 +3387,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Nested groups
-CREATE VIEW rpr_ev72 AS
+CREATE VIEW rpr_ev_grp_nested AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3396,7 +3396,7 @@ WINDOW w AS (
     PATTERN (((A B){2})+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev72'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN (((a b){2})+) 
@@ -3426,7 +3426,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Deep nesting (3+ levels)
-CREATE VIEW rpr_ev73 AS
+CREATE VIEW rpr_ev_grp_deep AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -3434,7 +3434,7 @@ WINDOW w AS (
     PATTERN ((((A | B)+)+)+)
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev73'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_deep'), E'\n')) AS line WHERE line ~ 'PATTERN';
             line             
 -----------------------------
    PATTERN ((((a | b)+)+)+) 
@@ -3463,7 +3463,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Bounded quantifier on alternation
-CREATE VIEW rpr_ev74 AS
+CREATE VIEW rpr_ev_grp_bounded_alt AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3471,7 +3471,7 @@ WINDOW w AS (
     PATTERN ((A | B){2,3} C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev74'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
             line             
 -----------------------------
    PATTERN ((a | b){2,3} c) 
@@ -3500,7 +3500,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Nested groups with quantifiers
-CREATE VIEW rpr_ev75 AS
+CREATE VIEW rpr_ev_grp_nested_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3508,7 +3508,7 @@ WINDOW w AS (
     PATTERN (((A B)+ C)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev75'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN (((a b)+ c)*) 
@@ -3537,7 +3537,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Partial nested quantification
-CREATE VIEW rpr_ev76 AS
+CREATE VIEW rpr_ev_grp_partial_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -3545,7 +3545,7 @@ WINDOW w AS (
     PATTERN ((A (B C)+)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev76'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_partial_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
            line           
 --------------------------
    PATTERN ((a (b c)+)*) 
@@ -3577,7 +3577,7 @@ WINDOW w AS (
 -- Window Function Combinations
 -- ============================================================
 -- count(*) with pattern
-CREATE VIEW rpr_ev77 AS
+CREATE VIEW rpr_ev_wfn_count AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -3586,7 +3586,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev77'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_count'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3616,7 +3616,7 @@ WINDOW w AS (
 (9 rows)
 
 -- first_value with pattern
-CREATE VIEW rpr_ev78 AS
+CREATE VIEW rpr_ev_wfn_first_value AS
 SELECT first_value(v) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -3625,7 +3625,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev78'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_first_value'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3655,7 +3655,7 @@ WINDOW w AS (
 (9 rows)
 
 -- last_value with pattern
-CREATE VIEW rpr_ev79 AS
+CREATE VIEW rpr_ev_wfn_last_value AS
 SELECT last_value(v) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -3664,7 +3664,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev79'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_last_value'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3694,7 +3694,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Multiple window functions
-CREATE VIEW rpr_ev80 AS
+CREATE VIEW rpr_ev_wfn_multi AS
 SELECT
     count(*) OVER w,
     first_value(v) OVER w,
@@ -3706,7 +3706,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev80'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3742,7 +3742,7 @@ WINDOW w AS (
 -- DEFINE Expression Variations
 -- ============================================================
 -- Complex boolean expressions
-CREATE VIEW rpr_ev81 AS
+CREATE VIEW rpr_ev_def_complex_bool AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -3753,7 +3753,7 @@ WINDOW w AS (
         A AS (v % 5 <> 0) AND (v % 3 <> 0),
         B AS (v % 5 = 0) OR (v % 3 = 0)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev81'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_complex_bool'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3785,7 +3785,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Using PREV function
-CREATE VIEW rpr_ev82 AS
+CREATE VIEW rpr_ev_def_prev AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -3797,7 +3797,7 @@ WINDOW w AS (
         U AS v > PREV(v),
         D AS v < PREV(v)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev82'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_prev'), E'\n')) AS line WHERE line ~ 'PATTERN';
          line         
 ----------------------
    PATTERN (s u+ d+) 
@@ -3829,7 +3829,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Using NULL comparisons
-CREATE VIEW rpr_ev83 AS
+CREATE VIEW rpr_ev_def_null AS
 SELECT count(*) OVER w
 FROM (
     SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
@@ -3841,7 +3841,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_null'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line        
 -------------------
    PATTERN (a+ b) 
@@ -3877,7 +3877,7 @@ WINDOW w AS (
 -- Large Scale Statistics Verification
 -- ============================================================
 -- 500 rows - verify statistics scale correctly
-CREATE VIEW rpr_ev84 AS
+CREATE VIEW rpr_ev_scale_500rows AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3886,7 +3886,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_500rows'), E'\n')) AS line WHERE line ~ 'PATTERN';
         line         
 ---------------------
    PATTERN (a+ b c) 
@@ -3916,7 +3916,7 @@ WINDOW w AS (
 (9 rows)
 
 -- High match count scenario
-CREATE VIEW rpr_ev85 AS
+CREATE VIEW rpr_ev_scale_high_match AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3925,7 +3925,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_match'), E'\n')) AS line WHERE line ~ 'PATTERN';
        line       
 ------------------
    PATTERN (a b) 
@@ -3955,7 +3955,7 @@ WINDOW w AS (
 (9 rows)
 
 -- High skip count scenario
-CREATE VIEW rpr_ev86 AS
+CREATE VIEW rpr_ev_scale_high_skip AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -3969,7 +3969,7 @@ WINDOW w AS (
         D AS v % 100 = 4,
         E AS v % 100 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
           line          
 ------------------------
    PATTERN (a b c d e) 
@@ -4013,17 +4013,17 @@ WINDOW w AS (
 -- Test with row_number() as representative case.
 --
 -- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
-CREATE VIEW rpr_ev87 AS
+CREATE VIEW rpr_ev_opt_no_rpr AS
 SELECT row_number() OVER w
 FROM generate_series(1, 10) AS s(v)
 WINDOW w AS (
     ORDER BY v
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 );
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_no_rpr;
                           QUERY PLAN                          
 --------------------------------------------------------------
- Subquery Scan on rpr_ev87
+ Subquery Scan on rpr_ev_opt_no_rpr
    ->  WindowAgg
          Window: w AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING)
          ->  Sort
@@ -4032,7 +4032,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
 (6 rows)
 
 -- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-CREATE VIEW rpr_ev88 AS
+CREATE VIEW rpr_ev_opt_with_rpr AS
 SELECT row_number() OVER w
 FROM generate_series(1, 10) AS s(v)
 WINDOW w AS (
@@ -4043,10 +4043,10 @@ WINDOW w AS (
     DEFINE
         B AS v > PREV(v)
 );
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr;
                                       QUERY PLAN                                      
 --------------------------------------------------------------------------------------
- Subquery Scan on rpr_ev88
+ Subquery Scan on rpr_ev_opt_with_rpr
    ->  WindowAgg
          Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
@@ -4059,7 +4059,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
 -- Planner optimization: non-RPR and RPR windows that share the same base frame
 -- after frame optimization are kept as separate WindowAgg nodes.
 --
-CREATE VIEW rpr_ev89 AS
+CREATE VIEW rpr_ev_opt_mixed AS
 SELECT
     row_number() OVER w_normal AS rn_normal,
     row_number() OVER w_rpr AS rn_rpr
@@ -4072,10 +4072,10 @@ WINDOW
         PATTERN (A+)
         DEFINE A AS v > 1
     );
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev89;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
                                         QUERY PLAN                                        
 ------------------------------------------------------------------------------------------
- Subquery Scan on rpr_ev89
+ Subquery Scan on rpr_ev_opt_mixed
    ->  WindowAgg
          Window: w_rpr AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a+"
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 237f0366631..65a775fdad9 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -118,7 +118,7 @@ VALUES
 -- ============================================================
 
 -- Simple pattern - should show basic statistics
-CREATE VIEW rpr_ev01 AS
+CREATE VIEW rpr_ev_basic_simple AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -127,7 +127,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS cat = 'A', B AS cat = 'B'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev01'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -140,7 +140,7 @@ WINDOW w AS (
 )');
 
 -- Pattern with no matches - 0 matched
-CREATE VIEW rpr_ev02 AS
+CREATE VIEW rpr_ev_basic_nomatch AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -149,7 +149,7 @@ WINDOW w AS (
     PATTERN (X Y Z)
     DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev02'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_nomatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -162,7 +162,7 @@ WINDOW w AS (
 );');
 
 -- Pattern matching every row - high match count
-CREATE VIEW rpr_ev03 AS
+CREATE VIEW rpr_ev_basic_allrows AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -171,7 +171,7 @@ WINDOW w AS (
     PATTERN (R)
     DEFINE R AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev03'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_allrows'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -185,7 +185,7 @@ WINDOW w AS (
 
 -- Regression test: Space before parenthesis in pattern deparse
 -- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
-CREATE VIEW rpr_ev04 AS
+CREATE VIEW rpr_ev_basic_deparse_space AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -193,7 +193,7 @@ WINDOW w AS (
     PATTERN (A (B | C))
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev04'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_space'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -207,7 +207,7 @@ WINDOW w AS (
 -- Regression test: Sequential alternations at same depth
 -- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
 -- Previously failed due to missing parentheses on ALT depth decrease
-CREATE VIEW rpr_ev05 AS
+CREATE VIEW rpr_ev_basic_deparse_seqalt AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -215,7 +215,7 @@ WINDOW w AS (
     PATTERN (A ((B | C) (D | E))*)
     DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev05'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_seqalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -231,7 +231,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Simple quantifier pattern - A+ with short matches (no merging)
-CREATE VIEW rpr_ev06 AS
+CREATE VIEW rpr_ev_state_simple_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -240,7 +240,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev06'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_simple_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -253,7 +253,7 @@ WINDOW w AS (
 );');
 
 -- Alternation pattern - multiple state branches
-CREATE VIEW rpr_ev07 AS
+CREATE VIEW rpr_ev_state_alt AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -264,7 +264,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev07'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -279,7 +279,7 @@ WINDOW w AS (
 );');
 
 -- Complex pattern with high state count
-CREATE VIEW rpr_ev08 AS
+CREATE VIEW rpr_ev_state_complex AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -291,7 +291,7 @@ WINDOW w AS (
         B AS v % 3 = 2,
         C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev08'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_complex'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -307,7 +307,7 @@ WINDOW w AS (
 );');
 
 -- Grouped pattern with quantifier - state count with grouping
-CREATE VIEW rpr_ev09 AS
+CREATE VIEW rpr_ev_state_group_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -316,7 +316,7 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev09'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_group_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -330,7 +330,7 @@ WINDOW w AS (
 
 -- State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
-CREATE VIEW rpr_ev10 AS
+CREATE VIEW rpr_ev_state_explosion AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -339,7 +339,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev10'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_explosion'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -353,7 +353,7 @@ WINDOW w AS (
 
 -- Consecutive ALT merge followed by different ALT
 -- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
-CREATE VIEW rpr_ev11 AS
+CREATE VIEW rpr_ev_state_alt_merge_alt AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -362,7 +362,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev11'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -376,7 +376,7 @@ WINDOW w AS (
 
 -- Consecutive ALT merge followed by non-ALT element
 -- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
-CREATE VIEW rpr_ev12 AS
+CREATE VIEW rpr_ev_state_alt_merge_nonalt AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -385,7 +385,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev12'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_nonalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -398,7 +398,7 @@ WINDOW w AS (
 );');
 
 -- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
-CREATE VIEW rpr_ev13 AS
+CREATE VIEW rpr_ev_state_alt_absorb_group AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -407,7 +407,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B)+ (A | B))
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev13'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -420,7 +420,7 @@ WINDOW w AS (
 );');
 
 -- High state count - alternation with plus quantifier
-CREATE VIEW rpr_ev14 AS
+CREATE VIEW rpr_ev_state_alt_plus AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -429,7 +429,7 @@ WINDOW w AS (
     PATTERN ((A | B | C)+ D)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev14'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -443,7 +443,7 @@ WINDOW w AS (
 
 -- Early termination: first ALT branch (A) reaches FIN immediately,
 -- pruning second branch (A B+) before it can accumulate B repetitions.
-CREATE VIEW rpr_ev15 AS
+CREATE VIEW rpr_ev_state_alt_prune AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -452,7 +452,7 @@ WINDOW w AS (
     PATTERN ((A | A B)+)
     DEFINE A AS v = 1, B AS v > 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev15'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_prune'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -465,7 +465,7 @@ WINDOW w AS (
 );');
 
 -- Nested quantifiers causing state growth
-CREATE VIEW rpr_ev16 AS
+CREATE VIEW rpr_ev_state_nested_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -474,7 +474,7 @@ WINDOW w AS (
     PATTERN (((A | B)+)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev16'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -491,7 +491,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Context absorption with unbounded quantifier at start
-CREATE VIEW rpr_ev17 AS
+CREATE VIEW rpr_ev_ctx_absorb_unbounded AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -500,7 +500,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev17'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_unbounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -513,7 +513,7 @@ WINDOW w AS (
 );');
 
 -- No absorption - bounded quantifier
-CREATE VIEW rpr_ev18 AS
+CREATE VIEW rpr_ev_ctx_no_absorb AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -522,7 +522,7 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev18'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -535,7 +535,7 @@ WINDOW w AS (
 );');
 
 -- Contexts skipped by SKIP PAST LAST ROW
-CREATE VIEW rpr_ev19 AS
+CREATE VIEW rpr_ev_ctx_skip AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -544,7 +544,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev19'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -557,7 +557,7 @@ WINDOW w AS (
 );');
 
 -- High context absorption - unbounded group
-CREATE VIEW rpr_ev20 AS
+CREATE VIEW rpr_ev_ctx_absorb_group AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -566,7 +566,7 @@ WINDOW w AS (
     PATTERN ((A B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -581,7 +581,7 @@ WINDOW w AS (
 -- Fixed-length group absorption: (A B B)+ C
 -- B B merged to B{2}; absorbable with fixed-length check
 -- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
-CREATE VIEW rpr_ev20b AS
+CREATE VIEW rpr_ev_ctx_absorb_fixedvar AS
 SELECT count(*) OVER w
 FROM generate_series(1, 70) AS s(v)
 WINDOW w AS (
@@ -590,7 +590,7 @@ WINDOW w AS (
     PATTERN ((A B B)+ C)
     DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixedvar'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -604,7 +604,7 @@ WINDOW w AS (
 
 -- Nested fixed-length group absorption: (A (B C){2} D)+ E
 -- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
-CREATE VIEW rpr_ev20c AS
+CREATE VIEW rpr_ev_ctx_absorb_nested AS
 SELECT count(*) OVER w
 FROM generate_series(1, 65) AS s(v)
 WINDOW w AS (
@@ -615,7 +615,7 @@ WINDOW w AS (
            C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
            E AS v % 13 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -631,7 +631,7 @@ WINDOW w AS (
 
 -- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
 -- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
-CREATE VIEW rpr_ev20d AS
+CREATE VIEW rpr_ev_ctx_absorb_deep AS
 SELECT count(*) OVER w
 FROM generate_series(1, 82) AS s(v)
 WINDOW w AS (
@@ -646,7 +646,7 @@ WINDOW w AS (
            E AS v % 41 IN (20, 40),
            F AS v % 41 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_deep'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -667,7 +667,7 @@ WINDOW w AS (
 -- 3-level END chain absorption: ((A (B C){2}){2})+
 -- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
 -- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
-CREATE VIEW rpr_ev20e AS
+CREATE VIEW rpr_ev_ctx_absorb_endchain AS
 SELECT count(*) OVER w
 FROM generate_series(1, 42) AS s(v)
 WINDOW w AS (
@@ -678,7 +678,7 @@ WINDOW w AS (
            B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
            C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_endchain'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -697,7 +697,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Fixed length matches - all same length
-CREATE VIEW rpr_ev21 AS
+CREATE VIEW rpr_ev_mlen_fixed AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -708,7 +708,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev21'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_fixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -723,7 +723,7 @@ WINDOW w AS (
 );');
 
 -- Variable length matches - min/max/avg differ
-CREATE VIEW rpr_ev22 AS
+CREATE VIEW rpr_ev_mlen_variable AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -732,7 +732,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev22'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_variable'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -745,7 +745,7 @@ WINDOW w AS (
 );');
 
 -- Very long matches
-CREATE VIEW rpr_ev23 AS
+CREATE VIEW rpr_ev_mlen_long AS
 SELECT count(*) OVER w
 FROM generate_series(1, 200) AS s(v)
 WINDOW w AS (
@@ -754,7 +754,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v <= 195, B AS v > 195
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev23'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -767,7 +767,7 @@ WINDOW w AS (
 );');
 
 -- Uniform match length with mismatches from gap rows (v%20 = 11..15)
-CREATE VIEW rpr_ev24 AS
+CREATE VIEW rpr_ev_mlen_with_mismatch AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -778,7 +778,7 @@ WINDOW w AS (
         A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
         B AS v % 20 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev24'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_with_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -798,7 +798,7 @@ WINDOW w AS (
 
 -- Pattern with complete match every cycle: 0 mismatched
 -- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched
-CREATE VIEW rpr_ev25 AS
+CREATE VIEW rpr_ev_mlen_no_mismatch AS
 SELECT count(*) OVER w
 FROM (
     SELECT v,
@@ -814,7 +814,7 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev25'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_no_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -834,7 +834,7 @@ WINDOW w AS (
 );');
 
 -- Long partial matches that fail
-CREATE VIEW rpr_ev26 AS
+CREATE VIEW rpr_ev_mlen_long_partial AS
 SELECT count(*) OVER w
 FROM (
     SELECT i AS v,
@@ -855,7 +855,7 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev26'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long_partial'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -884,7 +884,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- JSON format output with all statistics
-CREATE VIEW rpr_ev27 AS
+CREATE VIEW rpr_ev_json_basic AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -893,7 +893,7 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev27'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_basic'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -906,7 +906,7 @@ WINDOW w AS (
 )');
 
 -- JSON format with match length statistics
-CREATE VIEW rpr_ev28 AS
+CREATE VIEW rpr_ev_json_matchlen AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -915,7 +915,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev28'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -929,7 +929,7 @@ WINDOW w AS (
 
 -- JSON format with mismatch statistics
 -- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
-CREATE VIEW rpr_ev29 AS
+CREATE VIEW rpr_ev_json_mismatch AS
 SELECT count(*) OVER w
 FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
 WINDOW w AS (
@@ -938,7 +938,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v = 1, B AS v = 2, C AS v = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev29'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -952,7 +952,7 @@ WINDOW w AS (
 
 -- JSON format with skipped context statistics
 -- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
-CREATE VIEW rpr_ev30 AS
+CREATE VIEW rpr_ev_json_skip AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -961,7 +961,7 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev30'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -978,7 +978,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- XML format output
-CREATE VIEW rpr_ev31 AS
+CREATE VIEW rpr_ev_xml_basic AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -987,7 +987,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev31'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_xml_basic'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
 SELECT count(*) OVER w
@@ -1004,7 +1004,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Statistics across multiple partitions
-CREATE VIEW rpr_ev32 AS
+CREATE VIEW rpr_ev_part_multi AS
 SELECT count(*) OVER w
 FROM (
     SELECT p, v
@@ -1018,7 +1018,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev32'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1036,7 +1036,7 @@ WINDOW w AS (
 );');
 
 -- Different pattern behavior per partition
-CREATE VIEW rpr_ev33 AS
+CREATE VIEW rpr_ev_part_diff AS
 SELECT count(*) OVER w
 FROM (
     SELECT
@@ -1051,7 +1051,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS val < 5, B AS val >= 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev33'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_diff'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1074,7 +1074,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Empty result set
-CREATE VIEW rpr_ev34 AS
+CREATE VIEW rpr_ev_edge_empty AS
 SELECT count(*) OVER w
 FROM generate_series(1, 0) AS s(v)
 WINDOW w AS (
@@ -1083,7 +1083,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v = 1, B AS v = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev34'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1096,7 +1096,7 @@ WINDOW w AS (
 );');
 
 -- Single row
-CREATE VIEW rpr_ev35 AS
+CREATE VIEW rpr_ev_edge_single_row AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1) AS s(v)
 WINDOW w AS (
@@ -1105,7 +1105,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev35'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_row'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1118,7 +1118,7 @@ WINDOW w AS (
 );');
 
 -- Pattern longer than data
-CREATE VIEW rpr_ev36 AS
+CREATE VIEW rpr_ev_edge_pattern_longer AS
 SELECT count(*) OVER w
 FROM generate_series(1, 5) AS s(v)
 WINDOW w AS (
@@ -1129,7 +1129,7 @@ WINDOW w AS (
         A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
         F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev36'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_pattern_longer'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1144,7 +1144,7 @@ WINDOW w AS (
 );');
 
 -- All rows match as single match
-CREATE VIEW rpr_ev37 AS
+CREATE VIEW rpr_ev_edge_single_match AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1153,7 +1153,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS TRUE
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev37'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_match'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1170,7 +1170,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Nested groups
-CREATE VIEW rpr_ev38 AS
+CREATE VIEW rpr_ev_cpx_nested AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1179,7 +1179,7 @@ WINDOW w AS (
     PATTERN (((A B) C)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev38'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1192,7 +1192,7 @@ WINDOW w AS (
 );');
 
 -- Multiple alternations
-CREATE VIEW rpr_ev39 AS
+CREATE VIEW rpr_ev_cpx_multi_alt AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -1203,7 +1203,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev39'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_multi_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1218,7 +1218,7 @@ WINDOW w AS (
 );');
 
 -- Optional elements
-CREATE VIEW rpr_ev40 AS
+CREATE VIEW rpr_ev_cpx_optional AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1227,7 +1227,7 @@ WINDOW w AS (
     PATTERN (A B? C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev40'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_optional'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1240,7 +1240,7 @@ WINDOW w AS (
 );');
 
 -- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
+CREATE VIEW rpr_ev_cpx_bounded AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1249,7 +1249,7 @@ WINDOW w AS (
     PATTERN (A{2,5} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev41'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1262,7 +1262,7 @@ WINDOW w AS (
 );');
 
 -- Star quantifier
-CREATE VIEW rpr_ev42 AS
+CREATE VIEW rpr_ev_cpx_star AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1271,7 +1271,7 @@ WINDOW w AS (
     PATTERN (A B* C)
     DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev42'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_star'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1288,7 +1288,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Stock price pattern - V-shape (down then up)
-CREATE VIEW rpr_ev43 AS
+CREATE VIEW rpr_ev_real_vshape AS
 SELECT count(*) OVER w
 FROM rpr_nfa_complex
 WINDOW w AS (
@@ -1297,7 +1297,7 @@ WINDOW w AS (
     PATTERN (D+ U+)
     DEFINE D AS trend = 'D', U AS trend = 'U'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev43'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_vshape'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1310,7 +1310,7 @@ WINDOW w AS (
 );');
 
 -- Stock price pattern - peak (up, stable, down)
-CREATE VIEW rpr_ev44 AS
+CREATE VIEW rpr_ev_real_peak AS
 SELECT count(*) OVER w
 FROM rpr_nfa_complex
 WINDOW w AS (
@@ -1319,7 +1319,7 @@ WINDOW w AS (
     PATTERN (U+ S* D+)
     DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev44'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_peak'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1332,7 +1332,7 @@ WINDOW w AS (
 );');
 
 -- Consecutive increasing values (using PREV)
-CREATE VIEW rpr_ev45 AS
+CREATE VIEW rpr_ev_real_increasing AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1341,7 +1341,7 @@ WINDOW w AS (
     PATTERN (A{3,})
     DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev45'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_increasing'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1358,7 +1358,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Large dataset with simple pattern
-CREATE VIEW rpr_ev46 AS
+CREATE VIEW rpr_ev_perf_large_simple AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -1367,7 +1367,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev46'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1380,7 +1380,7 @@ WINDOW w AS (
 );');
 
 -- Large dataset with absorption
-CREATE VIEW rpr_ev47 AS
+CREATE VIEW rpr_ev_perf_large_absorb AS
 SELECT count(*) OVER w
 FROM generate_series(1, 1000) AS s(v)
 WINDOW w AS (
@@ -1389,7 +1389,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev47'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1402,7 +1402,7 @@ WINDOW w AS (
 );');
 
 -- High state merge ratio
-CREATE VIEW rpr_ev48 AS
+CREATE VIEW rpr_ev_perf_high_merge AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -1411,7 +1411,7 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev48'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_high_merge'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1428,7 +1428,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- With INITIAL keyword
-CREATE VIEW rpr_ev49 AS
+CREATE VIEW rpr_ev_initial_with AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1438,7 +1438,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev49'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_with'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1452,7 +1452,7 @@ WINDOW w AS (
 );');
 
 -- Without INITIAL keyword (same behavior currently)
-CREATE VIEW rpr_ev50 AS
+CREATE VIEW rpr_ev_initial_without AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1461,7 +1461,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev50'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_without'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1478,7 +1478,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Plus quantifier
-CREATE VIEW rpr_ev51 AS
+CREATE VIEW rpr_ev_quant_plus AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1487,7 +1487,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 4 <> 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev51'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1500,7 +1500,7 @@ WINDOW w AS (
 );');
 
 -- Star quantifier (zero or more)
-CREATE VIEW rpr_ev52 AS
+CREATE VIEW rpr_ev_quant_star AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1509,7 +1509,7 @@ WINDOW w AS (
     PATTERN (A* B)
     DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev52'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_star'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1522,7 +1522,7 @@ WINDOW w AS (
 );');
 
 -- Question mark (zero or one)
-CREATE VIEW rpr_ev53 AS
+CREATE VIEW rpr_ev_quant_question AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1531,7 +1531,7 @@ WINDOW w AS (
     PATTERN (A? B C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev53'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_question'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1544,7 +1544,7 @@ WINDOW w AS (
 );');
 
 -- Exact count {n}
-CREATE VIEW rpr_ev54 AS
+CREATE VIEW rpr_ev_quant_exact AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1553,7 +1553,7 @@ WINDOW w AS (
     PATTERN (A{3} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev54'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_exact'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1566,7 +1566,7 @@ WINDOW w AS (
 );');
 
 -- Range {n,m}
-CREATE VIEW rpr_ev55 AS
+CREATE VIEW rpr_ev_quant_range AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1575,7 +1575,7 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev55'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_range'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1588,7 +1588,7 @@ WINDOW w AS (
 );');
 
 -- At least {n,}
-CREATE VIEW rpr_ev56 AS
+CREATE VIEW rpr_ev_quant_atleast AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1597,7 +1597,7 @@ WINDOW w AS (
     PATTERN (A{3,} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev56'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_atleast'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1615,7 +1615,7 @@ WINDOW w AS (
 
 -- Verify state count accuracy
 -- Pattern A+ B with 20 rows should show predictable state behavior
-CREATE VIEW rpr_ev57 AS
+CREATE VIEW rpr_ev_reg_state_count AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -1624,7 +1624,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev57'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_state_count'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1637,7 +1637,7 @@ WINDOW w AS (
 );');
 
 -- Verify context count with known absorption
-CREATE VIEW rpr_ev58 AS
+CREATE VIEW rpr_ev_reg_ctx_absorb AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -1646,7 +1646,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev58'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_ctx_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1659,7 +1659,7 @@ WINDOW w AS (
 );');
 
 -- Verify match length with fixed-length pattern
-CREATE VIEW rpr_ev59 AS
+CREATE VIEW rpr_ev_reg_matchlen AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -1668,7 +1668,7 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev59'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1685,7 +1685,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Simple alternation
-CREATE VIEW rpr_ev60 AS
+CREATE VIEW rpr_ev_alt_simple AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -1694,7 +1694,7 @@ WINDOW w AS (
     PATTERN ((A | B) C)
     DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev60'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1707,7 +1707,7 @@ WINDOW w AS (
 );');
 
 -- Multiple items in alternation
-CREATE VIEW rpr_ev61 AS
+CREATE VIEW rpr_ev_alt_multi_item AS
 SELECT count(*) OVER w
 FROM rpr_nfa_test
 WINDOW w AS (
@@ -1718,7 +1718,7 @@ WINDOW w AS (
         A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
         D AS cat = 'D', E AS cat = 'E'
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev61'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_multi_item'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1733,7 +1733,7 @@ WINDOW w AS (
 );');
 
 -- Alternation with quantifiers
-CREATE VIEW rpr_ev62 AS
+CREATE VIEW rpr_ev_alt_with_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -1742,7 +1742,7 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev62'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_with_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1755,7 +1755,7 @@ WINDOW w AS (
 );');
 
 -- Multiple alternatives (4+)
-CREATE VIEW rpr_ev63 AS
+CREATE VIEW rpr_ev_alt_four_plus AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1763,7 +1763,7 @@ WINDOW w AS (
     PATTERN (A | B | C | D | E)
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev63'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_four_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1775,7 +1775,7 @@ WINDOW w AS (
 );');
 
 -- Alternation at start
-CREATE VIEW rpr_ev64 AS
+CREATE VIEW rpr_ev_alt_at_start AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1783,7 +1783,7 @@ WINDOW w AS (
     PATTERN ((A | B) C D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev64'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1795,7 +1795,7 @@ WINDOW w AS (
 );');
 
 -- Multiple sequential alternations
-CREATE VIEW rpr_ev65 AS
+CREATE VIEW rpr_ev_alt_sequential AS
 SELECT count(*) OVER w
 FROM generate_series(1, 100) AS s(v)
 WINDOW w AS (
@@ -1803,7 +1803,7 @@ WINDOW w AS (
     PATTERN ((A | B) C (D | E) F)
     DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev65'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_sequential'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1815,7 +1815,7 @@ WINDOW w AS (
 );');
 
 -- Quantified alternatives
-CREATE VIEW rpr_ev66 AS
+CREATE VIEW rpr_ev_alt_quantified AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1823,7 +1823,7 @@ WINDOW w AS (
     PATTERN ((A+ | B+) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev66'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_quantified'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1835,7 +1835,7 @@ WINDOW w AS (
 );');
 
 -- Alternation at end
-CREATE VIEW rpr_ev67 AS
+CREATE VIEW rpr_ev_alt_at_end AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1843,7 +1843,7 @@ WINDOW w AS (
     PATTERN (A B (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev67'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_end'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1856,7 +1856,7 @@ WINDOW w AS (
 
 -- Nested ALT at start of branch inside outer ALT
 -- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
-CREATE VIEW rpr_ev68 AS
+CREATE VIEW rpr_ev_alt_nested_start AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -1864,7 +1864,7 @@ WINDOW w AS (
     PATTERN (A ((B | C) D | E))
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev68'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1877,7 +1877,7 @@ WINDOW w AS (
 
 -- Nested ALT at end of branch inside outer ALT
 -- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
-CREATE VIEW rpr_ev69 AS
+CREATE VIEW rpr_ev_alt_nested_end AS
 SELECT count(*) OVER w
 FROM generate_series(1, 20) AS s(v)
 WINDOW w AS (
@@ -1885,7 +1885,7 @@ WINDOW w AS (
     PATTERN (C (A | B) | D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev69'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_end'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1901,7 +1901,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Simple group
-CREATE VIEW rpr_ev70 AS
+CREATE VIEW rpr_ev_grp_simple AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1910,7 +1910,7 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev70'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1923,7 +1923,7 @@ WINDOW w AS (
 );');
 
 -- Group with bounded quantifier
-CREATE VIEW rpr_ev71 AS
+CREATE VIEW rpr_ev_grp_bounded AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1932,7 +1932,7 @@ WINDOW w AS (
     PATTERN ((A B){2,4})
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev71'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1945,7 +1945,7 @@ WINDOW w AS (
 );');
 
 -- Nested groups
-CREATE VIEW rpr_ev72 AS
+CREATE VIEW rpr_ev_grp_nested AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1954,7 +1954,7 @@ WINDOW w AS (
     PATTERN (((A B){2})+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev72'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1967,7 +1967,7 @@ WINDOW w AS (
 );');
 
 -- Deep nesting (3+ levels)
-CREATE VIEW rpr_ev73 AS
+CREATE VIEW rpr_ev_grp_deep AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -1975,7 +1975,7 @@ WINDOW w AS (
     PATTERN ((((A | B)+)+)+)
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev73'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_deep'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1987,7 +1987,7 @@ WINDOW w AS (
 );');
 
 -- Bounded quantifier on alternation
-CREATE VIEW rpr_ev74 AS
+CREATE VIEW rpr_ev_grp_bounded_alt AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -1995,7 +1995,7 @@ WINDOW w AS (
     PATTERN ((A | B){2,3} C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev74'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2007,7 +2007,7 @@ WINDOW w AS (
 );');
 
 -- Nested groups with quantifiers
-CREATE VIEW rpr_ev75 AS
+CREATE VIEW rpr_ev_grp_nested_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -2015,7 +2015,7 @@ WINDOW w AS (
     PATTERN (((A B)+ C)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev75'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2027,7 +2027,7 @@ WINDOW w AS (
 );');
 
 -- Partial nested quantification
-CREATE VIEW rpr_ev76 AS
+CREATE VIEW rpr_ev_grp_partial_quant AS
 SELECT count(*) OVER w
 FROM generate_series(1, 60) AS s(v)
 WINDOW w AS (
@@ -2035,7 +2035,7 @@ WINDOW w AS (
     PATTERN ((A (B C)+)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev76'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_partial_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2051,7 +2051,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- count(*) with pattern
-CREATE VIEW rpr_ev77 AS
+CREATE VIEW rpr_ev_wfn_count AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2060,7 +2060,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev77'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_count'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2073,7 +2073,7 @@ WINDOW w AS (
 );');
 
 -- first_value with pattern
-CREATE VIEW rpr_ev78 AS
+CREATE VIEW rpr_ev_wfn_first_value AS
 SELECT first_value(v) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2082,7 +2082,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev78'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_first_value'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT first_value(v) OVER w
@@ -2095,7 +2095,7 @@ WINDOW w AS (
 );');
 
 -- last_value with pattern
-CREATE VIEW rpr_ev79 AS
+CREATE VIEW rpr_ev_wfn_last_value AS
 SELECT last_value(v) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2104,7 +2104,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev79'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_last_value'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT last_value(v) OVER w
@@ -2117,7 +2117,7 @@ WINDOW w AS (
 );');
 
 -- Multiple window functions
-CREATE VIEW rpr_ev80 AS
+CREATE VIEW rpr_ev_wfn_multi AS
 SELECT
     count(*) OVER w,
     first_value(v) OVER w,
@@ -2129,7 +2129,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev80'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT
@@ -2149,7 +2149,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- Complex boolean expressions
-CREATE VIEW rpr_ev81 AS
+CREATE VIEW rpr_ev_def_complex_bool AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -2160,7 +2160,7 @@ WINDOW w AS (
         A AS (v % 5 <> 0) AND (v % 3 <> 0),
         B AS (v % 5 = 0) OR (v % 3 = 0)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev81'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_complex_bool'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2175,7 +2175,7 @@ WINDOW w AS (
 );');
 
 -- Using PREV function
-CREATE VIEW rpr_ev82 AS
+CREATE VIEW rpr_ev_def_prev AS
 SELECT count(*) OVER w
 FROM generate_series(1, 30) AS s(v)
 WINDOW w AS (
@@ -2187,7 +2187,7 @@ WINDOW w AS (
         U AS v > PREV(v),
         D AS v < PREV(v)
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev82'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_prev'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2203,7 +2203,7 @@ WINDOW w AS (
 );');
 
 -- Using NULL comparisons
-CREATE VIEW rpr_ev83 AS
+CREATE VIEW rpr_ev_def_null AS
 SELECT count(*) OVER w
 FROM (
     SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
@@ -2215,7 +2215,7 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_null'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2235,7 +2235,7 @@ WINDOW w AS (
 -- ============================================================
 
 -- 500 rows - verify statistics scale correctly
-CREATE VIEW rpr_ev84 AS
+CREATE VIEW rpr_ev_scale_500rows AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2244,7 +2244,7 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_500rows'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2257,7 +2257,7 @@ WINDOW w AS (
 );');
 
 -- High match count scenario
-CREATE VIEW rpr_ev85 AS
+CREATE VIEW rpr_ev_scale_high_match AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2266,7 +2266,7 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_match'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2279,7 +2279,7 @@ WINDOW w AS (
 );');
 
 -- High skip count scenario
-CREATE VIEW rpr_ev86 AS
+CREATE VIEW rpr_ev_scale_high_skip AS
 SELECT count(*) OVER w
 FROM generate_series(1, 500) AS s(v)
 WINDOW w AS (
@@ -2293,7 +2293,7 @@ WINDOW w AS (
         D AS v % 100 = 4,
         E AS v % 100 = 5
 );
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -2321,7 +2321,7 @@ WINDOW w AS (
 --
 
 -- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
-CREATE VIEW rpr_ev87 AS
+CREATE VIEW rpr_ev_opt_no_rpr AS
 SELECT row_number() OVER w
 FROM generate_series(1, 10) AS s(v)
 WINDOW w AS (
@@ -2329,10 +2329,10 @@ WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 );
 
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_no_rpr;
 
 -- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-CREATE VIEW rpr_ev88 AS
+CREATE VIEW rpr_ev_opt_with_rpr AS
 SELECT row_number() OVER w
 FROM generate_series(1, 10) AS s(v)
 WINDOW w AS (
@@ -2344,13 +2344,13 @@ WINDOW w AS (
         B AS v > PREV(v)
 );
 
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr;
 
 --
 -- Planner optimization: non-RPR and RPR windows that share the same base frame
 -- after frame optimization are kept as separate WindowAgg nodes.
 --
-CREATE VIEW rpr_ev89 AS
+CREATE VIEW rpr_ev_opt_mixed AS
 SELECT
     row_number() OVER w_normal AS rn_normal,
     row_number() OVER w_rpr AS rn_rpr
@@ -2364,7 +2364,7 @@ WINDOW
         DEFINE A AS v > 1
     );
 
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev89;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;

From 1b45b59329f920174c90ee181c506505eb9357d7 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 23:52:42 +0900
Subject: [PATCH] Implement 1-slot PREV/NEXT navigation for RPR

 src/backend/parser/parse_rpr.c            |  86 ++++
 src/test/regress/expected/rpr_explain.out |  80 ++++
 src/test/regress/sql/rpr_explain.sql      |  56 +++
 21 files changed, 1606 insertions(+), 164 deletions(-)

@@ -318,12 +323,17 @@
         <indexterm>
          <primary>next</primary>
         </indexterm>

+		case T_RPRNavExpr:
+			{
+				/*
index 3c4843cde86..e41faa95be3 100644
 
+#include "common/int.h"

index 1c925e0221d..d2fffe0fce6 100644
 
-/*

@@ -253,9 +241,6 @@ static void clear_reduced_frame(WindowAggState *winstate);
 static int	get_reduced_frame_status(WindowAggState *winstate, int64 pos);
 static void update_reduced_frame(WindowObject winobj, int64 pos);

@@ -1280,6 +1265,25 @@ prepare_tuplestore(WindowAggState *winstate)
 		}
 	}
 
@@ -1391,6 +1395,13 @@ begin_partition(WindowAggState *winstate)
 
@@ -2729,15 +2740,18 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)

 
 
 	/*
 	 * create frame head and tail slots only if needed (must create slots in
@@ -2907,6 +2921,23 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->agg_winobj = agg_winobj;
 	}
 
+	/*
 
@@ -2947,7 +2978,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
@@ -2961,7 +2994,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
@@ -2996,107 +3028,38 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 }
 
 /*
 
-static bool
 
-	if (node == NULL)
-		return false;
+	/*

-/*

@@ -3157,8 +3120,8 @@ ExecReScanWindowAgg(WindowAggState *node)
@@ -4223,6 +4186,10 @@ register_result:
@@ -4233,37 +4200,25 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	ListCell   *lc;
 	int			varIdx = 0;
 	TupleTableSlot *slot;
+	int64		saved_pos;
 
-	/*

 
 	foreach(lc, winstate->defineClauseList)
 	{
@@ -4281,6 +4236,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 			break;
 	}

 
+	/*
 
+			case EEOP_RPR_NAV_SET:

index db1309ca311..3fb5d94abe9 100644
@@ -42,6 +42,8 @@ static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 
 /*
  * transformRPR
@@ -410,6 +412,10 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	foreach_ptr(TargetEntry, te, defineClause)
 		te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");

@@ -418,3 +424,83 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 	return defineClause;
 }
+
+/*
+ * check_rpr_nav_expr
index cfe24de43cf..742d889154b 100644
 
+		case T_RPRNavExpr:
+			{
 
 /*
  * prev
 
 /*
  * next
index ddf922d16d7..8e95169b7b0 100644

 
 
index c672d29f35b..74a6b682132 100644
@@ -2691,10 +2691,12 @@ typedef struct WindowAggState

 	/* RPR current match result */
 	bool		rpr_match_valid;	/* true if a match result is set */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f5b6b45664a..94723a3b909 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -648,6 +648,37 @@ typedef struct WindowFuncRunCondition
 	Expr	   *arg;
 } WindowFuncRunCondition;
 
+/*
+ * RPRNavExpr
+ *
 
+--
 
index f66caf8908e..4a646d1e1d8 100644
@@ -3828,6 +3828,86 @@ WINDOW w AS (
 
+-- Using 1-arg PREV (implicit offset 1)
+CREATE VIEW rpr_ev_nav_prev1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+          line           
+-------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v > PREV(v)) );
+(3 rows)
+
+-- Using 1-arg NEXT (implicit offset 1)
+CREATE VIEW rpr_ev_nav_next1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+          line           
+-------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v < NEXT(v)) );
+(3 rows)
+
+-- Using 2-arg PREV (explicit offset)
+CREATE VIEW rpr_ev_nav_prev2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+                 line                 
+--------------------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v > PREV(v, (2)::bigint)) );
+(3 rows)
+
+-- Using 2-arg NEXT (explicit offset)
+CREATE VIEW rpr_ev_nav_next2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+                 line                 
+--------------------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v < NEXT(v, (2)::bigint)) );
+(3 rows)
+
 -- Using NULL comparisons
 CREATE VIEW rpr_ev_def_null AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 95794d409e1..504476a2b02 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -440,6 +440,326 @@ WINDOW w AS (
   B AS val > PREV(val) * 0.99
 );
 
+--
 
 -- ReScan test: LATERAL join forces WindowAgg rescan with RPR
--- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+-- Tests ExecReScanWindowAgg clearing nav_slot
 SELECT g.x, sub.*
 FROM generate_series(1, 2) g(x),
 LATERAL (
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 65a775fdad9..aed2f69bc4f 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2202,6 +2202,62 @@ WINDOW w AS (
         D AS v < PREV(v)
 );');
 
+-- Using 1-arg PREV (implicit offset 1)
+CREATE VIEW rpr_ev_nav_prev1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 1-arg NEXT (implicit offset 1)
+CREATE VIEW rpr_ev_nav_next1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 2-arg PREV (explicit offset)
+CREATE VIEW rpr_ev_nav_prev2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 2-arg NEXT (explicit offset)
+CREATE VIEW rpr_ev_nav_next2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
 -- Using NULL comparisons
 CREATE VIEW rpr_ev_def_null AS
 SELECT count(*) OVER w
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6090c2d8950..16de1421302 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1801,7 +1801,6 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
-NavigationInfo
 NestLoop
 NestLoopParam
 NestLoopState
@@ -2478,6 +2477,8 @@ QuerySource
 QueueBackendStatus
 QueuePosition
 QuitSignalReason
+RPRNavExpr
+RPRNavKind
 RBTNode
 RBTOrderControl
 RBTree
-- 
2.50.1 (Apple Git-155)

From 56ec798cb75eaee3665c0e03a81ee1673e3f8a96 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 10:54:30 +0900
Subject: [PATCH] Add JIT compilation support for RPR PREV/NEXT navigation

---

 
 			case EEOP_RPR_NAV_SET:
 
index d586e17e0a1..de6ce4fba8a 100644
 
index 504476a2b02..b3bbc8254c4 100644

From a8481774babb51b4d5527fab6c134987f9bb49c9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 11:49:32 +0900
Subject: [PATCH] Add tuplestore trim optimization for RPR PREV navigation

Advance the tuplestore mark pointer based on the maximum PREV offset
found in DEFINE clause expressions, allowing tuplestore_trim() to
free rows that PREV can no longer reach.

The planner walks DEFINE expressions to find the maximum PREV offset.
If all offsets are constants, navMaxOffset is set directly. If any
offset is non-constant (parameter or expression), the planner sets
RPR_NAV_OFFSET_NEEDS_EVAL and the executor evaluates all PREV offsets
at init time. The executor then advances the mark to
(currentpos - navMaxOffset) each row.

NEXT offsets are ignored since they look forward and do not affect
trim. RPR_NAV_OFFSET_RETAIN_ALL is reserved for future navigation
functions (FIRST/LAST) that require the entire partition.
---
 src/backend/executor/nodeWindowAgg.c    | 136 ++++++++++++++++++++++--
 src/backend/optimizer/plan/createplan.c | 101 ++++++++++++++++++
 src/include/nodes/execnodes.h           |   1 +
 src/include/nodes/plannodes.h           |   9 ++
 src/include/optimizer/rpr.h             |   9 ++
 5 files changed, 246 insertions(+), 10 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index d2fffe0fce6..186029df1d9 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -244,6 +244,10 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
 /* Forward declarations - NFA row evaluation */
 static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
 
+/* Forward declarations - navigation offset evaluation */
+static bool collect_prev_offset_walker(Node *node, List **offsets);
+static void eval_nav_max_offset(WindowAggState *winstate, List *defineClause);
+
 /*
  * Not null info bit array consists of 2-bit items
  */
@@ -934,12 +938,18 @@ eval_windowaggregates(WindowAggState *winstate)
 		if (rpr_is_defined(winstate))
 		{
 			/*
-			 * If RPR is used, it is possible PREV wants to look at the
-			 * previous row.  So the mark pos should be frameheadpos - 1
-			 * unless it is below 0.
+			 * If RPR is used, PREV may need to look at rows before the frame
+			 * head.  Adjust mark by navMaxOffset if known, otherwise retain
+			 * from position 0.
 			 */
-			markpos -= 1;
-			if (markpos < 0)
+			if (winstate->navMaxOffset >= 0)
+			{
+				if (markpos > winstate->navMaxOffset)
+					markpos -= winstate->navMaxOffset;
+				else
+					markpos = 0;
+			}
+			else
 				markpos = 0;
 		}
 		WinSetMarkPosition(agg_winobj, markpos);
@@ -1269,12 +1279,15 @@ prepare_tuplestore(WindowAggState *winstate)
 	if (winstate->nav_winobj)
 	{
 		/*
-		 * Allocate a mark pointer pinned at position 0 so that the tuplestore
-		 * never truncates rows that a PREV(expr, N) might need.
+		 * Allocate mark and read pointers for PREV/NEXT navigation.
+		 *
+		 * If navMaxOffset >= 0, we advance the mark to (currentpos -
+		 * navMaxOffset) as rows are processed, allowing tuplestore_trim() to
+		 * free rows that are no longer reachable.
 		 *
-		 * XXX This retains the entire partition in the tuplestore.  If the
-		 * DEFINE clause only uses PREV/NEXT with small constant offsets, we
-		 * could advance the mark to (currentpos - max_offset) instead.
+		 * If navMaxOffset < 0 (RPR_NAV_OFFSET_NEEDS_EVAL or
+		 * RPR_NAV_OFFSET_RETAIN_ALL), the mark stays at 0, retaining the
+		 * entire partition in the tuplestore.
 		 */
 		winstate->nav_winobj->markptr =
 			tuplestore_alloc_read_pointer(winstate->buffer, 0);
@@ -2512,6 +2525,24 @@ 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 PREV.
+		 */
+		if (winstate->nav_winobj &&
+			winstate->rpPattern != NULL &&
+			winstate->navMaxOffset >= 0)
+		{
+			int64		navmarkpos;
+
+			if (winstate->currentpos > winstate->navMaxOffset)
+				navmarkpos = winstate->currentpos - winstate->navMaxOffset;
+			else
+				navmarkpos = 0;
+			if (navmarkpos > winstate->nav_winobj->markpos)
+				WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
+		}
+
 		/*
 		 * Truncate any no-longer-needed rows from the tuplestore.
 		 */
@@ -2960,6 +2991,10 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->rpSkipTo = node->rpSkipTo;
 	/* Set up row pattern recognition PATTERN clause (compiled NFA) */
 	winstate->rpPattern = node->rpPattern;
+	/* Set up max PREV offset for tuplestore trim */
+	winstate->navMaxOffset = node->navMaxOffset;
+	if (winstate->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
+		eval_nav_max_offset(winstate, node->defineClause);
 
 	/* Calculate NFA state size and allocate cycle detection bitmap */
 	if (node->rpPattern != NULL)
@@ -3872,6 +3907,87 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
 	mbp[bpos] = mb;
 }
 
+/*
+ * collect_prev_offset_walker
+ *		Walk expression tree to collect PREV offset_arg expressions.
+ */
+static bool
+collect_prev_offset_walker(Node *node, List **offsets)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		RPRNavExpr *nav = (RPRNavExpr *) node;
+
+		if (nav->kind == RPR_NAV_PREV && nav->offset_arg != NULL)
+			*offsets = lappend(*offsets, nav->offset_arg);
+
+		/* Don't walk into RPRNavExpr children */
+		return false;
+	}
+
+	return expression_tree_walker(node, collect_prev_offset_walker, offsets);
+}
+
+/*
+ * eval_nav_max_offset
+ *		Evaluate non-constant PREV offsets at executor init time.
+ *
+ * Called when the planner set navMaxOffset to RPR_NAV_OFFSET_NEEDS_EVAL
+ * because some PREV offset contains a parameter or non-foldable expression.
+ * Walks the original defineClause expression trees, compiles and evaluates
+ * each PREV offset_arg, and stores the maximum in winstate->navMaxOffset.
+ */
+static void
+eval_nav_max_offset(WindowAggState *winstate, List *defineClause)
+{
+	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	List	   *offsets = NIL;
+	ListCell   *lc;
+	int64		maxOffset = 0;
+
+	/* Collect all PREV offset expressions from DEFINE clause */
+	foreach(lc, defineClause)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+		collect_prev_offset_walker((Node *) te->expr, &offsets);
+	}
+
+	/* Evaluate each offset and find maximum */
+	foreach(lc, offsets)
+	{
+		Expr	   *offset_expr = (Expr *) lfirst(lc);
+		ExprState  *estate;
+		Datum		val;
+		bool		isnull;
+		int64		offset;
+
+		estate = ExecInitExpr(offset_expr, (PlanState *) winstate);
+		val = ExecEvalExprSwitchContext(estate, econtext, &isnull);
+
+		/*
+		 * NULL or negative offsets will cause a runtime error when PREV is
+		 * actually evaluated.  For trim purposes, treat them as 0.
+		 */
+		if (isnull)
+			continue;
+
+		offset = DatumGetInt64(val);
+		if (offset < 0)
+			continue;
+
+		if (offset > maxOffset)
+			maxOffset = offset;
+	}
+
+	winstate->navMaxOffset = maxOffset;
+
+	list_free(offsets);
+}
+
 /*
  * rpr_is_defined
  * return true if Row pattern recognition is defined.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 9ac24cc222d..ee2d53b5924 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2461,6 +2461,104 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	return plan;
 }
 
+/*
+ * nav_max_offset_walker
+ *		Walk expression tree to find the maximum PREV offset.
+ *
+ * Only PREV is relevant for tuplestore trim since it looks backward;
+ * NEXT looks forward and never references already-trimmed rows.
+ *
+ * Returns true (to stop walking) if a non-constant PREV offset is found,
+ * in which case *maxOffset is set to -1.  Otherwise accumulates the
+ * maximum constant offset value.
+ */
+static bool
+nav_max_offset_walker(Node *node, int64 *maxOffset)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		RPRNavExpr *nav = (RPRNavExpr *) node;
+
+		/* Only PREV looks backward; NEXT is irrelevant for trim */
+		if (nav->kind == RPR_NAV_PREV)
+		{
+			int64		offset;
+
+			if (nav->offset_arg == NULL)
+			{
+				/* 1-arg form: implicit offset of 1 */
+				offset = 1;
+			}
+			else if (IsA(nav->offset_arg, Const))
+			{
+				Const	   *c = (Const *) nav->offset_arg;
+
+				if (c->constisnull)
+				{
+					/*
+					 * NULL offset causes a runtime error, so this path is
+					 * never actually reached during execution.  Use 0 as a
+					 * safe placeholder for planning purposes.
+					 */
+					offset = 0;
+				}
+				else
+				{
+					offset = DatumGetInt64(c->constvalue);
+					if (offset < 0)
+						offset = 0; /* negative offset causes runtime error */
+				}
+			}
+			else
+			{
+				/*
+				 * Non-constant offset (Param, stable function, etc.). The
+				 * parser guarantees offset is a runtime constant, so it can
+				 * be evaluated at executor init time.
+				 */
+				*maxOffset = RPR_NAV_OFFSET_NEEDS_EVAL;
+				return true;	/* stop walking */
+			}
+
+			if (offset > *maxOffset)
+				*maxOffset = offset;
+		}
+
+		/* Don't walk into RPRNavExpr children - offset_arg already handled */
+		return false;
+	}
+
+	return expression_tree_walker(node, nav_max_offset_walker, maxOffset);
+}
+
+/*
+ * compute_nav_max_offset
+ *		Compute the maximum PREV offset from DEFINE clause expressions.
+ *
+ * Returns the maximum constant offset found, or -1 if any PREV offset
+ * cannot be determined statically.  NEXT offsets are ignored since they
+ * look forward and don't affect tuplestore trim.
+ */
+static int64
+compute_nav_max_offset(List *defineClause)
+{
+	int64		maxOffset = 0;
+	ListCell   *lc;
+
+	foreach(lc, defineClause)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+		if (nav_max_offset_walker((Node *) te->expr, &maxOffset))
+			return RPR_NAV_OFFSET_NEEDS_EVAL;
+	}
+
+	return maxOffset;
+}
+
 /*
  * create_windowagg_plan
  *
@@ -6678,6 +6776,9 @@ make_windowagg(List *tlist, WindowClause *wc,
 
 	node->defineClause = defineClause;
 
+	/* Compute max PREV offset for tuplestore trim optimization */
+	node->navMaxOffset = compute_nav_max_offset(defineClause);
+
 	plan->targetlist = tlist;
 	plan->lefttree = lefttree;
 	plan->righttree = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 74a6b682132..ff6d7d70a60 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2692,6 +2692,7 @@ typedef struct WindowAggState
 	TupleTableSlot *temp_slot_2;
 
 	/* RPR navigation */
+	int64		navMaxOffset;	/* max PREV offset; see RPR_NAV_OFFSET_* */
 	struct WindowObjectData *nav_winobj;	/* winobj for RPR nav fetch */
 	int64		nav_slot_pos;	/* position cached in nav_slot, or -1 */
 	TupleTableSlot *nav_slot;	/* slot for PREV/NEXT target row */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index ceaab4d97b0..27a2e7b48c7 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1386,6 +1386,15 @@ typedef struct WindowAgg
 	/* Row Pattern DEFINE clause (list of TargetEntry) */
 	List	   *defineClause;
 
+	/*
+	 * Maximum PREV offset for tuplestore mark optimization. >= 0: statically
+	 * determined max offset (mark = currentpos - offset).
+	 * RPR_NAV_OFFSET_NEEDS_EVAL: has non-constant offset; evaluate at
+	 * executor init.  RPR_NAV_OFFSET_RETAIN_ALL: must retain entire partition
+	 * (no trim possible).
+	 */
+	int64		navMaxOffset;
+
 	/*
 	 * false for all apart from the WindowAgg that's closest to the root of
 	 * the plan
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index e78092678bb..91148f6c574 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -55,6 +55,15 @@
 #define RPRElemIsFin(e)			((e)->varId == RPR_VARID_FIN)
 #define RPRElemCanSkip(e)		((e)->min == 0)
 
+/*
+ * navMaxOffset sentinel values.
+ * Non-negative values represent a statically determined maximum PREV offset.
+ */
+#define RPR_NAV_OFFSET_NEEDS_EVAL	(-1)	/* has non-constant PREV offset;
+											 * evaluate at executor init */
+#define RPR_NAV_OFFSET_RETAIN_ALL	(-2)	/* must retain entire partition
+											 * (e.g., future FIRST/LAST) */
+
 extern List *collectPatternVariables(RPRPatternNode *pattern);
 extern void buildDefineVariableList(List *defineClause,
 									List **defineVariableList);
-- 
2.50.1 (Apple Git-155)

From cddb155a9d06f3813f874105c7f66b802652586b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 16:06:06 +0900
Subject: [PATCH] Update RPR code comments to reflect 1-slot navigation model

---
 src/backend/executor/execRPR.c | 45 ++++++++++++++++++++++------------
 src/backend/parser/parse_rpr.c |  3 ++-
 2 files changed, 32 insertions(+), 16 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 8f0457e2b3c..329cbfbaaaa 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -191,10 +191,15 @@
  * transformDefineClause() processes each DEFINE variable as follows:
  *
  *   (1) Checks for duplicate variable names
- *   (2) Transforms the expression into a standard SQL expression
- *   (3) Coerces to Boolean type (coerce_to_boolean)
+ *   (2) Transforms the expression via transformExpr()
+ *   (3) Extracts Var nodes via pull_var_clause() and ensures each is
+ *       present in the query targetlist, so the planner propagates the
+ *       referenced columns through the plan tree
  *   (4) Wraps in a TargetEntry with the variable name set in resname
  *
+ * After all variables are processed:
+ *   (5) Coerces each expression to Boolean type (coerce_to_boolean)
+ *
  * Variables that are used in PATTERN but not defined in DEFINE are implicitly
  * evaluated as TRUE (matching all rows).
  *
@@ -431,8 +436,9 @@
  *
  *   Case 1: Simple VAR+ (e.g., A+)
  *           -> ABSORBABLE | ABSORBABLE_BRANCH set on the VAR
- *   Case 2: GROUP+ whose body consists only of {1,1} VARs (e.g., (A B)+)
- *           -> ABSORBABLE_BRANCH on children,
+ *   Case 2: GROUP+ with fixed-length children (min == max, recursively)
+ *           e.g., (A B)+, (A B{2})+, ((A (B C){2}){2})+
+ *           -> ABSORBABLE_BRANCH on all elements within the group,
  *             ABSORBABLE | ABSORBABLE_BRANCH on END
  *   Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
  *           -> Recurses from BEGIN into the body, applying Case 1.
@@ -604,11 +610,17 @@
  *       varMatched[i] = (not null and true)
  *
  * To support row navigation operators such as PREV() and NEXT(),
- * the previous row, current row, and next row are set in separate slots:
+ * a 1-slot model is used: only ecxt_outertuple is set to the current
+ * row.  PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE
+ * opcodes emitted during DEFINE expression compilation:
+ *
+ *   NAV_SET:     save ecxt_outertuple, swap in target row via nav_slot
+ *   (evaluate):  argument expression reads from swapped slot
+ *   NAV_RESTORE: restore original ecxt_outertuple
  *
- *   ecxt_scantuple  = previous row (for PREV reference)
- *   ecxt_outertuple = current row  (default reference)
- *   ecxt_innertuple = next row     (for NEXT reference)
+ * nav_slot caches the last fetched position (nav_slot_pos) to avoid
+ * redundant tuplestore lookups when multiple PREV/NEXT calls target
+ * the same row.
  *
  * The varMatched array is referenced later in Phase 1 (Match).
  *
@@ -908,8 +920,11 @@
  *
  *   (2) At runtime: initialize the nfaVisitedElems bitmap immediately before
  *       DFS expansion of each state within advance (once per state).
- *       During DFS, set the corresponding elemIdx bit when visiting each
- *       element.
+ *       During DFS, epsilon elements (END, ALT, BEGIN) are marked in the
+ *       bitmap at nfa_advance_state entry.  VAR elements are marked later
+ *       when added to the state list (nfa_add_state_unique), so that
+ *       legitimate loop-back to the same VAR in a new group iteration
+ *       (e.g., END -> ALT -> same VAR) is not blocked.
  *       If a previously visited elemIdx is revisited, that path is terminated.
  *
  *   Note: the bitmap tracks only elemIdx and does not consider counts.
@@ -1216,11 +1231,11 @@
  *   (3) State Deduplication (IX-5)
  *
  *     During advance, DFS may generate states with the same (elemIdx,
- *     counts) combination through multiple paths. Additionally, unlike
- *     VAR repetition, group repetition cannot perform absorption
- *     comparison using VAR states, so inline advance is performed from
- *     after Phase 1 match through to END; this process can also produce
- *     duplicate states reaching the same END.
+ *     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).
+ *     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/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3fb5d94abe9..d1e02e52e53 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -265,7 +265,8 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  *
  * Then for each DEFINE variable:
  *   2. Checks for duplicate variable names in DEFINE clause
- *   3. Transforms expressions and adds to targetlist via findTargetlistEntrySQL99
+ *   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
-- 
2.50.1 (Apple Git-155)

From 33a8de8aeb8c022cac0204f0b1fc84bcdc63570e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 5 Apr 2026 00:56:20 +0900
Subject: [PATCH] Implement FIRST/LAST navigation for RPR

Add FIRST(expr [, offset]) and LAST(expr [, offset]) navigation
functions for DEFINE clauses.  FIRST references the match start row,
LAST references the current row.  Includes slot swap elision,
per-context DEFINE re-evaluation for match_start-dependent variables,
and planner-level absorption disabling when FIRST or LAST-with-offset
is present.
---
 src/backend/executor/execExpr.c           |  16 +-
 src/backend/executor/execExprInterp.c     |  54 +++++--
 src/backend/executor/execRPR.c            |  78 ++++++++-
 src/backend/executor/nodeWindowAgg.c      |  28 ++--
 src/backend/optimizer/plan/createplan.c   |  95 ++++++++++-
 src/backend/optimizer/plan/rpr.c          |   6 +-
 src/backend/parser/parse_func.c           |  70 ++++++--
 src/backend/parser/parse_rpr.c            |  56 ++++++-
 src/backend/utils/adt/ruleutils.c         |  19 ++-
 src/backend/utils/adt/windowfuncs.c       |  56 +++++++
 src/include/catalog/pg_proc.dat           |  12 ++
 src/include/executor/execExpr.h           |   2 +-
 src/include/nodes/execnodes.h             |   9 +-
 src/include/nodes/plannodes.h             |  13 +-
 src/include/nodes/primnodes.h             |  11 +-
 src/include/optimizer/rpr.h               |  10 +-
 src/test/regress/expected/rpr.out         | 186 ++++++++++++++++++++--
 src/test/regress/expected/rpr_base.out    |  77 ++++++++-
 src/test/regress/expected/rpr_explain.out |  83 ++++++++++
 src/test/regress/sql/rpr.sql              | 100 ++++++++++++
 src/test/regress/sql/rpr_base.sql         |  49 +++++-
 src/test/regress/sql/rpr_explain.sql      |  49 ++++++
 22 files changed, 990 insertions(+), 89 deletions(-)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index dbed4f48a0f..af2e915701e 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1225,12 +1225,16 @@ ExecInitExprRec(Expr *node, ExprState *state,
 		case T_RPRNavExpr:
 			{
 				/*
-				 * RPR navigation functions (PREV/NEXT) are compiled into
-				 * EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE opcodes instead of
-				 * a normal function call.  The SET opcode swaps
-				 * ecxt_outertuple to the target row, the argument expression
-				 * is compiled normally (reads from the swapped slot), and the
-				 * RESTORE opcode restores the original slot.
+				 * 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.  The SET opcode
+				 * swaps ecxt_outertuple to the target row, the argument
+				 * expression is compiled normally (reads from the swapped
+				 * slot), and the RESTORE opcode restores the original slot.
+				 *
+				 * Default offset when offset_arg is NULL: PREV/NEXT: 1
+				 * (physical offset from currentpos) FIRST/LAST: 0 (logical
+				 * offset from match boundary)
 				 */
 				RPRNavExpr *nav = (RPRNavExpr *) node;
 				WindowAggState *winstate;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index e41faa95be3..88c5e3b7635 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -5942,7 +5942,7 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
 }
 
 /*
- * Evaluate RPR PREV/NEXT navigation: swap slot to target row.
+ * Evaluate RPR navigation (PREV/NEXT/FIRST/LAST): swap slot to target row.
  *
  * Saves the current outertuple into winstate for later restore, computes
  * the target row position, fetches the corresponding slot from the
@@ -5963,27 +5963,36 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	winstate->nav_saved_outertuple = econtext->ecxt_outertuple;
 
 	/*
-	 * Determine the unsigned offset.  For 2-arg PREV/NEXT the offset
-	 * expression has already been evaluated into offset_value.  NULL or
-	 * negative offsets are errors per the SQL standard (ISO/IEC 9075-2,
-	 * Subclause 5.6.2).
+	 * Determine the unsigned offset.  For 2-arg forms the offset expression
+	 * has already been evaluated into offset_value.  NULL or negative offsets
+	 * are errors per the SQL standard.
+	 *
+	 * Default offset when offset_arg is NULL: PREV/NEXT: 1 (standard 5.6.2)
+	 * FIRST/LAST: 0 (standard 5.6.3)
 	 */
 	if (op->d.rpr_nav.offset_value != NULL)
 	{
 		if (*op->d.rpr_nav.offset_isnull)
 			ereport(ERROR,
 					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-					 errmsg("PREV/NEXT offset must not be null")));
+					 errmsg("row pattern navigation offset must not be null")));
 
 		offset = DatumGetInt64(*op->d.rpr_nav.offset_value);
 
 		if (offset < 0)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("PREV/NEXT offset must not be negative")));
+					 errmsg("row pattern navigation offset must not be negative")));
 	}
 	else
-		offset = 1;
+	{
+		/* Default offset: 1 for PREV/NEXT, 0 for FIRST/LAST */
+		if (op->d.rpr_nav.kind == RPR_NAV_FIRST ||
+			op->d.rpr_nav.kind == RPR_NAV_LAST)
+			offset = 0;
+		else
+			offset = 1;
+	}
 
 	/*
 	 * Calculate target position based on navigation direction.  On overflow,
@@ -5999,8 +6008,33 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 			if (pg_add_s64_overflow(winstate->currentpos, offset, &target_pos))
 				target_pos = -1;
 			break;
+		case RPR_NAV_FIRST:
+			/* FIRST: offset from match_start, clamped to currentpos */
+			if (pg_add_s64_overflow(winstate->nav_match_start, offset, &target_pos))
+				target_pos = -1;
+			else if (target_pos > winstate->currentpos)
+				target_pos = -1;	/* beyond current match range */
+			break;
+		case RPR_NAV_LAST:
+			/* LAST: offset backward from currentpos, clamped to match_start */
+			if (pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos))
+				target_pos = -1;
+			else if (target_pos < winstate->nav_match_start)
+				target_pos = -1;	/* before match_start */
+			break;
 	}
 
+	/*
+	 * Slot swap elision: if target_pos is the current row, skip the
+	 * tuplestore fetch and slot swap entirely.  This benefits LAST(expr),
+	 * PREV(expr, 0), NEXT(expr, 0), and similar cases.
+	 *
+	 * We must still set nav_saved_outertuple (done above) so that
+	 * EEOP_RPR_NAV_RESTORE is a harmless no-op.
+	 */
+	if (target_pos == winstate->currentpos)
+		return;
+
 	/* Fetch target row slot (returns nav_null_slot if out of range) */
 	target_slot = ExecRPRNavGetSlot(winstate, target_pos);
 
@@ -6015,9 +6049,11 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 }
 
 /*
- * Evaluate RPR PREV/NEXT navigation: restore slot to original row.
+ * Evaluate RPR navigation: restore slot to original row.
  *
  * Restores econtext->ecxt_outertuple from the saved slot in winstate.
+ * When slot swap was elided (target == currentpos), this is a harmless
+ * no-op since saved and current slots are identical.
  * The caller is responsible for updating any local slot cache.
  */
 void
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 329cbfbaaaa..10e06a30f8e 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -609,17 +609,17 @@
  *       result = ExecEvalExpr(defineClause[i])
  *       varMatched[i] = (not null and true)
  *
- * To support row navigation operators such as PREV() and NEXT(),
+ * To support row navigation operators (PREV, NEXT, FIRST, LAST),
  * a 1-slot model is used: only ecxt_outertuple is set to the current
- * row.  PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE
- * opcodes emitted during DEFINE expression compilation:
+ * row.  Navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
+ * emitted during DEFINE expression compilation:
  *
  *   NAV_SET:     save ecxt_outertuple, swap in target row via nav_slot
  *   (evaluate):  argument expression reads from swapped slot
  *   NAV_RESTORE: restore original ecxt_outertuple
  *
  * nav_slot caches the last fetched position (nav_slot_pos) to avoid
- * redundant tuplestore lookups when multiple PREV/NEXT calls target
+ * redundant tuplestore lookups when multiple navigation calls target
  * the same row.
  *
  * The varMatched array is referenced later in Phase 1 (Match).
@@ -2997,12 +2997,63 @@ ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
  *   2. Absorb redundant contexts - ideal timing after convergence
  *   3. Advance all contexts (divergence) - create new states for next row
  */
+/*
+ * nfa_reevaluate_dependent_vars
+ *		Re-evaluate match_start-dependent DEFINE variables for a specific
+ *		context whose matchStartRow differs from the shared evaluation's
+ *		nav_match_start.
+ *
+ * Only variables in defineMatchStartDependent are re-evaluated.  The
+ * current row's slot (ecxt_outertuple) must already be set up by
+ * nfa_evaluate_row().
+ */
+static void
+nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
+							  int64 currentPos)
+{
+	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	int64		saved_match_start = winstate->nav_match_start;
+	int64		saved_pos = winstate->currentpos;
+	int			varIdx = 0;
+	ListCell   *lc;
+
+	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
+	winstate->nav_match_start = ctx->matchStartRow;
+	winstate->currentpos = currentPos;
+
+	/* Invalidate nav_slot cache since match_start changed */
+	winstate->nav_slot_pos = -1;
+
+	foreach(lc, winstate->defineClauseList)
+	{
+		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
+		{
+			ExprState  *exprState = (ExprState *) lfirst(lc);
+			Datum		result;
+			bool		isnull;
+
+			result = ExecEvalExpr(exprState, econtext, &isnull);
+			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
+		}
+
+		varIdx++;
+		if (varIdx >= list_length(winstate->defineVariableList))
+			break;
+	}
+
+	/* Restore original match_start, currentpos, and invalidate cache */
+	winstate->nav_match_start = saved_match_start;
+	winstate->currentpos = saved_pos;
+	winstate->nav_slot_pos = -1;
+}
+
 void
 ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 				  bool hasLimitedFrame, int64 frameOffset)
 {
 	RPRNFAContext *ctx;
 	bool	   *varMatched = winstate->nfaVarMatched;
+	bool		hasDependent = !bms_is_empty(winstate->defineMatchStartDependent);
 
 	/* Allow query cancellation once per row for simple/low-state patterns */
 	CHECK_FOR_INTERRUPTS();
@@ -3029,7 +3080,24 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 			}
 		}
 
-		nfa_match(winstate, ctx, varMatched);
+		/*
+		 * If this context has a different matchStartRow than the one used in
+		 * the shared evaluation, re-evaluate match_start-dependent variables
+		 * with this context's matchStartRow.
+		 */
+		if (hasDependent && ctx->matchStartRow != winstate->nav_match_start)
+		{
+			nfa_reevaluate_dependent_vars(winstate, ctx, currentPos);
+			nfa_match(winstate, ctx, varMatched);
+
+			/* Restore shared varMatched values for dependent variables */
+			nfa_reevaluate_dependent_vars(winstate, winstate->nfaContext,
+										  currentPos);
+		}
+		else
+		{
+			nfa_match(winstate, ctx, varMatched);
+		}
 		ctx->lastProcessedRow = currentPos;
 	}
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 186029df1d9..867ff5068e3 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -938,9 +938,9 @@ eval_windowaggregates(WindowAggState *winstate)
 		if (rpr_is_defined(winstate))
 		{
 			/*
-			 * If RPR is used, PREV may need to look at rows before the frame
-			 * head.  Adjust mark by navMaxOffset if known, otherwise retain
-			 * from position 0.
+			 * If RPR is used, navigation may need to look at rows before the
+			 * frame head.  Adjust mark by navMaxOffset if known, otherwise
+			 * retain from position 0.
 			 */
 			if (winstate->navMaxOffset >= 0)
 			{
@@ -1279,7 +1279,7 @@ prepare_tuplestore(WindowAggState *winstate)
 	if (winstate->nav_winobj)
 	{
 		/*
-		 * Allocate mark and read pointers for PREV/NEXT navigation.
+		 * Allocate mark and read pointers for RPR navigation.
 		 *
 		 * If navMaxOffset >= 0, we advance the mark to (currentpos -
 		 * navMaxOffset) as rows are processed, allowing tuplestore_trim() to
@@ -2527,7 +2527,7 @@ ExecWindowAgg(PlanState *pstate)
 
 		/*
 		 * Advance RPR navigation mark pointer if possible, so that
-		 * tuplestore_trim() can free rows no longer reachable by PREV.
+		 * tuplestore_trim() can free rows no longer reachable by navigation.
 		 */
 		if (winstate->nav_winobj &&
 			winstate->rpPattern != NULL &&
@@ -2782,6 +2782,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->nav_null_slot = ExecStoreAllNullTuple(winstate->nav_null_slot);
 
 		winstate->nav_saved_outertuple = NULL;
+		winstate->nav_match_start = 0;
 	}
 
 	/*
@@ -2991,11 +2992,14 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->rpSkipTo = node->rpSkipTo;
 	/* Set up row pattern recognition PATTERN clause (compiled NFA) */
 	winstate->rpPattern = node->rpPattern;
-	/* Set up max PREV offset for tuplestore trim */
+	/* Set up max backward nav offset for tuplestore trim */
 	winstate->navMaxOffset = node->navMaxOffset;
 	if (winstate->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
 		eval_nav_max_offset(winstate, node->defineClause);
 
+	/* Copy match_start dependency bitmapset for per-context evaluation */
+	winstate->defineMatchStartDependent = bms_copy(node->defineMatchStartDependent);
+
 	/* Calculate NFA state size and allocate cycle detection bitmap */
 	if (node->rpPattern != NULL)
 	{
@@ -4215,8 +4219,14 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 
 		/*
 		 * Evaluate variables for this row - done only once, shared by all
-		 * contexts
+		 * 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 */
@@ -4304,8 +4314,8 @@ register_result:
  * varMatched[i] = true if variable i matched at current row.
  *
  * Uses 1-slot model: only ecxt_outertuple is set to the current row.
- * PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
- * during expression evaluation, which temporarily swap the slot.
+ * PREV/NEXT/FIRST/LAST navigation is handled by EEOP_RPR_NAV_SET/RESTORE
+ * opcodes during expression evaluation, which temporarily swap the slot.
  */
 static bool
 nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ee2d53b5924..c4c40d24cf2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -292,6 +292,7 @@ static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 List *runCondition, RPSkipTo rpSkipTo,
 								 RPRPattern *compiledPattern,
 								 List *defineClause,
+								 Bitmapset *defineMatchStartDependent,
 								 List *qual, bool topWindow,
 								 Plan *lefttree);
 static Group *make_group(List *tlist, List *qual, int numGroupCols,
@@ -2482,6 +2483,20 @@ nav_max_offset_walker(Node *node, int64 *maxOffset)
 	{
 		RPRNavExpr *nav = (RPRNavExpr *) node;
 
+		/*
+		 * FIRST always references match_start, so we must retain the entire
+		 * partition.  LAST with an explicit offset can look back to
+		 * match_start for the boundary check, so same treatment. LAST without
+		 * offset always resolves to currentpos and never looks backward —
+		 * no impact on trim.
+		 */
+		if (nav->kind == RPR_NAV_FIRST ||
+			(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
+		{
+			*maxOffset = RPR_NAV_OFFSET_RETAIN_ALL;
+			return true;		/* stop walking */
+		}
+
 		/* Only PREV looks backward; NEXT is irrelevant for trim */
 		if (nav->kind == RPR_NAV_PREV)
 		{
@@ -2538,9 +2553,10 @@ nav_max_offset_walker(Node *node, int64 *maxOffset)
  * compute_nav_max_offset
  *		Compute the maximum PREV offset from DEFINE clause expressions.
  *
- * Returns the maximum constant offset found, or -1 if any PREV offset
- * cannot be determined statically.  NEXT offsets are ignored since they
- * look forward and don't affect tuplestore trim.
+ * Returns the maximum constant offset found, RPR_NAV_OFFSET_NEEDS_EVAL if
+ * any PREV offset cannot be determined statically, or
+ * RPR_NAV_OFFSET_RETAIN_ALL if FIRST/LAST is present.  NEXT offsets are
+ * ignored since they look forward and don't affect tuplestore trim.
  */
 static int64
 compute_nav_max_offset(List *defineClause)
@@ -2559,6 +2575,65 @@ compute_nav_max_offset(List *defineClause)
 	return maxOffset;
 }
 
+/*
+ * has_match_start_dependency
+ *		Check if an expression tree contains FIRST or LAST-with-offset,
+ *		which depend on match_start and require per-context evaluation.
+ *
+ * LAST without offset always resolves to currentpos and is
+ * match_start-independent.
+ */
+static bool
+has_match_start_dependency(Node *node)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		RPRNavExpr *nav = (RPRNavExpr *) node;
+
+		if (nav->kind == RPR_NAV_FIRST)
+			return true;
+		if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
+			return true;
+
+		/* Check children (arg may contain further nav expressions) */
+		return has_match_start_dependency((Node *) nav->arg);
+	}
+
+	return expression_tree_walker(node, has_match_start_dependency, NULL);
+}
+
+/*
+ * compute_match_start_dependent
+ *		Build a Bitmapset of DEFINE variable indices whose expressions
+ *		depend on match_start (contain FIRST or LAST-with-offset).
+ *
+ * Variables in this set require per-context re-evaluation during NFA
+ * processing, because different contexts may have different match_start
+ * values.
+ */
+static Bitmapset *
+compute_match_start_dependent(List *defineClause)
+{
+	Bitmapset  *result = NULL;
+	ListCell   *lc;
+	int			varIdx = 0;
+
+	foreach(lc, defineClause)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+		if (has_match_start_dependency((Node *) te->expr))
+			result = bms_add_member(result, varIdx);
+
+		varIdx++;
+	}
+
+	return result;
+}
+
 /*
  * create_windowagg_plan
  *
@@ -2586,6 +2661,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 	List	   *defineVariableList = NIL;
 	List	   *filteredDefineClause = NIL;
 	RPRPattern *compiledPattern = NULL;
+	Bitmapset  *matchStartDependent = NULL;

 	/*
@@ -2648,11 +2724,15 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		buildDefineVariableList(wc->defineClause, &defineVariableList);
 		filteredDefineClause = wc->defineClause;
 
+		/* Identify match_start-dependent DEFINE variables */
+		matchStartDependent = compute_match_start_dependent(wc->defineClause);
+
 		/* Compile and optimize RPR patterns */
 		compiledPattern = buildRPRPattern(wc->rpPattern,
 										  defineVariableList,
 										  wc->rpSkipTo,
-										  wc->frameOptions);
+										  wc->frameOptions,
+										  !bms_is_empty(matchStartDependent));
 	}
 
 	/* And finally we can make the WindowAgg node */
@@ -2670,6 +2750,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  wc->rpSkipTo,
 						  compiledPattern,
 						  filteredDefineClause,
+						  matchStartDependent,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -6742,6 +6823,7 @@ make_windowagg(List *tlist, WindowClause *wc,
 			   List *runCondition, RPSkipTo rpSkipTo,
 			   RPRPattern *compiledPattern,
 			   List *defineClause,
+			   Bitmapset *defineMatchStartDependent,
 			   List *qual, bool topWindow, Plan *lefttree)
 {
 	WindowAgg  *node = makeNode(WindowAgg);
@@ -6776,7 +6858,10 @@ make_windowagg(List *tlist, WindowClause *wc,
 
 	node->defineClause = defineClause;
 
-	/* Compute max PREV offset for tuplestore trim optimization */
+	/* Store pre-computed match_start dependency bitmapset */
+	node->defineMatchStartDependent = defineMatchStartDependent;
+
+	/* Compute max backward nav offset for tuplestore trim optimization */
 	node->navMaxOffset = compute_nav_max_offset(defineClause);
 
 	plan->targetlist = tlist;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index c0e9d134aa9..767a214016c 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1893,7 +1893,8 @@ buildDefineVariableList(List *defineClause, List **defineVariableList)
  */
 RPRPattern *
 buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
-				RPSkipTo rpSkipTo, int frameOptions)
+				RPSkipTo rpSkipTo, int frameOptions,
+				bool hasMatchStartDependent)
 {
 	RPRPattern *result;
 	RPRPatternNode *optimized;
@@ -1947,7 +1948,8 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
 		!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
 
-	if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame)
+	if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame &&
+		!hasMatchStartDependent)
 	{
 		/* Runtime conditions met - check structural absorbability */
 		computeAbsorbability(result);
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index e14ff4dc494..aa45a98713f 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -757,35 +757,79 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	if (retset)
 		check_srf_call_placement(pstate, last_srf, location);
 
-	/* next() and prev() are only allowed in a WINDOW DEFINE clause */
+	/*
+	 * 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_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("%s can only be used in 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_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))
 	{
 		/*
-		 * PREV() and NEXT() 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.
+		 * 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.
 		 */
-		bool		is_next = (funcid == F_NEXT_ANYELEMENT ||
-							   funcid == F_NEXT_ANYELEMENT_INT8);
-		bool		has_offset = (funcid == F_PREV_ANYELEMENT_INT8 ||
-								  funcid == F_NEXT_ANYELEMENT_INT8);
-		RPRNavExpr *navexpr = makeNode(RPRNavExpr);
+		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 = is_next ? RPR_NAV_NEXT : RPR_NAV_PREV;
+		navexpr->kind = kind;
 		navexpr->arg = (Expr *) linitial(fargs);
 		navexpr->offset_arg = has_offset ? (Expr *) lsecond(fargs) : NULL;
 		navexpr->resulttype = rettype;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index d1e02e52e53..dab02fff4f2 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -339,8 +339,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		/*
 		 * Transform the DEFINE expression.  We must NOT add the whole
 		 * expression to the query targetlist, because it may contain
-		 * RPRNavExpr nodes (PREV/NEXT) that can only be evaluated inside the
-		 * owning WindowAgg.
+		 * 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
@@ -429,13 +429,20 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 /*
  * check_rpr_nav_expr
  *		Validate a single RPRNavExpr node by walking its arg and offset_arg
- *		subtrees in a single pass each.  Checks for nested PREV/NEXT, missing
+ *		subtrees in a single pass each.  Checks for illegal nesting, missing
  *		column references, and non-constant offset expressions.
+ *
+ * Nesting rules (SQL standard 5.6.4):
+ *   - PREV/NEXT wrapping FIRST/LAST: allowed (compound navigation)
+ *   - FIRST/LAST wrapping PREV/NEXT: prohibited
+ *   - Same-category nesting (PREV inside PREV, FIRST inside FIRST, etc.):
+ *     prohibited
  */
 typedef struct
 {
 	bool		has_nav;		/* RPRNavExpr found (nesting) */
 	bool		has_column_ref; /* Var found */
+	RPRNavKind	inner_kind;		/* kind of nested RPRNavExpr, if any */
 }			NavCheckResult;
 
 static bool
@@ -446,7 +453,10 @@ nav_check_walker(Node *node, void *context)
 	if (node == NULL)
 		return false;
 	if (IsA(node, RPRNavExpr))
+	{
 		result->has_nav = true;
+		result->inner_kind = ((RPRNavExpr *) node)->kind;
+	}
 	if (IsA(node, Var))
 		result->has_column_ref = true;
 
@@ -457,16 +467,46 @@ static void
 check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
 {
 	NavCheckResult result;
+	bool		outer_is_physical = (nav->kind == RPR_NAV_PREV ||
+									 nav->kind == RPR_NAV_NEXT);
 
 	/* Check arg subtree: nesting + column reference in one walk */
 	memset(&result, 0, sizeof(result));
 	(void) nav_check_walker((Node *) nav->arg, &result);
 
 	if (result.has_nav)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("PREV and NEXT cannot be nested"),
-				 parser_errposition(pstate, nav->location)));
+	{
+		bool		inner_is_physical = (result.inner_kind == RPR_NAV_PREV ||
+										 result.inner_kind == RPR_NAV_NEXT);
+
+		if (outer_is_physical && !inner_is_physical)
+		{
+			/*
+			 * PREV/NEXT wrapping FIRST/LAST: valid compound navigation per
+			 * SQL standard 5.6.4, but not yet implemented.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("compound row pattern navigation is not yet supported"),
+					 parser_errposition(pstate, nav->location)));
+		}
+		else if (!outer_is_physical && inner_is_physical)
+		{
+			/* FIRST/LAST wrapping PREV/NEXT: prohibited by standard */
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+					 parser_errposition(pstate, nav->location)));
+		}
+		else
+		{
+			/* Same-category nesting: prohibited */
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("row pattern navigation operations cannot be nested"),
+					 parser_errposition(pstate, nav->location)));
+		}
+	}
 	if (!result.has_column_ref)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -483,7 +523,7 @@ check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
 			contain_volatile_functions((Node *) nav->offset_arg))
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("PREV/NEXT offset must be a run-time constant"),
+					 errmsg("row pattern navigation offset must be a run-time constant"),
 					 parser_errposition(pstate, nav->location)));
 	}
 }
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 742d889154b..e9cf49ca210 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10109,9 +10109,24 @@ get_rule_expr(Node *node, deparse_context *context,
 		case T_RPRNavExpr:
 			{
 				RPRNavExpr *nav = (RPRNavExpr *) node;
+				const char *funcname;
 
-				appendStringInfoString(buf,
-									   nav->kind == RPR_NAV_PREV ? "PREV(" : "NEXT(");
+				switch (nav->kind)
+				{
+					case RPR_NAV_PREV:
+						funcname = "PREV(";
+						break;
+					case RPR_NAV_NEXT:
+						funcname = "NEXT(";
+						break;
+					case RPR_NAV_FIRST:
+						funcname = "FIRST(";
+						break;
+					case RPR_NAV_LAST:
+						funcname = "LAST(";
+						break;
+				}
+				appendStringInfoString(buf, funcname);
 				get_rule_expr((Node *) nav->arg, context, showimplicit);
 				if (nav->offset_arg != NULL)
 				{
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 091260d2cce..420a4962395 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -785,3 +785,59 @@ window_next_offset(PG_FUNCTION_ARGS)
 			 errmsg("next() can only be used in 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("first() can only be used in 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("last() can only be used in 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("first() can only be used in 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("last() can only be used in 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 8e95169b7b0..6d70ed23aeb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10984,6 +10984,18 @@
 { oid => '8129', descr => 'next value at offset',
   proname => 'next', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
   proargtypes => 'anyelement int8', prosrc => 'window_next_offset' },
+{ oid => '8130', descr => 'first value in match',
+  proname => 'first', provolatile => 's', prorettype => 'anyelement',
+  proargtypes => 'anyelement', prosrc => 'window_first' },
+{ oid => '8132', descr => 'first value in match at offset',
+  proname => 'first', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'anyelement int8', prosrc => 'window_first_offset' },
+{ oid => '8131', descr => 'last value in match',
+  proname => 'last', provolatile => 's', prorettype => 'anyelement',
+  proargtypes => 'anyelement', prosrc => 'window_last' },
+{ oid => '8133', descr => 'last value in match at offset',
+  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/include/executor/execExpr.h b/src/include/executor/execExpr.h
index fac37c96896..a34e1a8dfe6 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -699,7 +699,7 @@ typedef struct ExprEvalStep
 		struct
 		{
 			WindowAggState *winstate;
-			RPRNavKind	kind;	/* PREV or NEXT */
+			RPRNavKind	kind;	/* PREV, NEXT, FIRST, or LAST */
 			Datum	   *offset_value;	/* 2-arg: runtime offset value, or
 										 * NULL */
 			bool	   *offset_isnull;	/* 2-arg: runtime offset null flag */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ff6d7d70a60..d65cee1aefd 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2638,6 +2638,9 @@ typedef struct WindowAggState
 	Size		nfaStateSize;	/* pre-calculated RPRNFAState size */
 	bool	   *nfaVarMatched;	/* per-row cache: varMatched[varId] for varId
 								 * < numDefines */
+	Bitmapset  *defineMatchStartDependent;	/* DEFINE vars needing per-context
+											 * evaluation
+											 * (match_start-dependent) */
 	bitmapword *nfaVisitedElems;	/* elemIdx visited bitmap for cycle
 									 * detection */
 	int			nfaVisitedNWords;	/* number of bitmapwords in
@@ -2692,12 +2695,14 @@ typedef struct WindowAggState
 	TupleTableSlot *temp_slot_2;
 
 	/* RPR navigation */
-	int64		navMaxOffset;	/* max PREV offset; see RPR_NAV_OFFSET_* */
+	int64		navMaxOffset;	/* max backward nav offset; see
+								 * RPR_NAV_OFFSET_* */
 	struct WindowObjectData *nav_winobj;	/* winobj for RPR nav fetch */
 	int64		nav_slot_pos;	/* position cached in nav_slot, or -1 */
-	TupleTableSlot *nav_slot;	/* slot for PREV/NEXT target row */
+	TupleTableSlot *nav_slot;	/* slot for PREV/NEXT/FIRST/LAST target row */
 	TupleTableSlot *nav_saved_outertuple;	/* saved slot during nav swap */
 	TupleTableSlot *nav_null_slot;	/* all NULL slot */
+	int64		nav_match_start;	/* match_start for FIRST/LAST nav */
 
 	/* RPR current match result */
 	bool		rpr_match_valid;	/* true if a match result is set */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 27a2e7b48c7..a4b1e880fe4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1387,11 +1387,18 @@ typedef struct WindowAgg
 	List	   *defineClause;
 
 	/*
-	 * Maximum PREV offset for tuplestore mark optimization. >= 0: statically
-	 * determined max offset (mark = currentpos - offset).
+	 * Bitmapset of DEFINE variable indices whose expressions depend on
+	 * match_start (contain FIRST or LAST-with-offset).  Variables in this set
+	 * require per-context re-evaluation during NFA processing.
+	 */
+	Bitmapset  *defineMatchStartDependent;
+
+	/*
+	 * Maximum backward navigation offset for tuplestore mark optimization. >=
+	 * 0: statically determined max offset (mark = currentpos - offset).
 	 * RPR_NAV_OFFSET_NEEDS_EVAL: has non-constant offset; evaluate at
 	 * executor init.  RPR_NAV_OFFSET_RETAIN_ALL: must retain entire partition
-	 * (no trim possible).
+	 * (e.g., FIRST/LAST navigation).
 	 */
 	int64		navMaxOffset;
 
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 94723a3b909..0e40fad6291 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -651,25 +651,28 @@ typedef struct WindowFuncRunCondition
 /*
  * RPRNavExpr
  *
- * Represents a PREV() or NEXT() navigation call in an RPR DEFINE clause.
+ * Represents a PREV/NEXT/FIRST/LAST navigation call in an RPR DEFINE clause.
  * At expression compile time this is translated into EEOP_RPR_NAV_SET /
  * EEOP_RPR_NAV_RESTORE opcodes rather than a normal function call.
  *
- * kind:       RPR_NAV_PREV or RPR_NAV_NEXT
+ * kind:       RPR_NAV_PREV, RPR_NAV_NEXT, RPR_NAV_FIRST, or RPR_NAV_LAST
  * arg:        the expression to evaluate against the target row
  * offset_arg: optional explicit offset expression (2-arg form); NULL for
- *             the 1-arg form which uses an implicit offset of 1
+ *             the 1-arg form (implicit offset: 1 for PREV/NEXT, 0 for
+ *             FIRST/LAST)
  */
 typedef enum RPRNavKind
 {
 	RPR_NAV_PREV,
 	RPR_NAV_NEXT,
+	RPR_NAV_FIRST,
+	RPR_NAV_LAST,
 } RPRNavKind;
 
 typedef struct RPRNavExpr
 {
 	Expr		xpr;
-	RPRNavKind	kind;			/* PREV or NEXT */
+	RPRNavKind	kind;			/* PREV, NEXT, FIRST, or LAST */
 	Expr	   *arg;			/* argument expression */
 	Expr	   *offset_arg;		/* offset expression, or NULL for 1-arg form */
 	Oid			resulttype;		/* result type (same as arg's type) */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 91148f6c574..90e13ed7249 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -57,17 +57,19 @@
 
 /*
  * navMaxOffset sentinel values.
- * Non-negative values represent a statically determined maximum PREV offset.
+ * Non-negative values represent a statically determined maximum backward
+ * navigation offset (PREV).
  */
-#define RPR_NAV_OFFSET_NEEDS_EVAL	(-1)	/* has non-constant PREV offset;
+#define RPR_NAV_OFFSET_NEEDS_EVAL	(-1)	/* has non-constant offset;
 											 * evaluate at executor init */
 #define RPR_NAV_OFFSET_RETAIN_ALL	(-2)	/* must retain entire partition
-											 * (e.g., future FIRST/LAST) */
+											 * (e.g., FIRST/LAST navigation) */
 
 extern List *collectPatternVariables(RPRPatternNode *pattern);
 extern void buildDefineVariableList(List *defineClause,
 									List **defineVariableList);
 extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
-								   RPSkipTo rpSkipTo, int frameOptions);
+								   RPSkipTo rpSkipTo, int frameOptions,
+								   bool hasMatchStartDependent);
 
 #endif							/* OPTIMIZER_RPR_H */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index de6ce4fba8a..fb623ea152b 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1040,7 +1040,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > PREV(PREV(price))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  row pattern navigation operations cannot be nested
 LINE 7:     DEFINE A AS price > PREV(PREV(price))
                                 ^
 -- Nested NEXT
@@ -1052,7 +1052,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > NEXT(NEXT(price))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  row pattern navigation operations cannot be nested
 LINE 7:     DEFINE A AS price > NEXT(NEXT(price))
                                 ^
 -- PREV nested inside NEXT
@@ -1064,7 +1064,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > NEXT(PREV(price))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  row pattern navigation operations cannot be nested
 LINE 7:     DEFINE A AS price > NEXT(PREV(price))
                                 ^
 -- PREV nested inside expression inside NEXT
@@ -1076,7 +1076,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > NEXT(price * PREV(price))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  row pattern navigation operations cannot be nested
 LINE 7:     DEFINE A AS price > NEXT(price * PREV(price))
                                 ^
 -- Triple nesting: error reported at outermost PREV
@@ -1088,7 +1088,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > PREV(PREV(PREV(price)))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  row pattern navigation operations cannot be nested
 LINE 7:     DEFINE A AS price > PREV(PREV(PREV(price)))
                                 ^
 -- No column reference in PREV/NEXT argument
@@ -1137,7 +1137,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS PREV(price, price) > 0
 );
-ERROR:  PREV/NEXT offset must be a run-time constant
+ERROR:  row pattern navigation offset must be a run-time constant
 LINE 7:     DEFINE A AS PREV(price, price) > 0
                         ^
 -- Non-constant offset: volatile function as offset
@@ -1149,7 +1149,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS PREV(price, random()::int) > 0
 );
-ERROR:  PREV/NEXT offset must be a run-time constant
+ERROR:  row pattern navigation offset must be a run-time constant
 LINE 7:     DEFINE A AS PREV(price, random()::int) > 0
                         ^
 -- Non-constant offset: subquery as offset
@@ -1442,7 +1442,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price, -1) IS NOT NULL
 );
-ERROR:  PREV/NEXT offset must not be negative
+ERROR:  row pattern navigation offset must not be negative
 -- 2-arg PREV/NEXT: NULL offset (typed)
 SELECT company, tdate, price, first_value(price) OVER w
 FROM stock
@@ -1452,7 +1452,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price, NULL::int8) IS NOT NULL
 );
-ERROR:  PREV/NEXT offset must not be null
+ERROR:  row pattern navigation offset must not be null
 -- 2-arg PREV/NEXT: NULL offset (untyped)
 SELECT company, tdate, price, first_value(price) OVER w
 FROM stock
@@ -1462,7 +1462,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price, NULL) IS NOT NULL
 );
-ERROR:  PREV/NEXT offset must not be null
+ERROR:  row pattern navigation offset must not be null
 -- 2-arg PREV/NEXT: host variable negative and NULL
 PREPARE test_prev_offset(int8) AS
 SELECT company, tdate, price, first_value(price) OVER w
@@ -1474,9 +1474,9 @@ WINDOW w AS (
     DEFINE A AS price > PREV(price, $1)
 );
 EXECUTE test_prev_offset(-1);
-ERROR:  PREV/NEXT offset must not be negative
+ERROR:  row pattern navigation offset must not be negative
 EXECUTE test_prev_offset(NULL);
-ERROR:  PREV/NEXT offset must not be null
+ERROR:  row pattern navigation offset must not be null
 DEALLOCATE test_prev_offset;
 -- 2-arg PREV/NEXT: host variable with expression (0 + $1)
 PREPARE test_prev_offset(int8) AS
@@ -1489,9 +1489,9 @@ WINDOW w AS (
     DEFINE A AS price > PREV(price, 0 + $1)
 );
 EXECUTE test_prev_offset(-1);
-ERROR:  PREV/NEXT offset must not be negative
+ERROR:  row pattern navigation offset must not be negative
 EXECUTE test_prev_offset(NULL);
-ERROR:  PREV/NEXT offset must not be null
+ERROR:  row pattern navigation offset must not be null
 DEALLOCATE test_prev_offset;
 -- 2-arg: two PREV with different offsets in same DEFINE clause
 -- B: price exceeds both 1-back and 2-back values
@@ -1567,6 +1567,164 @@ WINDOW w AS (
  company2 | 07-10-2023 |  1300 |             |            |     0
 (20 rows)
 
+--
+-- FIRST/LAST navigation
+--
+-- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val)
+-- at specific positions.
+CREATE TEMP TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10);
+-- FIRST(val) = constant: B matches when match_start has val=10
+-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 → match {1,2}
+-- match_start=3(30): A=id3, B=id4, FIRST(val)=30≠10 → no match
+-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 → match {4,5}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS FIRST(val) = 10
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  2
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |  4 |  5
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- LAST(val): always equals current row's val (offset 0 default)
+-- Equivalent to: B AS val > 15
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS LAST(val) > 15
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  2
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |  4 |  5
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where
+-- first and last rows have the same val.
+-- match_start=1(10): reluctant tries B early:
+--   id2(20≠10), id3(30≠10), id4(10=10) → match {1,2,3,4}
+-- match_start=5(50): id6(10≠50) → no match
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+? B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  4
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |    |   
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- Greedy A+ with FIRST(val) = LAST(val): find longest match where
+-- first and last rows have the same val.
+-- match_start=1(10): greedy A eats all, B tries last:
+--   id6(10=10) → match {1,2,3,4,5,6}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  6
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |    |   
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts.
+-- With ONE ROW PER MATCH, each row shows only its first match result.
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+? B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  4
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |  4 |  6
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- FIRST/LAST outside DEFINE clause (error cases)
+SELECT first(val) FROM rpr_nav;
+ERROR:  first can only be used in a DEFINE clause
+LINE 1: SELECT first(val) FROM rpr_nav;
+               ^
+SELECT last(val) FROM rpr_nav;
+ERROR:  last can only be used in a DEFINE clause
+LINE 1: SELECT last(val) FROM rpr_nav;
+               ^
+SELECT first(val, 1) FROM rpr_nav;
+ERROR:  first can only be used in 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');
+SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f;
+ prev | next | first | last 
+------+------+-------+------
+    1 |    2 | Joe   | Blow
+(1 row)
+
+DROP TABLE rpr_names;
+-- Compound navigation: not yet supported
+SELECT id, val FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+ERROR:  compound row pattern navigation is not yet supported
+LINE 5:     DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+                                   ^
+-- Reverse nesting: FIRST wrapping PREV is prohibited
+SELECT id, val FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0
+);
+ERROR:  FIRST and LAST cannot contain PREV or NEXT
+LINE 5:     DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0
+                                   ^
+DROP TABLE rpr_nav;
 --
 -- SKIP TO / Backtracking / Frame boundary
 --
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 7452cf1b3ab..709d8f5d8e6 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1629,7 +1629,7 @@ LINE 6:     PATTERN (A{,2147483647})
 -- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 DROP TABLE rpr_bounds;
 -- ============================================================
--- Navigation Functions Tests (PREV / NEXT)
+-- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
 -- ============================================================
 CREATE TABLE rpr_nav (id INT, val INT);
 INSERT INTO rpr_nav VALUES
@@ -1730,6 +1730,81 @@ ERROR:  next can only be used in a DEFINE clause
 LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
                ^
 -- Expected: ERROR: next can only be used in a DEFINE clause
+-- FIRST function - reference match_start row
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS val > 0,
+        B AS val > FIRST(val)
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   5
+  2 |  20 |   0
+  3 |  15 |   0
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+-- LAST function without offset - equivalent to current row's value
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS val > 0,
+        B AS LAST(val) > PREV(val)
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   3
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+-- FIRST and LAST combined
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS val > 0,
+        B AS val > FIRST(val) AND LAST(val) > PREV(val)
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   3
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+-- FIRST function cannot be used other than in DEFINE
+SELECT FIRST(id), id, val FROM rpr_nav;
+ERROR:  first can only be used in a DEFINE clause
+LINE 1: SELECT FIRST(id), id, val FROM rpr_nav;
+               ^
+-- Expected: ERROR: first can only be used in a DEFINE clause
+-- LAST function cannot be used other than in DEFINE
+SELECT LAST(id), id, val FROM rpr_nav;
+ERROR:  last can only be used in a DEFINE clause
+LINE 1: SELECT LAST(id), id, val FROM rpr_nav;
+               ^
+-- Expected: ERROR: last can only be used in a DEFINE clause
 DROP TABLE rpr_nav;
 -- ============================================================
 -- SKIP TO / INITIAL Tests
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 4a646d1e1d8..83a035023ba 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -1086,6 +1086,89 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=42.00 loops=1)
 (9 rows)
 
+-- No absorption when DEFINE uses FIRST (match_start-dependent)
+-- Same pattern as rpr_ev_ctx_absorb_unbounded but with FIRST in DEFINE.
+-- Compare: absorbed count should be 0 here vs >0 above.
+CREATE VIEW rpr_ev_ctx_no_absorb_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v)
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 9 peak, 151 total, 0 merged
+   NFA Contexts: 5 peak, 51 total, 0 pruned
+   NFA: 10 matched (len 5/5/5.0), 0 mismatched
+   NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+-- Absorption preserved when DEFINE uses only LAST without offset
+-- LAST(v) is match_start-independent (always currentpos), so absorption
+-- remains active.  Compare: absorbed count should be >0, like the
+-- PREV-only case above.
+CREATE VIEW rpr_ev_ctx_absorb_last AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS LAST(v) % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_last'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS LAST(v) % 5 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+" b
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 91 total, 0 merged
+   NFA Contexts: 2 peak, 51 total, 0 pruned
+   NFA: 10 matched (len 5/5/5.0), 0 mismatched
+   NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
 -- ============================================================
 -- Match Length Statistics Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b3bbc8254c4..27929c47d7b 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -760,6 +760,106 @@ WINDOW w AS (
     DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
 );
 
+--
+-- FIRST/LAST navigation
+--
+
+-- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val)
+-- at specific positions.
+CREATE TEMP TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10);
+
+-- FIRST(val) = constant: B matches when match_start has val=10
+-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 → match {1,2}
+-- match_start=3(30): A=id3, B=id4, FIRST(val)=30≠10 → no match
+-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 → match {4,5}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS FIRST(val) = 10
+);
+
+-- LAST(val): always equals current row's val (offset 0 default)
+-- Equivalent to: B AS val > 15
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS LAST(val) > 15
+);
+
+-- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where
+-- first and last rows have the same val.
+-- match_start=1(10): reluctant tries B early:
+--   id2(20≠10), id3(30≠10), id4(10=10) → match {1,2,3,4}
+-- match_start=5(50): id6(10≠50) → no match
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+? B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- Greedy A+ with FIRST(val) = LAST(val): find longest match where
+-- first and last rows have the same val.
+-- match_start=1(10): greedy A eats all, B tries last:
+--   id6(10=10) → match {1,2,3,4,5,6}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts.
+-- With ONE ROW PER MATCH, each row shows only its first match result.
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+? B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- 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');
+SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f;
+DROP TABLE rpr_names;
+
+-- Compound navigation: not yet supported
+SELECT id, val FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+
+-- Reverse nesting: FIRST wrapping PREV is prohibited
+SELECT id, val FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0
+);
+
+DROP TABLE rpr_nav;
+
 --
 -- SKIP TO / Backtracking / Frame boundary
 --
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 8c23c7598a3..6ac248b3101 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1202,7 +1202,7 @@ WINDOW w AS (
 DROP TABLE rpr_bounds;
 
 -- ============================================================
--- Navigation Functions Tests (PREV / NEXT)
+-- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
 -- ============================================================

@@ -1278,6 +1278,53 @@ WINDOW w AS (
 ORDER BY id;
 -- Expected: ERROR: next can only be used in a DEFINE clause
 
+-- FIRST function - reference match_start row
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS val > 0,
+        B AS val > FIRST(val)
+)
+ORDER BY id;
+
+-- LAST function without offset - equivalent to current row's value
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS val > 0,
+        B AS LAST(val) > PREV(val)
+)
+ORDER BY id;
+
+-- FIRST and LAST combined
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS val > 0,
+        B AS val > FIRST(val) AND LAST(val) > PREV(val)
+)
+ORDER BY id;
+
+-- FIRST function cannot be used other than in DEFINE
+SELECT FIRST(id), id, val FROM rpr_nav;
+-- Expected: ERROR: first can only be used in a DEFINE clause
+
+-- LAST function cannot be used other than in DEFINE
+SELECT LAST(id), id, val FROM rpr_nav;
+-- Expected: ERROR: last can only be used in a DEFINE clause
+
 DROP TABLE rpr_nav;
 
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index aed2f69bc4f..9aed4f72070 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -692,6 +692,55 @@ WINDOW w AS (
            C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
 );');
 
+-- No absorption when DEFINE uses FIRST (match_start-dependent)
+-- Same pattern as rpr_ev_ctx_absorb_unbounded but with FIRST in DEFINE.
+-- Compare: absorbed count should be 0 here vs >0 above.
+CREATE VIEW rpr_ev_ctx_no_absorb_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v)
+);');
+
+-- Absorption preserved when DEFINE uses only LAST without offset
+-- LAST(v) is match_start-independent (always currentpos), so absorption
+-- remains active.  Compare: absorbed count should be >0, like the
+-- PREV-only case above.
+CREATE VIEW rpr_ev_ctx_absorb_last AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS LAST(v) % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_last'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS LAST(v) % 5 = 0
+);');
+
 -- ============================================================
 -- Match Length Statistics Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)

---

From: Tatsuo Ishii <[email-masked]>
Date: Fri, 10 Apr 2026 10:18:47 +0900 (JST)

Hi Henson,

>         to also preserve columns referenced by DEFINE

I took a look at this and have a question.

+		/*

Before this, there's a check in the function:

		if (tle->ressortgroupref || tle->resjunk)
			continue;

Below is the query in the regression test that is suppoed to trigger
the error.  In this case column "i" in the target list should have
resjunk flag to be set to true. So I thought the if statenment above
becomes true and the column i is not removed from subquery's target
list. Am I missing something?

The test case in the patch:

-- Subquery wrapping: RPR window inside outer aggregate.
-- Tests that WindowAgg is not removed by remove_unused_subquery_outputs()
-- when DEFINE clause contains PREV/NEXT.
--
-- PREV in DEFINE + outer aggregate

--EXPLAIN
SELECT count(*) FROM (
  SELECT count(*) OVER w FROM generate_series(1,10) i
  WINDOW w AS (
    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
    PATTERN (A+)
    DEFINE A AS i > PREV(i)
  )
) t;

Also I think removal of the WindowAgg node is fine here because it's
not necessary to calculate the result of the sub query at
all. Actually the query above runs fine on v46. I guess some of the
incremental patches caused this to stop working. Can you elaborate?

Regards,

---

From: Henson Choi <[email-masked]>
Date: Fri, 10 Apr 2026 17:51:54 +0900

Hi Tatsuo,

Thank you for the careful review.

> list. Am I missing something?

That is a very good observation. For the test query you cited, the
resjunk check does indeed protect column "i", as you noted.

The allpaths.c guard is needed for a different scenario: when a Var
already exists in the inner SELECT as a non-resjunk entry, but the
outer query does not reference it. For example:

  SELECT count(*) FROM (
    SELECT i, count(*) OVER w FROM generate_series(1,10) i
    --     ^ in SELECT -> resjunk=false
    WINDOW w AS (
      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
      PATTERN (A+)
      DEFINE A AS i > PREV(i)
    )
  ) t;
  -- outer uses only count(*) -> i is not in attrs_used

In this case, the Var addition logic in parse_rpr.c (pull_var_clause
+ dup check) finds that "i" already exists in the targetlist, so it
does not add a new resjunk entry. Since "i" is resjunk=false and the
outer query does not reference it, remove_unused_subquery_outputs()
would replace it with NULL, breaking DEFINE evaluation.

Changing the existing entry to resjunk=true is not an option either,
since it would alter the subquery's output schema -- the outer query
may reference that column, and at parse time we do not yet know
whether it will. The existing protection mechanisms (resjunk,
ressortgroupref, attrs_used, volatile check) cannot express
"referenced by DEFINE", since DEFINE is a new column reference path
that did not exist in PostgreSQL before. The allpaths.c guard
follows the same pattern as the existing SRF and volatile guards in
that function.

> incremental patches caused this to stop working. Can you elaborate?

I agree -- for this specific query, removing the WindowAgg does not
change the count(*) result, since window functions do not change the
number of rows.

Regarding v46: the two bugs were actually latent from the initial
implementation, not caused by incremental patches. The old code
added the entire DEFINE expression (e.g., "i > PREV(i)") to the
query targetlist via findTargetlistEntrySQL99(). In most cases,
DEFINE conditions involve comparison operators, so the resulting
OpExpr did not match existing Var entries in the targetlist, and a
new resjunk entry was created. This accidentally prevented removal.
(In fact, a bare boolean DEFINE such as "DEFINE A AS flag" would
have exhibited the same removal bug even in v46, since
findTargetlistEntrySQL99 would reuse the existing entry instead
of creating a new resjunk one -- confirming that the issue was
latent in the original implementation, not introduced by recent
patches.)

However, that same approach caused the SIGSEGV (Bug 1): when RPR
and non-RPR windows coexist, the non-RPR WindowAgg inherits
targetlist entries containing RPRNavExpr nodes that it cannot
evaluate.

The two bugs are two sides of the same coin -- the old approach
prevented removal but caused SIGSEGV. Switching to Var-only
addition fixed the SIGSEGV but required the allpaths.c guard to
prevent removal.

As for more precise optimization (allowing removal when all
WindowFuncs for an RPR clause are unused), it would require
structural changes to the loop in remove_unused_subquery_outputs(),
which currently evaluates entries individually. Since this function
runs for all subqueries, not just RPR, such a change would need
broader testing.

The patch addresses two distinct issues: the parse_rpr.c change
(Var-only addition) prevents RPRNavExpr from propagating to
non-RPR WindowAgg nodes, while the allpaths.c guard prevents
incorrect column removal. Since both are needed, the guard
cannot simply be reverted. Would it make sense to keep it as-is
for now, and explore the more precise optimization as a separate
follow-up patch?

Best regards,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Fri, 10 Apr 2026 18:35:15 +0900 (JST)

Hi Henson,

> cannot simply be reverted.

Thank you for the detailed explanation. That makes sense.

> Would it make sense to keep it as-is

I agree keep it as is. I think priority of "more precise optimization"
is low for now. We could explore when we have time. What do you think?

Regards,

---

From: Henson Choi <[email-masked]>
Date: Sun, 12 Apr 2026 16:27:26 +0900

Hi Tatsuo,

When I started this work a little over three months ago, I had
no idea it would grow into what it is today. There is still
much to refine, but I would not have come this far without
your guidance and support along the way.

This series completes RPR navigation support, including
PREV/NEXT, FIRST/LAST, and compound navigation such as
PREV(FIRST(col)).

Attached are 31 incremental patches on top of v46 (replacing
the previous 15-patch set). Here is a summary of the changes
from the previous version:

  0001-0007: Unchanged from the previous set.

  0008: Replace reduced frame map with single match result
        (revised: int -> int64 type widening for
        row_is_in_reduced_frame() and related variables,
        cascading from 0025)

  0009: Add fixed-length group absorption for RPR
        (unchanged, design note [1])

        names (unchanged)

  0011: Fix quote_identifier() for RPR pattern variable
        name deparse (new)

        Pattern variable names that match SQL keywords were
        not being quoted in pg_get_viewdef() output, causing
        the deparsed view to fail on re-parse.

  0012: Fix execRPR.o ordering in executor Makefile to
        match meson.build (new)

        Ensures consistent build file ordering between Make
        and Meson build systems.

  0013: Remove unused force_colno parameter from RPR
        deparse functions (new)

        Dead parameter cleanup in get_rpr_pattern() and
        get_rpr_define() in ruleutils.c.

  0014: Add CHECK_FOR_INTERRUPTS to RPR context cleanup
        and finalize loops (new)

        Extends interrupt checking to ExecRPRCleanupContexts()
        and ExecRPRFinalizeMatch() loops, complementing the
        earlier additions in 0002 and 0003.

  0015: Narrow variable scope in ExecInitWindowAgg DEFINE
        clause loop (new)

        Moves local variable declarations into their
        respective loop bodies for clarity.

  0016: Normalize RPR element flag macros to return bool
        (new)

        RPR_ELEM_IS_* macros now return bool instead of the
        raw bitmask value, consistent with PostgreSQL style.

  0017: Implement 1-slot PREV/NEXT navigation for RPR
        (was 0011, context changes from 0008 int64)

  0018: Add JIT compilation support for RPR PREV/NEXT
        (was 0012, unchanged)

  0019: Add tuplestore trim optimization for RPR PREV
        navigation (was 0013, context changes from 0008
        int64)

  0020: Update RPR code comments to reflect 1-slot
        navigation model (was 0014, context only)

  0021: Enable JIT compilation for PREV/NEXT navigation
        tests in RPR (new)

        Adds test coverage for JIT-compiled PREV/NEXT
        expressions under jit_above_cost=0, verifying
        that the slot reload path produces correct results.

  0022: Add 2-arg PREV/NEXT test for row pattern
        navigation with host variable (new)

        Tests the PREV(value, offset) form where offset
        is a host variable (prepared statement parameter),
        exercising the RPR_NAV_OFFSET_NEEDS_EVAL path.

  0023: Add Nav Mark Lookback to EXPLAIN and fix
        compute_nav_max_offset() (new)

        EXPLAIN now displays "Nav Mark Lookback: N" for
        RPR windows with PREV navigation, showing how
        many rows the tuplestore retains. Also fixes
        compute_nav_max_offset() to correctly handle
        non-constant offset expressions.

  0024: Implement FIRST/LAST and compound navigation
        for RPR (was 0015, completed)

        The previous WIP patch is now complete with:

        - Compound navigation: PREV(FIRST(col)),
          NEXT(LAST(col, N)), etc. per SQL standard 5.6.4.
          Flattened into a single RPRNavExpr node with
          two offset values (inner position + outer offset).

        - RPRNavOffsetKind enum replaces sentinel macros
          for offset classification (CONSTANT, NEEDS_EVAL,
          FIRST_BASED).

        - Per-context DEFINE re-evaluation for match_start-
          dependent expressions (FIRST, LAST with offset),
          tracked via defineMatchStartDependent bitmapset
          computed by the planner.

        - Tuplestore mark handling extended for FIRST-based
          navigation: mark position accounts for both
          backward reach (PREV) and forward-from-match-start
          reach (FIRST).

        - Documentation added to func-window.sgml.

        See the design note [2] for the absorption safety
        analysis and evaluation sharing rules.

  0025: Guard against int64 overflow in RPR bounded frame
        end computation (new)

        Widens frame position variables from int to int64
        in row_is_in_reduced_frame() and related functions.
        Prevents overflow when frame end position exceeds
        INT_MAX in large partitions.

  0026: Fix RPR error message style: hint format,
        terminology, capitalization (new)

        Aligns RPR error messages with PostgreSQL conventions:
        lowercase first word, consistent use of "row pattern"
        terminology, HINT format corrections.

  0027: Fix comment typos, grammar, and inaccuracies in
        RPR code (new)

        Comment-only changes across execRPR.c, parse_rpr.c,
        nodeWindowAgg.c, and ruleutils.c. No functional
        changes.

  0028: Fix RPR documentation: synopsis, grammar, and
        terminology (new)

        Updates select.sgml, advanced.sgml, and
        func-window.sgml for consistent terminology
        and corrected synopsis examples.

  0029: Fix nav_slot pass-by-ref dangling pointer in RPR
        navigation (new)

        When a DEFINE expression contains multiple navigation
        calls targeting different positions (e.g.,
        PREV(x,1) > PREV(x,2)), the second call re-fetches
        nav_slot, freeing the previous tuple via pfree. Any
        pass-by-ref datum extracted from the first navigation
        becomes a dangling pointer. Fix by copying pass-by-ref
        results into per-tuple memory in the RESTORE step.

  0030: Add inline comments for complex RPR algorithms and
        design notes (new)

        Adds explanatory comments to key algorithms in
        execRPR.c and rpr.c (planner). Covers NFA absorption
        logic, match evaluation flow, navigation mark
        computation, and DEFINE re-evaluation decisions.
        Comment-only changes, no functional changes.

  0031: Remove unused include and fix header ordering in RPR
        files (new)

        Removes an unnecessary include from execExprInterp.c,
        drops a redundant nodeWindowAgg.h include from
        nodeWindowAgg.c, and fixes header ordering in
        parse_rpr.c to match PostgreSQL convention.

Changes from the previous version:

  - 0015 (FIRST/LAST) is now complete as 0024, with compound
    navigation, RPRNavOffsetKind, defineMatchStartDependent,
    and full test coverage.

  - 16 new patches (0011-0016, 0021-0023, 0025-0031) added
    since the previous set. These are all quality improvements:
    bug fixes, safety guards, style normalization, comments,
    and test coverage.

  - New features: compound navigation in 0024 (was "not
    yet done" in previous set), Nav Mark Lookback in
    EXPLAIN output (0023). All other new patches are
    quality improvements.

I am planning to submit PREFIX pattern absorption [3]
as a separate patch after this set is committed. The
changes are confined to the Absorb phase with no
external dependencies, so it can be submitted
independently. The design itself has some complexity
(shadow path tracking), and I would like to refine it
further until I am fully confident in the approach.

With this set I have covered the features I could
foresee. Whether anything else belongs in scope is
your call. From here, I think the emphasis should
shift from adding features to raising overall
quality.
If there are directions you think should be explored,
I am happy to work on them. Otherwise, I plan to focus
on code review, test coverage, and stabilization, and
will continue to submit bug fixes and test additions
as I find them.

I also very much welcome refactoring or restructuring
suggestions from a committer's maintainability
perspective. If anything in the current code will be
painful to maintain long-term, I would much rather
address it now than leave it for after commit.

For stabilization, I am thinking of the same approach
as in early January: gcov analysis, Valgrind runs, and
expanding the test suite.

**WITH THE PLANNER**, the biggest risk is what we do
not know we are missing -- there are too many possible
feature interactions to enumerate up front. To surface
these, I plan to run RPR-only tests under gcov and
review the uncovered planner paths: any line that RPR
could plausibly reach but does not becomes a candidate
risk, and a candidate for a new test. That turns the
unknown into a concrete review list rather than a guess
about which interactions to try. Does that seem like a
good direction?

If anything comes to mind that should be added or
fixed -- at any time -- please let me know.

[3] PREFIX pattern absorption design note

Best regards,
Henson

---

---

---

---

 	/*

 
 out_of_frame:

 
+		/*

 		/*
 
-		/*

-		/* -------------------
 
-		/*

 
 # ----------

From 59e99c6b9322f402df560bc693863491487e12db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 14:09:12 +0900
Subject: [PATCH] Replace reduced frame map with single match result

Widen row_is_in_reduced_frame() return type from int to int64,
since it returns rpr_match_length which is int64.
---
 src/backend/executor/execRPR.c            |  56 +++---
 src/backend/executor/nodeWindowAgg.c      | 233 +++++++++-------------
 src/include/nodes/execnodes.h             |  21 +-
 src/test/regress/expected/rpr_explain.out |   8 +-
 4 files changed, 132 insertions(+), 186 deletions(-)

index aed7cbef99a..dca2de570e8 100644
@@ -247,13 +247,10 @@ static void attno_map(Node *node);
 
 static bool rpr_is_defined(WindowAggState *winstate);
-static int	row_is_in_reduced_frame(WindowObject winobj, int64 pos);
+static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);

@@ -1035,13 +1032,7 @@ eval_windowaggregates(WindowAggState *winstate)
 	for (;;)
-		int			ret;
+		int64		ret;

 
 			/*
 
 			/*

 
 
 	CHECK_FOR_INTERRUPTS();
 
-#ifdef RPR_DEBUG

 			/*
 
-#ifdef RPR_DEBUG
-			printf("defineVariable name: %s\n", name);
-#endif
 			winstate->defineVariableList =
 				lappend(winstate->defineVariableList,
 						makeString(pstrdup(name)));
@@ -3668,7 +3637,7 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
 	int			notnull_offset;
 	int			notnull_relpos;
 	int			forward;
-	int			num_reduced_frame;
+	int64		num_reduced_frame;
 
 	Assert(WindowObjectIsValid(winobj));
 	winstate = winobj->winstate;
@@ -3968,14 +3937,12 @@ rpr_is_defined(WindowAggState *winstate)
  * AFTER MATCH SKIP PAST LAST ROW
  * -----------------
  */
-static int
+static int64
 row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 {
 	WindowAggState *winstate = winobj->winstate;
 	int			state;
-	int			rtn;
-	int64		i;
-	int			num_reduced_rows;
+	int64		rtn;

 

 	switch (state)
 	{
 		case RF_FRAME_HEAD:
-			num_reduced_rows = 1;
-			for (i = pos + 1;
 
 		case RF_SKIPPED:
 
 		case RF_UNMATCHED:
+		case RF_EMPTY_MATCH:
 			rtn = -1;
 			break;

-#ifdef RPR_DEBUG

 /*
 
-	/*

-/*

 
 
 /*

 	/*

 
 
 	/* Remove the matched context */
@@ -4747,7 +4692,7 @@ WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
 	WindowAggState *winstate;
 	int64		abs_pos;
 	int64		mark_pos;
-	int			num_reduced_frame;
+	int64		num_reduced_frame;

 
 /*
 
-	/*
 
 /* ----------------

From 7d9c68fffcf6eeebea395033da550c966456def2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 21:47:01 +0900
Subject: [PATCH] Add fixed-length group absorption for RPR

 
 
+		/*
 
 	/*
 
 				/*
 
-					Assert(count == 1);
-

 
+			/*
 
+		/*

+	/*

+/*

 	/*

 

 

 

 

From 3e4d65b9f75444ff58cb1b7858558bbe6c77616a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 22:00:50 +0900
Subject: [PATCH] Rename rpr_explain test views from sequential numbers to
 descriptive names

 

 

 

 

 

 

 
 
 -- Very long matches
-CREATE VIEW rpr_ev23 AS

 

 
 
 -- Single row
-CREATE VIEW rpr_ev35 AS

 
 -- Multiple alternations
-CREATE VIEW rpr_ev39 AS
 
 -- Optional elements
-CREATE VIEW rpr_ev40 AS
 
 -- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
 
 -- Star quantifier
-CREATE VIEW rpr_ev42 AS

 

 

 -- Exact count {n}
-CREATE VIEW rpr_ev54 AS
 
 -- Range {n,m}
-CREATE VIEW rpr_ev55 AS
 
 -- At least {n,}
-CREATE VIEW rpr_ev56 AS

 

 
 -- Alternation at start
-CREATE VIEW rpr_ev64 AS

 
 -- Alternation at end
-CREATE VIEW rpr_ev67 AS

 
 
 -- Nested groups
-CREATE VIEW rpr_ev72 AS

 

 

 -- Using PREV function
-CREATE VIEW rpr_ev82 AS

 

 

 

 

 

 

 

 

 
 
 -- Very long matches
-CREATE VIEW rpr_ev23 AS

 

 

 -- XML format output
-CREATE VIEW rpr_ev31 AS

 
 -- Empty result set
-CREATE VIEW rpr_ev34 AS
 
 -- Single row
-CREATE VIEW rpr_ev35 AS

 
 -- Nested groups
-CREATE VIEW rpr_ev38 AS
 
 -- Multiple alternations
-CREATE VIEW rpr_ev39 AS
 
 -- Optional elements
-CREATE VIEW rpr_ev40 AS
 
 -- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
 
 -- Star quantifier
-CREATE VIEW rpr_ev42 AS

 

 
 
 -- With INITIAL keyword
-CREATE VIEW rpr_ev49 AS

 -- Plus quantifier
-CREATE VIEW rpr_ev51 AS

 
 -- Exact count {n}
-CREATE VIEW rpr_ev54 AS
 
 -- Range {n,m}
-CREATE VIEW rpr_ev55 AS
 
 -- At least {n,}
-CREATE VIEW rpr_ev56 AS

 
 
 -- Simple alternation
-CREATE VIEW rpr_ev60 AS

 
 
 -- Alternation at start
-CREATE VIEW rpr_ev64 AS

 
 -- Alternation at end
-CREATE VIEW rpr_ev67 AS

 
 -- Simple group
-CREATE VIEW rpr_ev70 AS

 -- Nested groups
-CREATE VIEW rpr_ev72 AS

 

 -- count(*) with pattern
-CREATE VIEW rpr_ev77 AS

 

 -- Using PREV function
-CREATE VIEW rpr_ev82 AS

 

 

 

From 115425a057767ebf2b6e4c877641ed156b0ed07e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 08:53:46 +0900
Subject: [PATCH] Fix quote_identifier() for RPR pattern variable name deparse

Add quote_identifier() to PATTERN and DEFINE variable name output
in ruleutils.c and explain.c.  Without quoting, mixed-case or
reserved-word variable names (e.g., "Start", "Up") lose their
case or conflict with keywords in pg_get_viewdef() output,
breaking pg_dump/pg_restore round-trips.

Add regression test with quoted identifiers ("Start", "Up") to
verify correct deparse in both pg_get_viewdef and EXPLAIN output.
---
 src/backend/commands/explain.c            |  2 +-
 src/backend/utils/adt/ruleutils.c         |  4 ++--
 src/test/regress/expected/rpr_base.out    | 24 +++++++++++++++++++++++
 src/test/regress/expected/rpr_explain.out | 19 ++++++++++++++++++
 src/test/regress/sql/rpr_base.sql         | 10 ++++++++++
 src/test/regress/sql/rpr_explain.sql      | 12 ++++++++++++
 6 files changed, 68 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7f0367ce546..933eadab71e 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3176,7 +3176,7 @@ deparse_rpr_var(RPRPattern *pattern, int *idx, StringInfoData *buf,
 		appendStringInfoChar(buf, ' ');
 
 	Assert(elem->varId < pattern->numVars);
-	appendStringInfoString(buf, pattern->varNames[elem->varId]);
+	appendStringInfoString(buf, quote_identifier(pattern->varNames[elem->varId]));
 	append_rpr_quantifier(buf, elem);
 	*needSpace = true;
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index cfe24de43cf..c755a42efd6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7160,7 +7160,7 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 	switch (node->nodeType)
 	{
 		case RPR_PATTERN_VAR:
-			appendStringInfoString(buf, node->varName);
+			appendStringInfoString(buf, quote_identifier(node->varName));
 			append_pattern_quantifier(buf, node);
 			break;
 
@@ -7229,7 +7229,7 @@ get_rule_define(List *defineClause, bool force_colno, deparse_context *context)
 	{
 		TargetEntry *te = (TargetEntry *) lfirst(lc_def);
 
-		appendStringInfo(buf, "%s%s AS ", sep, te->resname);
+		appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
 		get_rule_expr((Node *) te->expr, context, false);
 		sep = ",\n  ";
 	}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 7452cf1b3ab..6526365dd6a 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2252,6 +2252,30 @@ SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
    b AS (val > 0) );
 (1 row)
 
+-- Quoted identifier round-trip: mixed case and reserved words need quoting
+CREATE VIEW rpr_serial_quoted AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ("Start" "Up"+)
+             DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
+SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (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)
+
 -- Materialized view (if supported)
 CREATE TABLE rpr_mview (id INT, val INT);
 INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index f66caf8908e..a68ec61e10f 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -301,6 +301,25 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (8 rows)
 
+-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
+-- Mixed case names must be quoted to preserve round-trip safety
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ("Start" "Up"+)
+    DEFINE "Start" AS TRUE, "Up" AS v > PREV(v)
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: "Start" "Up"+
+   ->  Function Scan on generate_series s
+(4 rows)
+
 -- ============================================================
 -- State Statistics Tests (peak, total, merged)
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 8c23c7598a3..3accecb73ba 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1559,6 +1559,16 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS val > 0, B AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
 
+-- Quoted identifier round-trip: mixed case and reserved words need quoting
+CREATE VIEW rpr_serial_quoted AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ("Start" "Up"+)
+             DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
+SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
+
 -- Materialized view (if supported)
 
 CREATE TABLE rpr_mview (id INT, val INT);
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 65a775fdad9..703ecd3b23b 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -226,6 +226,18 @@ WINDOW w AS (
     DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
 );');
 
+-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
+-- Mixed case names must be quoted to preserve round-trip safety
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ("Start" "Up"+)
+    DEFINE "Start" AS TRUE, "Up" AS v > PREV(v)
+);');
+
 -- ============================================================
 -- State Statistics Tests (peak, total, merged)
 -- ============================================================
-- 
2.50.1 (Apple Git-155)

From b46e0efc2dac822d7b68734345cd9e051e8aa235 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:14:59 +0900
Subject: [PATCH] Fix execRPR.o ordering in executor Makefile to match
 meson.build

---
 src/backend/executor/Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index eeed9a904e5..2b257427795 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -25,8 +25,8 @@ OBJS = \
 	execParallel.o \
 	execPartition.o \
 	execProcnode.o \
-	execReplication.o \
 	execRPR.o \
+	execReplication.o \
 	execSRF.o \
 	execScan.o \
 	execTuples.o \
-- 
2.50.1 (Apple Git-155)

From 4d64cb3907e95d3ffe61d206040b65207b31950d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:40:53 +0900
Subject: [PATCH] Remove unused force_colno parameter from RPR deparse
 functions

---
 src/backend/utils/adt/ruleutils.c | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c755a42efd6..e93c03a351c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -449,10 +449,8 @@ static void get_rule_orderby(List *orderList, List *targetList,
 							 bool force_colno, deparse_context *context);
 static void append_pattern_quantifier(StringInfo buf, RPRPatternNode *node);
 static void get_rule_pattern_node(RPRPatternNode *node, deparse_context *context);
-static void get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
-							 deparse_context *context);
-static void get_rule_define(List *defineClause, bool force_colno,
-							deparse_context *context);
+static void get_rule_pattern(RPRPatternNode *rpPattern, deparse_context *context);
+static void get_rule_define(List *defineClause, deparse_context *context);
 static void get_rule_windowclause(Query *query, deparse_context *context);
 static void get_rule_windowspec(WindowClause *wc, List *targetList,
 								deparse_context *context);
@@ -7203,8 +7201,7 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
  * Display a PATTERN clause.
  */
 static void
-get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
-				 deparse_context *context)
+get_rule_pattern(RPRPatternNode *rpPattern, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
 
@@ -7217,7 +7214,7 @@ get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
  * Display a DEFINE clause.
  */
 static void
-get_rule_define(List *defineClause, bool force_colno, deparse_context *context)
+get_rule_define(List *defineClause, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
 	const char *sep;
@@ -7356,7 +7353,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
 		if (needspace)
 			appendStringInfoChar(buf, ' ');
 		appendStringInfoString(buf, "\n  PATTERN ");
-		get_rule_pattern(wc->rpPattern, false, context);
+		get_rule_pattern(wc->rpPattern, context);
 		needspace = true;
 	}
 
@@ -7365,7 +7362,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
 		if (needspace)
 			appendStringInfoChar(buf, ' ');
 		appendStringInfoString(buf, "\n  DEFINE\n");
-		get_rule_define(wc->defineClause, false, context);
+		get_rule_define(wc->defineClause, context);
 		appendStringInfoChar(buf, ' ');
 	}
 
-- 
2.50.1 (Apple Git-155)

From 4f779ed3658d7b4d97819b5cca39d6a1d555d67b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:43:07 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS to RPR context cleanup and finalize
 loops

---
 src/backend/executor/execRPR.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 8f0457e2b3c..97efa9a4924 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -3068,6 +3068,8 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 
 	for (ctx = winstate->nfaContext; ctx != NULL; ctx = next)
 	{
+		CHECK_FOR_INTERRUPTS();
+
 		next = ctx->next;
 
 		/* Skip the target context and contexts still processing */
@@ -3108,6 +3110,8 @@ ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos)
 
 	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
 	{
+		CHECK_FOR_INTERRUPTS();
+
 		if (ctx->states != NULL)
 		{
 			nfa_match(winstate, ctx, NULL);
-- 
2.50.1 (Apple Git-155)

From 366068f89fd0385c3231b2f1bd7d7e75f9641718 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:48:55 +0900
Subject: [PATCH] Narrow variable scope in ExecInitWindowAgg DEFINE clause loop

---
 src/backend/executor/nodeWindowAgg.c | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index dca2de570e8..0202c508323 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2628,9 +2628,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	TupleDesc	scanDesc;
 	ListCell   *l;
 
-	TargetEntry *te;
-	Expr	   *expr;
-
 	/* check for unsupported flags */
 	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
 
@@ -2951,13 +2948,11 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		 */
 		foreach(l, node->defineClause)
 		{
-			char	   *name;
+			TargetEntry *te = lfirst(l);
+			char	   *name = te->resname;
+			Expr	   *expr = te->expr;
 			ExprState  *exps;
 
-			te = lfirst(l);
-			name = te->resname;
-			expr = te->expr;
-

From d4454b9a6b576a79adca7f2ff96abd0b28db3a86 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 11:32:56 +0900
Subject: [PATCH] Normalize RPR element flag macros to return bool

---
 src/include/optimizer/rpr.h | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index e78092678bb..360e1bb777f 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -44,10 +44,10 @@
 #define RPR_ELEM_ABSORBABLE			0x08	/* absorption judgment point */
 
 /* Accessor macros for RPRPatternElement */
-#define RPRElemIsReluctant(e)			((e)->flags & RPR_ELEM_RELUCTANT)
-#define RPRElemCanEmptyLoop(e)			((e)->flags & RPR_ELEM_EMPTY_LOOP)
-#define RPRElemIsAbsorbableBranch(e)	((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH)
-#define RPRElemIsAbsorbable(e)			((e)->flags & RPR_ELEM_ABSORBABLE)
+#define RPRElemIsReluctant(e)			(((e)->flags & RPR_ELEM_RELUCTANT) != 0)
+#define RPRElemCanEmptyLoop(e)			(((e)->flags & RPR_ELEM_EMPTY_LOOP) != 0)
+#define RPRElemIsAbsorbableBranch(e)	(((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH) != 0)
+#define RPRElemIsAbsorbable(e)			(((e)->flags & RPR_ELEM_ABSORBABLE) != 0)
 #define RPRElemIsVar(e)			((e)->varId <= RPR_VARID_MAX)
 #define RPRElemIsBegin(e)		((e)->varId == RPR_VARID_BEGIN)
 #define RPRElemIsEnd(e)			((e)->varId == RPR_VARID_END)
-- 
2.50.1 (Apple Git-155)

From 87d9b0b288b9f07663dcb5191db0c34a3edb5473 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 23:52:42 +0900
Subject: [PATCH] Implement 1-slot PREV/NEXT navigation for RPR

 src/test/regress/expected/rpr_base.out    |   2 +-
 22 files changed, 1607 insertions(+), 165 deletions(-)

@@ -318,12 +323,17 @@
         <indexterm>
          <primary>next</primary>
         </indexterm>

+		case T_RPRNavExpr:
+			{
+				/*
 
+#include "common/int.h"

index 0202c508323..4e643df94cf 100644
 
-/*
 
 static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);

 

@@ -2726,15 +2737,18 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)

 
 
 	/*
 	 * create frame head and tail slots only if needed (must create slots in
@@ -2904,6 +2918,23 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->agg_winobj = agg_winobj;
 	}
 
+	/*
 
@@ -2944,7 +2975,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
@@ -2956,7 +2989,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
@@ -2991,107 +3023,38 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 }
 
 /*
 
-static bool
 
-	if (node == NULL)
-		return false;
+	/*

-/*

@@ -3152,8 +3115,8 @@ ExecReScanWindowAgg(WindowAggState *node)
@@ -4218,6 +4181,10 @@ register_result:
@@ -4228,37 +4195,25 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	ListCell   *lc;
 	int			varIdx = 0;
 	TupleTableSlot *slot;
+	int64		saved_pos;
 
-	/*

 
 	foreach(lc, winstate->defineClauseList)
 	{
@@ -4276,6 +4231,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 			break;
 	}

 
+	/*
 
+			case EEOP_RPR_NAV_SET:

 
 /*
  * transformRPR

 
 	return defineClause;
 }
+
+/*
+ * check_rpr_nav_expr
index e93c03a351c..a4fe725646c 100644
@@ -10103,6 +10103,22 @@ get_rule_expr(Node *node, deparse_context *context,
 
+		case T_RPRNavExpr:
+			{
 
 /*
  * prev
 
 /*
  * next

 

 
 
+/*
+ * RPRNavExpr
+ *
 
+--
 
 -- ReScan test: LATERAL join forces WindowAgg rescan with RPR
--- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+-- Tests ExecReScanWindowAgg clearing nav_slot
 SELECT g.x, sub.*
 FROM generate_series(1, 2) g(x),
 LATERAL (
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 6526365dd6a..37aa81ebdea 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2273,7 +2273,7 @@ SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
    PATTERN ("Start" "Up"+)                                                   +
    DEFINE                                                                    +
    "Start" AS true,                                                          +
-   "Up" AS (val > prev(val)) );
+   "Up" AS (val > PREV(val)) );
 (1 row)
 
 -- Materialized view (if supported)
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index a68ec61e10f..dc3075e6bd3 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3847,6 +3847,86 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (8 rows)

+--
 
 -- ReScan test: LATERAL join forces WindowAgg rescan with RPR
--- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+-- Tests ExecReScanWindowAgg clearing nav_slot
 SELECT g.x, sub.*
 FROM generate_series(1, 2) g(x),
 LATERAL (
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 703ecd3b23b..e339edd7e91 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2214,6 +2214,62 @@ WINDOW w AS (
         D AS v < PREV(v)
 );');

From 49555669b00a11ec6b106026f075b03825f6bc3f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 10:54:30 +0900
Subject: [PATCH] Add JIT compilation support for RPR PREV/NEXT navigation

---

 
 			case EEOP_RPR_NAV_SET:

 

From 0b71c6b947e6b279bff33d34c2e263833f9f5408 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 11:49:32 +0900
Subject: [PATCH] Add tuplestore trim optimization for RPR PREV navigation

index 4e643df94cf..9787ef7756f 100644

+		/*
@@ -2957,6 +2988,10 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 
 	/* Calculate NFA state size and allocate cycle detection bitmap */
 	if (node->rpPattern != NULL)
@@ -3867,6 +3902,87 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
 	mbp[bpos] = mb;
 }
 
+/*
 
+/*

 
 	/* RPR navigation */
 
+	/*
index 360e1bb777f..00a28abe2b4 100644
 
+/*

From 08c98ae40028559e6047cdbd4f20f2388ccab86c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 16:06:06 +0900
Subject: [PATCH] Update RPR code comments to reflect 1-slot navigation model

---

index 97efa9a4924..995acdd7be5 100644

From 74883a5f7ec23d20f0352d59595839099b141f89 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 09:48:54 +0900
Subject: [PATCH] Enable JIT compilation for PREV/NEXT navigation tests in RPR

---
 src/test/regress/expected/rpr.out | 2 ++
 src/test/regress/sql/rpr.sql      | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index de6ce4fba8a..5a460e9bd52 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2156,6 +2156,7 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
 -- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
 -- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
 -- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit = on;
 SET jit_above_cost = 0;
 WITH data AS (
  SELECT i, abs(50000 - i) AS price
@@ -2184,6 +2185,7 @@ FROM result WHERE match_len > 0;
 (1 row)
 
 RESET jit_above_cost;
+RESET jit;
 --
 -- IGNORE NULLS
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b3bbc8254c4..e417789eb2b 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1087,6 +1087,7 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
 -- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
 -- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
 -- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit = on;
 SET jit_above_cost = 0;
 WITH data AS (
  SELECT i, abs(50000 - i) AS price
@@ -1110,6 +1111,7 @@ result AS (
 SELECT count(*) AS matched_rows, max(match_len) AS longest_match
 FROM result WHERE match_len > 0;
 RESET jit_above_cost;
+RESET jit;
 
 --
 -- IGNORE NULLS
-- 
2.50.1 (Apple Git-155)

From c29f3eb34d13f2c0211d3070abaf1062da0090ae Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 09:53:14 +0900
Subject: [PATCH] Add 2-arg PREV/NEXT test for row pattern navigation with host
 variable

---
 src/test/regress/expected/rpr.out | 63 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 16 ++++++++
 2 files changed, 79 insertions(+)

diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 5a460e9bd52..c02dbd4c08d 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1492,6 +1492,69 @@ EXECUTE test_prev_offset(-1);
 ERROR:  PREV/NEXT offset must not be negative
 EXECUTE test_prev_offset(NULL);
 ERROR:  PREV/NEXT offset must not be null
+DEALLOCATE test_prev_offset;
+-- 2-arg PREV/NEXT: host variable with positive value
+-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS price > PREV(price, $1)
+);
+EXECUTE test_prev_offset(1);
+ company  |   tdate    | price | first_value | count 
+----------+------------+-------+-------------+-------
+ company1 | 07-01-2023 |   100 |         100 |     2
+ company1 | 07-02-2023 |   200 |             |     0
+ company1 | 07-03-2023 |   150 |             |     0
+ company1 | 07-04-2023 |   140 |         140 |     2
+ company1 | 07-05-2023 |   150 |             |     0
+ company1 | 07-06-2023 |    90 |          90 |     3
+ company1 | 07-07-2023 |   110 |             |     0
+ company1 | 07-08-2023 |   130 |             |     0
+ company1 | 07-09-2023 |   120 |         120 |     2
+ company1 | 07-10-2023 |   130 |             |     0
+ company2 | 07-01-2023 |    50 |          50 |     2
+ company2 | 07-02-2023 |  2000 |             |     0
+ company2 | 07-03-2023 |  1500 |             |     0
+ company2 | 07-04-2023 |  1400 |        1400 |     2
+ company2 | 07-05-2023 |  1500 |             |     0
+ company2 | 07-06-2023 |    60 |          60 |     3
+ company2 | 07-07-2023 |  1100 |             |     0
+ company2 | 07-08-2023 |  1300 |             |     0
+ company2 | 07-09-2023 |  1200 |        1200 |     2
+ company2 | 07-10-2023 |  1300 |             |     0
+(20 rows)
+
+EXECUTE test_prev_offset(2);
+ company  |   tdate    | price | first_value | count 
+----------+------------+-------+-------------+-------
+ company1 | 07-01-2023 |   100 |             |     0
+ company1 | 07-02-2023 |   200 |         200 |     2
+ company1 | 07-03-2023 |   150 |             |     0
+ company1 | 07-04-2023 |   140 |             |     0
+ company1 | 07-05-2023 |   150 |             |     0
+ company1 | 07-06-2023 |    90 |             |     0
+ company1 | 07-07-2023 |   110 |         110 |     3
+ company1 | 07-08-2023 |   130 |             |     0
+ company1 | 07-09-2023 |   120 |             |     0
+ company1 | 07-10-2023 |   130 |             |     0
+ company2 | 07-01-2023 |    50 |             |     0
+ company2 | 07-02-2023 |  2000 |        2000 |     2
+ company2 | 07-03-2023 |  1500 |             |     0
+ company2 | 07-04-2023 |  1400 |             |     0
+ company2 | 07-05-2023 |  1500 |             |     0
+ company2 | 07-06-2023 |    60 |             |     0
+ company2 | 07-07-2023 |  1100 |        1100 |     3
+ company2 | 07-08-2023 |  1300 |             |     0
+ company2 | 07-09-2023 |  1200 |             |     0
+ company2 | 07-10-2023 |  1300 |             |     0
+(20 rows)
+
 DEALLOCATE test_prev_offset;
 -- 2-arg: two PREV with different offsets in same DEFINE clause
 -- B: price exceeds both 1-back and 2-back values
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e417789eb2b..47f33904690 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -732,6 +732,22 @@ EXECUTE test_prev_offset(-1);
 EXECUTE test_prev_offset(NULL);
 DEALLOCATE test_prev_offset;
 
+-- 2-arg PREV/NEXT: host variable with positive value
+-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS price > PREV(price, $1)
+);
+EXECUTE test_prev_offset(1);
+EXECUTE test_prev_offset(2);
+DEALLOCATE test_prev_offset;
+
 -- 2-arg: two PREV with different offsets in same DEFINE clause
 -- B: price exceeds both 1-back and 2-back values
 SELECT company, tdate, price,
-- 
2.50.1 (Apple Git-155)

From 8b1ef0227cb3e353a93b38a07602d51caa2a26d6 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 10:46:38 +0900
Subject: [PATCH] Add Nav Mark Lookback to EXPLAIN and fix
 compute_nav_max_offset()

Show the planner-computed navMaxOffset in EXPLAIN output as
"Nav Mark Lookback" for RPR windows, so that tuplestore trim
behavior is visible in query plans.  Displayed as an integer for
constant offsets, "runtime" for non-constant (Param) offsets, and
"retain all" when the entire partition must be preserved.

Fix compute_nav_max_offset() to return the walker-set value instead
of unconditionally returning RPR_NAV_OFFSET_NEEDS_EVAL when the
walker stops early.

Add regression tests covering constant offsets, NEXT-only, multiple
PREV offsets, and host variable offsets under both custom and generic
plans.
---
 src/backend/commands/explain.c                |   9 +
 src/backend/optimizer/plan/createplan.c       |   2 +-
 src/test/regress/expected/rpr_base.out        | 231 ++++++----
 src/test/regress/expected/rpr_explain.out     | 397 ++++++++++++++----
 src/test/regress/expected/rpr_integration.out |  25 +-
 src/test/regress/sql/rpr_explain.sql          |  71 ++++
 6 files changed, 560 insertions(+), 175 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 933eadab71e..1848de9de7a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3254,6 +3254,15 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 			ExplainPropertyText("Pattern", patternStr, es);
 			pfree(patternStr);
 		}
+
+		/* Show navigation offsets for tuplestore trim */
+		if (wagg->navMaxOffset == RPR_NAV_OFFSET_RETAIN_ALL)
+			ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+		else if (wagg->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
+			ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+		else
+			ExplainPropertyInteger("Nav Mark Lookback", NULL,
+								   wagg->navMaxOffset, es);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ee2d53b5924..8ee3ccf6d0d 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2553,7 +2553,7 @@ compute_nav_max_offset(List *defineClause)
 		TargetEntry *te = (TargetEntry *) lfirst(lc);
 
 		if (nav_max_offset_walker((Node *) te->expr, &maxOffset))
-			return RPR_NAV_OFFSET_NEEDS_EVAL;
+			return maxOffset;	/* NEEDS_EVAL or RETAIN_ALL */
 	}
 
 	return maxOffset;
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 37aa81ebdea..1e450a07ced 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3065,10 +3065,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{3}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive VAR merge: A{2} A{3} -> a{5}
 EXPLAIN (COSTS OFF)
@@ -3080,10 +3081,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{5}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive VAR merge: A+ A* -> a+
 EXPLAIN (COSTS OFF)
@@ -3095,10 +3097,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive VAR merge: A A+ -> a{2,}
 -- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
@@ -3112,10 +3115,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{2,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
 EXPLAIN (COSTS OFF)
@@ -3127,10 +3131,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b){15}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
@@ -3142,10 +3147,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b'){2,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
 -- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
@@ -3159,10 +3165,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b'){3,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- PREFIX merge: A B (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
@@ -3174,10 +3181,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b'){2,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){3,}
 EXPLAIN (COSTS OFF)
@@ -3189,10 +3197,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b'){3,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Flatten nested: A ((B) (C)) -> a b c
 EXPLAIN (COSTS OFF)
@@ -3204,10 +3213,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Data execution: SEQ flatten produces correct results
 SELECT id, val, count(*) OVER w AS cnt
@@ -3239,10 +3249,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b | c)+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- ALT deduplicate: (A | B | A) -> (a | b)
 EXPLAIN (COSTS OFF)
@@ -3254,10 +3265,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Data execution: ALT dedup produces correct results
 SELECT id, val, count(*) OVER w AS cnt
@@ -3289,10 +3301,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{6}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
 -- outer exact, child range - optimization applies
@@ -3305,10 +3318,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{6,9}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Quantifier NO multiply: (A{2}){2,3} stays as (a{2}){2,3}
 -- outer range - gaps would occur (4,6 not 4,5,6), no optimization
@@ -3321,10 +3335,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2}){2,3}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Quantifier NO multiply: (A{2}){2,} stays as (a{2}){2,}
 -- outer unbounded - gaps would occur (4,6,8,... not 4,5,6,...), no optimization
@@ -3337,10 +3352,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2}'){2,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Quantifier multiply: (A){2,} -> a{2,}
 -- child exact 1 - no gaps, optimization applies
@@ -3353,10 +3369,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{2,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Quantifier multiply: (A)+ -> a+
 -- child exact 1 - no gaps, optimization applies
@@ -3369,10 +3386,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Quantifier NO multiply: (A{2}){3,5} stays as (a{2}){3,5}
 -- outer range, child exact > 1 - gaps would occur (6,8,10 not 6,7,8,9,10)
@@ -3385,10 +3403,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2}){3,5}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
 -- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
@@ -3401,10 +3420,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2,3}){2,3}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Nested unbounded: (A*)* -> a*
 EXPLAIN (COSTS OFF)
@@ -3416,10 +3436,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a*"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Nested unbounded: (A+)* -> a*
 EXPLAIN (COSTS OFF)
@@ -3431,10 +3452,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a*"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Nested unbounded: (A+)+ -> a+
 EXPLAIN (COSTS OFF)
@@ -3446,10 +3468,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Unwrap GROUP{1,1}: (A) -> a
 EXPLAIN (COSTS OFF)
@@ -3461,10 +3484,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Unwrap GROUP{1,1}: (A B) -> a b
 EXPLAIN (COSTS OFF)
@@ -3476,10 +3500,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{3}
 EXPLAIN (COSTS OFF)
@@ -3492,10 +3517,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{2} (b{2}){2,} c{3}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,}
 -- Tests mergeConsecutiveGroups with child->max == INF
@@ -3508,10 +3534,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{2,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive GROUP merge finite: (A{10}){20} -> a{200}
 -- Tests mergeConsecutiveGroups with both finite
@@ -3524,10 +3551,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{200}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Different GROUP prevents merge: (A B){2} (C D){3}
 -- Tests mergeConsecutiveGroups flush previous
@@ -3542,10 +3570,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b){2} (c d){3}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Different children count prevents merge: (A B)+ (A B C)+
 -- Tests rprPatternChildrenEqual length check
@@ -3559,10 +3588,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+" (a b c)+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- PREFIX only merge: A B (A B)+ -> (a b){2,}
 -- Tests mergeGroupPrefixSuffix: absorb preceding elements into GROUP min
@@ -3575,10 +3605,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b'){2,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- SUFFIX only merge: (A B)+ A B -> (a b){2,}
 -- Tests mergeGroupPrefixSuffix: absorb following elements into GROUP min
@@ -3591,10 +3622,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b'){2,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C
 -- Tests mergeGroupPrefixSuffix: skip absorbed suffix elements
@@ -3608,10 +3640,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b'){3,}" c
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- PREFIX merge with remaining prefix: A B C D (C D)+
 -- Tests mergeGroupPrefixSuffix: trimmed list reconstruction
@@ -3626,10 +3659,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b (c d){2,}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- PREFIX merge with quantifiers: A B* (A B*)+ -> (a b*){2,}
 -- Tests mergeGroupPrefixSuffix: quantifier comparison in rprPatternEqual
@@ -3643,10 +3677,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b*){2,}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,}
 -- Tests mergeGroupPrefixSuffix: complex quantifier patterns
@@ -3660,10 +3695,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a+" b* c?){2,}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- SUFFIX merge with quantifiers: (A B*)+ A B* -> (a b*){2,}
 -- Tests mergeGroupPrefixSuffix: suffix with quantifiers
@@ -3677,10 +3713,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b*){2,}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Unwrap GROUP{1,1}: ((A | B | C)) -> (a | b | c)
 -- Tests tryUnwrapGroup removing redundant outer GROUP
@@ -3693,10 +3730,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b | c)
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Data execution: GROUP unwrap produces correct results
 SELECT id, val, count(*) OVER w AS cnt
@@ -3729,10 +3767,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+? a
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Reluctant optimization bypass: GROUP merge
 -- (A B)+? (A B) stays separate (greedy merges to (a b){2,})
@@ -3745,10 +3784,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b)+? a b
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Reluctant optimization bypass: quantifier multiply (outer reluctant)
 -- (A{2}){3}? stays as (a{2}){3}? (greedy merges to a{6})
@@ -3761,10 +3801,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2}){3}?
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Reluctant optimization bypass: quantifier multiply (inner reluctant)
 -- (A{2}?){3} stays as (a{2}?){3} (greedy merges to a{6})
@@ -3777,10 +3818,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2}?){3}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Reluctant optimization bypass: PREFIX merge
 -- A B (A B)+? stays separate (greedy merges to (a b){2,})
@@ -3793,10 +3835,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b (a b)+?
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Reluctant optimization bypass: SUFFIX merge
 -- (A B)+? A B stays separate (greedy merges to (a b){2,})
@@ -3809,10 +3852,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b)+? a b
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- GROUP unwrap with quantifier propagation: (A)?? B -> a?? b
 -- Single VAR child {1,1} receives GROUP's quantifier and reluctant
@@ -3825,10 +3869,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a?? b
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Reluctant preserved through ALT flatten
 -- (A | (B | C))+? flattens to (a | b | c)+? - inner ALT flattened, reluctant kept
@@ -3841,10 +3886,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b | c)+?
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Reluctant optimization bypass: absorption flags
 -- A+? with SKIP PAST LAST ROW - no absorption markers (greedy A+ gets a+")
@@ -3857,10 +3903,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+?
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Duplicate GROUP removal: ((A | B)+ | (A | B)+) -> (a | b)+
 EXPLAIN (COSTS OFF)
@@ -3872,10 +3919,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive VAR merge with zero-min: A* A+ -> a+
 EXPLAIN (COSTS OFF)
@@ -3887,10 +3935,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Consecutive VAR merge (4-element): A A{2} A+ A{3} -> a{7,}
 EXPLAIN (COSTS OFF)
@@ -3902,10 +3951,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{7,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- PREFIX+SUFFIX merge (5-way): A B A B (A B)+ A B A B -> (a b){5,}
 EXPLAIN (COSTS OFF)
@@ -3918,10 +3968,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b'){5,}"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Unwrap single-item ALT after dedup: (A | A)+ -> a+
 -- ALT dedup reduces to single-item, then GROUP unwrap
@@ -3934,10 +3985,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- GROUP{1,1} to SEQ with flatten: ((A B)(C D)) -> a b c d
 EXPLAIN (COSTS OFF)
@@ -3951,10 +4003,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c d
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Nested ALT pattern: ((A B) | C) D | A B C
 EXPLAIN (COSTS OFF)
@@ -3968,10 +4021,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a b | c) d | a b c)
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Nested ALT with unbounded: ((A+ B) | C) D | A B C
 EXPLAIN (COSTS OFF)
@@ -3985,10 +4039,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a+" b | c) d | a b c)
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- ============================================================
 -- Absorption Flag Display Tests
@@ -4006,10 +4061,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
 EXPLAIN (COSTS OFF)
@@ -4021,10 +4077,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- ALT both absorbable: A+ | B+ -> (a+" | b+")
 EXPLAIN (COSTS OFF)
@@ -4036,10 +4093,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a+" | b+")
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- ALT one absorbable: A+ | B -> (a+" | b)
 EXPLAIN (COSTS OFF)
@@ -4051,10 +4109,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a+" | b)
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Sequence with absorbable start: A+ B -> a+" b
 EXPLAIN (COSTS OFF)
@@ -4066,10 +4125,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT
 EXPLAIN (COSTS OFF)
@@ -4082,10 +4142,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a+" b | c) d | a b c)
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Nested unbounded: (A+ | B)+ -> (a+" | b)+ (first iteration absorbable)
 EXPLAIN (COSTS OFF)
@@ -4098,10 +4159,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a+" | b)+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+" b | a b)* (first iteration absorbable)
 EXPLAIN (COSTS OFF)
@@ -4114,10 +4176,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a+" b | a b)*
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}"
 -- All children have min == max, equivalent to unrolling to {1,1}
@@ -4131,10 +4194,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2}' b{3}')+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Nested fixed-length group: (A (B C){2} D)+ -> absorbable
 EXPLAIN (COSTS OFF)
@@ -4147,10 +4211,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' (b' c'){2}' d')+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable
 EXPLAIN (COSTS OFF)
@@ -4163,10 +4228,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a{2}' b{3}'){2}')+"
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max)
 EXPLAIN (COSTS OFF)
@@ -4179,10 +4245,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b{2,5})+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max)
 EXPLAIN (COSTS OFF)
@@ -4195,10 +4262,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b?)+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
 EXPLAIN (COSTS OFF)
@@ -4210,10 +4278,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers)
 EXPLAIN (COSTS OFF)
@@ -4225,10 +4294,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b){2,}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers)
 EXPLAIN (COSTS OFF)
@@ -4240,10 +4310,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Non-absorbable (limited frame): A+ -> a+ (no markers)
 EXPLAIN (COSTS OFF)
@@ -4255,10 +4326,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND '10'::bigint FOLLOWING)
    Pattern: a+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- Reluctant {1}? quantifier deparse
 -- A{1}? is a reluctant {1,1} quantifier.  The deparse code must
@@ -4277,10 +4349,11 @@ WINDOW w AS (
  WindowAgg
    Window: w AS (ORDER BY val ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{1}? b
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: val
          ->  Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
 
 -- ============================================================
 -- Absorption Analysis Tests
@@ -4775,10 +4848,11 @@ WINDOW w AS (
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2000000000}){2}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_fallback
-(6 rows)
+(7 rows)
 
 -- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
 -- Test: max-only quantifier overflow causes optimization fallback
@@ -4795,10 +4869,11 @@ WINDOW w AS (
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{1,2000000000}){2}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_fallback
-(6 rows)
+(7 rows)
 
 -- Expected: Fallback - min OK (2*1=2), but max overflow (2*2000000000 > INT32_MAX)
 -- Test: max quantifier exceeds valid range (2147483647 = INT_MAX, limit is 2147483646)
@@ -4828,10 +4903,11 @@ WINDOW w AS (
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a{2000000000,}"){2000000000,}
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_fallback
-(6 rows)
+(7 rows)
 
 -- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
 -- Test: prefix mismatch causes optimization fallback
@@ -4848,10 +4924,11 @@ WINDOW w AS (
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b (c d)+
+   Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_fallback
-(6 rows)
+(7 rows)
 
 -- Expected: Fallback - prefix elements don't match GROUP content
 DROP TABLE rpr_fallback;
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index dc3075e6bd3..77ab25a2289 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -36,6 +36,7 @@
 --   Window Function Combinations
 --   DEFINE Expression Variations
 --   Large Scale Statistics Verification
+--   Nav Mark Lookback (tuplestore trim)
 -- ============================================================
 -- Filter function to normalize platform-dependent memory values (not NFA statistics).
 -- NFA statistics should not change between platforms; if they do, it could
@@ -141,13 +142,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
    NFA Contexts: 2 peak, 101 total, 60 pruned
    NFA: 20 matched (len 2/2/2.0), 0 mismatched
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Pattern with no matches - 0 matched
 CREATE VIEW rpr_ev_basic_nomatch AS
@@ -180,12 +182,13 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: x y z
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 1 peak, 101 total, 0 merged
    NFA Contexts: 2 peak, 101 total, 100 pruned
    NFA: 0 matched, 0 mismatched
    ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Pattern matching every row - high match count
 CREATE VIEW rpr_ev_basic_allrows AS
@@ -218,12 +221,13 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: r
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
    NFA Contexts: 2 peak, 101 total, 0 pruned
    NFA: 100 matched (len 1/1/1.0), 0 mismatched
    ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Regression test: Space before parenthesis in pattern deparse
 -- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
@@ -255,13 +259,14 @@ WINDOW w AS (
  WindowAgg (actual rows=20.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a (b | c)
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 35 total, 0 merged
    NFA Contexts: 2 peak, 21 total, 6 pruned
    NFA: 7 matched (len 2/2/2.0), 0 mismatched
    NFA: 0 absorbed, 7 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Regression test: Sequential alternations at same depth
 -- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
@@ -294,12 +299,13 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a ((b | c) (d | e))*
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 61 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 24 pruned
    NFA: 6 matched (len 1/1/1.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Regression test: Quoted identifiers in EXPLAIN pattern deparse
 -- Mixed case names must be quoted to preserve round-trip safety
@@ -317,8 +323,9 @@ WINDOW w AS (
  WindowAgg
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: "Start" "Up"+
+   Nav Mark Lookback: 1
    ->  Function Scan on generate_series s
-(4 rows)
+(5 rows)
 
 -- ============================================================
 -- State Statistics Tests (peak, total, merged)
@@ -354,12 +361,13 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 76 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 25 pruned
    NFA: 25 matched (len 1/1/1.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Alternation pattern - multiple state branches
 CREATE VIEW rpr_ev_state_alt AS
@@ -396,13 +404,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b | c) (d | e)
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 6 peak, 524 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 20 pruned
    NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Complex pattern with high state count
 CREATE VIEW rpr_ev_state_complex AS
@@ -441,13 +450,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b* c+
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 235 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 34 pruned
    NFA: 33 matched (len 3/3/3.0), 0 mismatched
    NFA: 0 absorbed, 33 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Grouped pattern with quantifier - state count with grouping
 CREATE VIEW rpr_ev_state_group_quant AS
@@ -480,13 +490,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+"
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 91 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 0 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
    NFA: 29 absorbed (len 2/2/2.0), 30 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
@@ -520,13 +531,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b){8}
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 17 peak, 995 total, 0 merged
    NFA Contexts: 8 peak, 101 total, 1 pruned
    NFA: 12 matched (len 8/8/8.0), 3 mismatched (len 2/4/3.0)
    NFA: 0 absorbed, 84 skipped (len 1/7/4.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Consecutive ALT merge followed by different ALT
 -- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
@@ -560,13 +572,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b){2} (c | d)
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 181 total, 0 merged
    NFA Contexts: 3 peak, 41 total, 12 pruned
    NFA: 9 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 18 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Consecutive ALT merge followed by non-ALT element
 -- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
@@ -600,13 +613,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b){2} c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 6 peak, 177 total, 0 merged
    NFA Contexts: 3 peak, 41 total, 2 pruned
    NFA: 12 matched (len 3/3/3.0), 2 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 24 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
 CREATE VIEW rpr_ev_state_alt_absorb_group AS
@@ -639,13 +653,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b){3,}
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 243 total, 0 merged
    NFA Contexts: 3 peak, 41 total, 0 pruned
    NFA: 1 matched (len 40/40/40.0), 0 mismatched
    NFA: 0 absorbed, 39 skipped (len 1/2/1.0)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- High state count - alternation with plus quantifier
 CREATE VIEW rpr_ev_state_alt_plus AS
@@ -678,13 +693,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b | c)+ d
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 16 peak, 1004 total, 0 merged
    NFA Contexts: 4 peak, 101 total, 0 pruned
    NFA: 25 matched (len 4/4/4.0), 0 mismatched
    NFA: 0 absorbed, 75 skipped (len 1/3/2.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Early termination: first ALT branch (A) reaches FIN immediately,
 -- pruning second branch (A B+) before it can accumulate B repetitions.
@@ -718,12 +734,13 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | a b)+
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 6 peak, 306 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 99 pruned
    NFA: 1 matched (len 1/1/1.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Nested quantifiers causing state growth
 CREATE VIEW rpr_ev_state_nested_quant AS
@@ -756,13 +773,14 @@ WINDOW w AS (
  WindowAgg (actual rows=1000.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 6 peak, 5004 total, 0 merged
    NFA Contexts: 3 peak, 1001 total, 333 pruned
    NFA: 334 matched (len 1/2/2.0), 0 mismatched
    NFA: 0 absorbed, 333 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
@@ -798,13 +816,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 2 peak, 51 total, 0 pruned
    NFA: 10 matched (len 5/5/5.0), 0 mismatched
    NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- No absorption - bounded quantifier
 CREATE VIEW rpr_ev_ctx_no_absorb AS
@@ -837,13 +856,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{2,4} b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 101 total, 0 merged
    NFA Contexts: 5 peak, 51 total, 0 pruned
    NFA: 10 matched (len 5/5/5.0), 0 mismatched
    NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Contexts skipped by SKIP PAST LAST ROW
 CREATE VIEW rpr_ev_ctx_skip AS
@@ -876,13 +896,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 80 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
    NFA: 0 absorbed, 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- High context absorption - unbounded group
 CREATE VIEW rpr_ev_ctx_absorb_group AS
@@ -915,13 +936,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+" c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 134 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 34 pruned
    NFA: 33 matched (len 3/3/3.0), 0 mismatched
    NFA: 0 absorbed, 33 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Fixed-length group absorption: (A B B)+ C
 -- B B merged to B{2}; absorbable with fixed-length check
@@ -956,13 +978,14 @@ WINDOW w AS (
  WindowAgg (actual rows=70.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b{2}')+" c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 4 peak, 71 total, 40 pruned
    NFA: 10 matched (len 7/7/7.0), 0 mismatched
    NFA: 10 absorbed (len 3/3/3.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=70.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Nested fixed-length group absorption: (A (B C){2} D)+ E
 -- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
@@ -1000,13 +1023,14 @@ WINDOW w AS (
  WindowAgg (actual rows=65.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' (b' c'){2}' d')+" e
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 76 total, 0 merged
    NFA Contexts: 4 peak, 66 total, 50 pruned
    NFA: 5 matched (len 13/13/13.0), 0 mismatched
    NFA: 5 absorbed (len 6/6/6.0), 5 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=65.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
 -- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
@@ -1052,13 +1076,14 @@ WINDOW w AS (
  WindowAgg (actual rows=82.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' ((b' c{3}'){2}' d'){2}' e')+" f
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 87 total, 0 merged
    NFA Contexts: 4 peak, 83 total, 76 pruned
    NFA: 2 matched (len 41/41/41.0), 0 mismatched
    NFA: 2 absorbed (len 20/20/20.0), 2 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=82.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- 3-level END chain absorption: ((A (B C){2}){2})+
 -- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
@@ -1097,13 +1122,14 @@ WINDOW w AS (
  WindowAgg (actual rows=42.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a' (b' c'){2}'){2}')+"
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 47 total, 0 merged
    NFA Contexts: 5 peak, 43 total, 30 pruned
    NFA: 2 matched (len 20/20/20.0), 0 mismatched
    NFA: 2 absorbed (len 10/10/10.0), 8 skipped (len 1/5/3.0)
    ->  Function Scan on generate_series s (actual rows=42.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Match Length Statistics Tests
@@ -1143,13 +1169,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c d e
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 60 pruned
    NFA: 20 matched (len 5/5/5.0), 0 mismatched
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Variable length matches - min/max/avg differ
 CREATE VIEW rpr_ev_mlen_variable AS
@@ -1182,13 +1209,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 191 total, 0 merged
    NFA Contexts: 2 peak, 101 total, 0 pruned
    NFA: 10 matched (len 10/10/10.0), 0 mismatched
    NFA: 80 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Very long matches
 CREATE VIEW rpr_ev_mlen_long AS
@@ -1221,13 +1249,14 @@ WINDOW w AS (
  WindowAgg (actual rows=200.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 396 total, 0 merged
    NFA Contexts: 2 peak, 201 total, 4 pruned
    NFA: 1 matched (len 196/196/196.0), 0 mismatched
    NFA: 194 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=200.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Uniform match length with mismatches from gap rows (v%20 = 11..15)
 CREATE VIEW rpr_ev_mlen_with_mismatch AS
@@ -1264,13 +1293,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 171 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 25 pruned
    NFA: 5 matched (len 5/5/5.0), 5 mismatched (len 11/11/11.0)
    NFA: 60 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Mismatch Length Statistics Tests
@@ -1321,13 +1351,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b+ c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 151 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 60 pruned
    NFA: 10 matched (len 6/6/6.0), 0 mismatched
    NFA: 20 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Long partial matches that fail
 CREATE VIEW rpr_ev_mlen_long_partial AS
@@ -1384,13 +1415,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b+ c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 115 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 15 pruned
    NFA: 1 matched (len 30/30/30.0), 1 mismatched (len 26/26/26.0)
    NFA: 42 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series i (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- JSON Format Tests
@@ -1434,6 +1466,7 @@ WINDOW w AS (
        "Disabled": false,                                                  +
        "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
        "Pattern": "a+\" b+",                                               +
+       "Nav Mark Lookback": 0,                                             +
        "Storage": "Memory",                                                +
        "Maximum Storage": 0,                                               +
        "NFA States Peak": 3,                                               +
@@ -1511,6 +1544,7 @@ WINDOW w AS (
        "Disabled": false,                                                  +
        "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
        "Pattern": "a+\" b",                                                +
+       "Nav Mark Lookback": 0,                                             +
        "Storage": "Memory",                                                +
        "Maximum Storage": 0,                                               +
        "NFA States Peak": 3,                                               +
@@ -1592,6 +1626,7 @@ WINDOW w AS (
        "Disabled": false,                                                  +
        "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
        "Pattern": "a b c",                                                 +
+       "Nav Mark Lookback": 0,                                             +
        "Storage": "Memory",                                                +
        "Maximum Storage": 0,                                               +
        "NFA States Peak": 2,                                               +
@@ -1672,6 +1707,7 @@ WINDOW w AS (
        "Disabled": false,                                                  +
        "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
        "Pattern": "(a | b){8}",                                            +
+       "Nav Mark Lookback": 0,                                             +
        "Storage": "Memory",                                                +
        "Maximum Storage": 0,                                               +
        "NFA States Peak": 17,                                              +
@@ -1755,6 +1791,7 @@ WINDOW w AS (
        <Disabled>false</Disabled>                                              +
        <Window>w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)</Window>+
        <Pattern>a b</Pattern>                                                  +
+       <Nav-Mark-Lookback>0</Nav-Mark-Lookback>                                +
        <Storage>Memory</Storage>                                               +
        <Maximum-Storage>0</Maximum-Storage>                                    +
        <NFA-States-Peak>2</NFA-States-Peak>                                    +
@@ -1837,6 +1874,7 @@ WINDOW w AS (
  WindowAgg (actual rows=90.00 loops=1)
    Window: w AS (PARTITION BY p.p ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 165 total, 0 merged
    NFA Contexts: 2 peak, 93 total, 0 pruned
@@ -1848,7 +1886,7 @@ WINDOW w AS (
          ->  Nested Loop (actual rows=90.00 loops=1)
                ->  Function Scan on generate_series p (actual rows=3.00 loops=1)
                ->  Function Scan on generate_series v (actual rows=30.00 loops=3)
-(14 rows)
+(15 rows)
 
 -- Different pattern behavior per partition
 CREATE VIEW rpr_ev_part_diff AS
@@ -1893,6 +1931,7 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (PARTITION BY (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 77 total, 0 merged
    NFA Contexts: 2 peak, 52 total, 21 pruned
@@ -1902,7 +1941,7 @@ WINDOW w AS (
          Sort Key: (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END)
          Sort Method: quicksort  Memory: NkB
          ->  Function Scan on generate_series v (actual rows=50.00 loops=1)
-(12 rows)
+(13 rows)
 
 -- ============================================================
 -- Edge Cases
@@ -1938,8 +1977,9 @@ WINDOW w AS (
  WindowAgg (actual rows=0.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b
+   Nav Mark Lookback: 0
    ->  Function Scan on generate_series s (actual rows=0.00 loops=1)
-(4 rows)
+(5 rows)
 
 -- Single row
 CREATE VIEW rpr_ev_edge_single_row AS
@@ -1972,12 +2012,13 @@ WINDOW w AS (
  WindowAgg (actual rows=1.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 2 total, 0 merged
    NFA Contexts: 2 peak, 2 total, 0 pruned
    NFA: 1 matched (len 1/1/1.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=1.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Pattern longer than data
 CREATE VIEW rpr_ev_edge_pattern_longer AS
@@ -2014,12 +2055,13 @@ WINDOW w AS (
  WindowAgg (actual rows=5.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c d e f g h i j
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 6 total, 0 merged
    NFA Contexts: 3 peak, 6 total, 4 pruned
    NFA: 0 matched, 1 mismatched (len 5/5/5.0)
    ->  Function Scan on generate_series s (actual rows=5.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- All rows match as single match
 CREATE VIEW rpr_ev_edge_single_match AS
@@ -2052,13 +2094,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 101 total, 0 merged
    NFA Contexts: 2 peak, 51 total, 0 pruned
    NFA: 1 matched (len 50/50/50.0), 0 mismatched
    NFA: 49 absorbed (len 1/1/1.0), 0 skipped
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Complex Pattern Tests
@@ -2094,13 +2137,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b' c')+"
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 81 total, 0 merged
    NFA Contexts: 4 peak, 61 total, 20 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
    NFA: 19 absorbed (len 3/3/3.0), 20 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Multiple alternations
 CREATE VIEW rpr_ev_cpx_multi_alt AS
@@ -2137,13 +2181,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b) (c | d | e)
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 6 peak, 423 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 40 pruned
    NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Optional elements
 CREATE VIEW rpr_ev_cpx_optional AS
@@ -2176,13 +2221,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b? c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 64 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 25 pruned
    NFA: 12 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 12 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Bounded quantifiers
 CREATE VIEW rpr_ev_cpx_bounded AS
@@ -2215,13 +2261,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{2,5} b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 9 peak, 311 total, 0 merged
    NFA Contexts: 7 peak, 101 total, 0 pruned
    NFA: 10 matched (len 6/6/6.0), 40 mismatched (len 6/6/6.0)
    NFA: 0 absorbed, 50 skipped (len 1/5/3.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Star quantifier
 CREATE VIEW rpr_ev_cpx_star AS
@@ -2254,13 +2301,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b* c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 40 pruned
    NFA: 5 matched (len 9/9/9.0), 0 mismatched
    NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Real-world Pattern Examples
@@ -2296,13 +2344,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: d+" u+
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 58 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 3 pruned
    NFA: 3 matched (len 3/14/8.0), 1 mismatched (len 3/3/3.0)
    NFA: 9 absorbed (len 1/1/1.0), 14 skipped (len 1/1/1.0)
    ->  Seq Scan on rpr_nfa_complex (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Stock price pattern - peak (up, stable, down)
 CREATE VIEW rpr_ev_real_peak AS
@@ -2335,13 +2384,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: u+" s* d+
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 76 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 1 pruned
    NFA: 4 matched (len 3/11/7.2), 0 mismatched
    NFA: 12 absorbed (len 1/1/1.0), 13 skipped (len 1/1/1.0)
    ->  Seq Scan on rpr_nfa_complex (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Consecutive increasing values (using PREV)
 CREATE VIEW rpr_ev_real_increasing AS
@@ -2374,13 +2424,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{3,}"
+   Nav Mark Lookback: 1
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 99 total, 0 merged
    NFA Contexts: 2 peak, 51 total, 0 pruned
    NFA: 1 matched (len 50/50/50.0), 0 mismatched
    NFA: 49 absorbed (len 1/1/1.0), 0 skipped
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Performance-oriented Tests
@@ -2416,13 +2467,14 @@ WINDOW w AS (
  WindowAgg (actual rows=1000.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 1001 total, 0 merged
    NFA Contexts: 2 peak, 1001 total, 0 pruned
    NFA: 500 matched (len 2/2/2.0), 0 mismatched
    NFA: 0 absorbed, 500 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Large dataset with absorption
 CREATE VIEW rpr_ev_perf_large_absorb AS
@@ -2455,13 +2507,14 @@ WINDOW w AS (
  WindowAgg (actual rows=1000.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 1991 total, 0 merged
    NFA Contexts: 2 peak, 1001 total, 0 pruned
    NFA: 10 matched (len 100/100/100.0), 0 mismatched
    NFA: 980 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- High state merge ratio
 CREATE VIEW rpr_ev_perf_high_merge AS
@@ -2494,13 +2547,14 @@ WINDOW w AS (
  WindowAgg (actual rows=500.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+ c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 9 peak, 3006 total, 0 merged
    NFA Contexts: 3 peak, 501 total, 1 pruned
    NFA: 166 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 332 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- INITIAL vs no INITIAL comparison
@@ -2538,13 +2592,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 2 peak, 51 total, 0 pruned
    NFA: 10 matched (len 5/5/5.0), 0 mismatched
    NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Without INITIAL keyword (same behavior currently)
 CREATE VIEW rpr_ev_initial_without AS
@@ -2577,13 +2632,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 2 peak, 51 total, 0 pruned
    NFA: 10 matched (len 5/5/5.0), 0 mismatched
    NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Quantifier Variations
@@ -2619,13 +2675,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 71 total, 0 merged
    NFA Contexts: 3 peak, 41 total, 10 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
    NFA: 20 absorbed (len 1/1/1.0), 0 skipped
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Star quantifier (zero or more)
 CREATE VIEW rpr_ev_quant_star AS
@@ -2658,13 +2715,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a*" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 102 total, 0 merged
    NFA Contexts: 2 peak, 41 total, 10 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
    NFA: 10 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Question mark (zero or one)
 CREATE VIEW rpr_ev_quant_question AS
@@ -2697,13 +2755,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a? b c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 82 total, 0 merged
    NFA Contexts: 3 peak, 41 total, 10 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
    NFA: 0 absorbed, 20 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Exact count {n}
 CREATE VIEW rpr_ev_quant_exact AS
@@ -2736,13 +2795,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{3} b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 51 total, 0 merged
    NFA Contexts: 5 peak, 51 total, 0 pruned
    NFA: 10 matched (len 4/4/4.0), 10 mismatched (len 4/4/4.0)
    NFA: 0 absorbed, 30 skipped (len 1/3/2.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Range {n,m}
 CREATE VIEW rpr_ev_quant_range AS
@@ -2775,13 +2835,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{2,4} b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 101 total, 0 merged
    NFA Contexts: 5 peak, 51 total, 0 pruned
    NFA: 10 matched (len 5/5/5.0), 0 mismatched
    NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- At least {n,}
 CREATE VIEW rpr_ev_quant_atleast AS
@@ -2814,13 +2875,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{3,}" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 86 total, 0 merged
    NFA Contexts: 2 peak, 51 total, 0 pruned
    NFA: 5 matched (len 10/10/10.0), 0 mismatched
    NFA: 40 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Regression Tests for Statistics Accuracy
@@ -2857,13 +2919,14 @@ WINDOW w AS (
  WindowAgg (actual rows=20.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 37 total, 0 merged
    NFA Contexts: 2 peak, 21 total, 0 pruned
    NFA: 4 matched (len 5/5/5.0), 0 mismatched
    NFA: 12 absorbed (len 1/1/1.0), 4 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Verify context count with known absorption
 CREATE VIEW rpr_ev_reg_ctx_absorb AS
@@ -2896,13 +2959,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 52 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 6 pruned
    NFA: 3 matched (len 9/9/9.0), 0 mismatched
    NFA: 18 absorbed (len 1/1/1.0), 3 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Verify match length with fixed-length pattern
 CREATE VIEW rpr_ev_reg_matchlen AS
@@ -2935,13 +2999,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 31 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 10 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
    NFA: 0 absorbed, 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Alternation Pattern Tests
@@ -2977,13 +3042,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b) c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 303 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 40 pruned
    NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Multiple items in alternation
 CREATE VIEW rpr_ev_alt_multi_item AS
@@ -3020,13 +3086,14 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b | c | d) e
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 6 peak, 505 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 0 pruned
    NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Alternation with quantifiers
 CREATE VIEW rpr_ev_alt_with_quant AS
@@ -3059,13 +3126,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+ c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 9 peak, 306 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 1 pruned
    NFA: 16 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 32 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Multiple alternatives (4+)
 CREATE VIEW rpr_ev_alt_four_plus AS
@@ -3096,12 +3164,13 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b | c | d | e)
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 606 total, 0 merged
    NFA Contexts: 2 peak, 101 total, 0 pruned
    NFA: 100 matched (len 1/1/1.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Alternation at start
 CREATE VIEW rpr_ev_alt_at_start AS
@@ -3132,13 +3201,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b) c d
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 183 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 16 pruned
    NFA: 15 matched (len 3/3/3.0), 14 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 15 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Multiple sequential alternations
 CREATE VIEW rpr_ev_alt_sequential AS
@@ -3169,12 +3239,13 @@ WINDOW w AS (
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b) c (d | e) f
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 337 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 67 pruned
    NFA: 0 matched, 33 mismatched (len 2/4/3.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Quantified alternatives
 CREATE VIEW rpr_ev_alt_quantified AS
@@ -3205,13 +3276,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a+" | b+") c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 223 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 1 pruned
    NFA: 20 matched (len 2/2/2.0), 19 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Alternation at end
 CREATE VIEW rpr_ev_alt_at_end AS
@@ -3242,13 +3314,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b (c | d)
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 89 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 32 pruned
    NFA: 14 matched (len 3/3/3.0), 0 mismatched
    NFA: 0 absorbed, 14 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Nested ALT at start of branch inside outer ALT
 -- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
@@ -3280,12 +3353,13 @@ WINDOW w AS (
  WindowAgg (actual rows=20.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a ((b | c) d | e)
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 37 total, 0 merged
    NFA Contexts: 3 peak, 21 total, 17 pruned
    NFA: 0 matched, 3 mismatched (len 3/3/3.0)
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Nested ALT at end of branch inside outer ALT
 -- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
@@ -3317,12 +3391,13 @@ WINDOW w AS (
  WindowAgg (actual rows=20.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (c (a | b) | d)
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 73 total, 0 merged
    NFA Contexts: 3 peak, 21 total, 10 pruned
    NFA: 5 matched (len 1/1/1.0), 5 mismatched (len 2/2/2.0)
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- ============================================================
 -- Group Pattern Tests
@@ -3358,13 +3433,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+"
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 61 total, 0 merged
    NFA Contexts: 3 peak, 41 total, 0 pruned
    NFA: 1 matched (len 40/40/40.0), 0 mismatched
    NFA: 19 absorbed (len 2/2/2.0), 20 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Group with bounded quantifier
 CREATE VIEW rpr_ev_grp_bounded AS
@@ -3397,13 +3473,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b){2,4}
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 51 total, 0 merged
    NFA Contexts: 3 peak, 41 total, 5 pruned
    NFA: 5 matched (len 8/8/8.0), 0 mismatched
    NFA: 0 absorbed, 30 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Nested groups
 CREATE VIEW rpr_ev_grp_nested AS
@@ -3436,13 +3513,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a' b'){2}')+"
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 76 total, 0 merged
    NFA Contexts: 4 peak, 61 total, 15 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
    NFA: 14 absorbed (len 4/4/4.0), 30 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Deep nesting (3+ levels)
 CREATE VIEW rpr_ev_grp_deep AS
@@ -3473,13 +3551,14 @@ WINDOW w AS (
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 6 peak, 243 total, 0 merged
    NFA Contexts: 2 peak, 41 total, 0 pruned
    NFA: 1 matched (len 40/40/40.0), 0 mismatched
    NFA: 0 absorbed, 39 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Bounded quantifier on alternation
 CREATE VIEW rpr_ev_grp_bounded_alt AS
@@ -3510,13 +3589,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b){2,3} c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 8 peak, 320 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 2 pruned
    NFA: 19 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
    NFA: 0 absorbed, 38 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Nested groups with quantifiers
 CREATE VIEW rpr_ev_grp_nested_quant AS
@@ -3547,13 +3627,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a' b')+" c)*
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 9 peak, 178 total, 0 merged
    NFA Contexts: 4 peak, 61 total, 20 pruned
    NFA: 3 matched (len 0/57/19.0), 0 mismatched
    NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Partial nested quantification
 CREATE VIEW rpr_ev_grp_partial_quant AS
@@ -3584,13 +3665,14 @@ WINDOW w AS (
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a (b c)+)*
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 160 total, 0 merged
    NFA Contexts: 4 peak, 61 total, 20 pruned
    NFA: 3 matched (len 0/57/19.0), 0 mismatched
    NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Window Function Combinations
@@ -3626,13 +3708,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 2 peak, 31 total, 0 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
    NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- first_value with pattern
 CREATE VIEW rpr_ev_wfn_first_value AS
@@ -3665,13 +3748,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 2 peak, 31 total, 0 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
    NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- last_value with pattern
 CREATE VIEW rpr_ev_wfn_last_value AS
@@ -3704,13 +3788,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 2 peak, 31 total, 0 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
    NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Multiple window functions
 CREATE VIEW rpr_ev_wfn_multi AS
@@ -3749,13 +3834,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 2 peak, 31 total, 0 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
    NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- DEFINE Expression Variations
@@ -3795,13 +3881,14 @@ WINDOW w AS (
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 78 total, 0 merged
    NFA Contexts: 2 peak, 51 total, 6 pruned
    NFA: 17 matched (len 2/3/2.6), 0 mismatched
    NFA: 10 absorbed (len 1/1/1.0), 17 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- Using PREV function
 CREATE VIEW rpr_ev_def_prev AS
@@ -3840,12 +3927,13 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: s u+ d+
+   Nav Mark Lookback: 1
    Storage: Memory  Maximum Storage: NkB
    NFA States: 60 peak, 466 total, 0 merged
    NFA Contexts: 31 peak, 31 total, 1 pruned
    NFA: 0 matched, 29 mismatched (len 2/30/16.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(8 rows)
+(9 rows)
 
 -- Using 1-arg PREV (implicit offset 1)
 CREATE VIEW rpr_ev_nav_prev1 AS
@@ -3964,13 +4052,14 @@ WINDOW w AS (
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 2 peak, 31 total, 0 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
    NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series v (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- ============================================================
 -- Large Scale Statistics Verification
@@ -4006,13 +4095,14 @@ WINDOW w AS (
  WindowAgg (actual rows=500.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b c
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 851 total, 0 merged
    NFA Contexts: 3 peak, 501 total, 101 pruned
    NFA: 50 matched (len 8/9/9.0), 0 mismatched
    NFA: 299 absorbed (len 1/1/1.0), 50 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- High match count scenario
 CREATE VIEW rpr_ev_scale_high_match AS
@@ -4045,13 +4135,14 @@ WINDOW w AS (
  WindowAgg (actual rows=500.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 501 total, 0 merged
    NFA Contexts: 2 peak, 501 total, 0 pruned
    NFA: 250 matched (len 2/2/2.0), 0 mismatched
    NFA: 0 absorbed, 250 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
+(10 rows)
 
 -- High skip count scenario
 CREATE VIEW rpr_ev_scale_high_skip AS
@@ -4094,13 +4185,14 @@ WINDOW w AS (
  WindowAgg (actual rows=500.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c d e
+   Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 501 total, 0 merged
    NFA Contexts: 3 peak, 501 total, 490 pruned
    NFA: 5 matched (len 5/5/5.0), 0 mismatched
    NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
+(10 rows)
 
 --
 -- Planner optimization: optimize_window_clauses must not alter RPR frame
@@ -4149,10 +4241,11 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr;
    ->  WindowAgg
          Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: s.v
                ->  Function Scan on generate_series s
-(7 rows)
+(8 rows)
 
 --
 -- Planner optimization: non-RPR and RPR windows that share the same base frame
@@ -4178,12 +4271,13 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
    ->  WindowAgg
          Window: w_rpr AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a+"
+         Nav Mark Lookback: 0
          ->  WindowAgg
                Window: w_normal AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING)
                ->  Sort
                      Sort Key: s.v
                      ->  Function Scan on generate_series s
-(9 rows)
+(10 rows)
 
 --
 -- Planner optimization: find_window_run_conditions must not push down
@@ -4242,8 +4336,133 @@ SELECT * FROM (
    ->  WindowAgg
          Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: s.v
                ->  Function Scan on generate_series s
-(8 rows)
+(9 rows)
+
+-- ============================================================
+-- Nav Mark Lookback Tests
+-- Verifies planner-computed navigation offset for tuplestore trim.
+-- Lookback: how far back from currentpos (PREV/LAST).
+-- ============================================================
+-- Prepare statement for host variable offset test below
+PREPARE rpr_nav_offset_prep(int8) AS
+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 v > PREV(v, $1)
+);
+-- No navigation function: offset 0
+EXPLAIN (COSTS OFF) 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 v > 0
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: 0
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- NEXT only: no backward navigation, offset 0
+EXPLAIN (COSTS OFF) 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 v < NEXT(v)
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: 0
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- PREV(v): implicit offset 1
+EXPLAIN (COSTS OFF) 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 v > PREV(v)
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: 1
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- PREV(v, 3): explicit constant offset 3
+EXPLAIN (COSTS OFF) 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 v > PREV(v, 3)
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: 3
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- Two PREV with different offsets: max(1, 5) = 5
+EXPLAIN (COSTS OFF) 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, 1) < v AND PREV(v, 5) < v
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: 5
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- Host variable offset: custom plan resolves $1=2 to constant 2
+EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: 2
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- Force generic plan: offset becomes "runtime" (Param node)
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: runtime
+   ->  Function Scan on generate_series s
+(5 rows)
 
+RESET plan_cache_mode;
+DEALLOCATE rpr_nav_offset_prep;
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index a21ac5a8588..20c9fe89cec 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -66,10 +66,11 @@ WINDOW w AS (ORDER BY id
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_integ
-(6 rows)
+(7 rows)
 
 -- ============================================================
 -- A2. Run condition pushdown bypass
@@ -112,10 +113,11 @@ SELECT * FROM (
    ->  WindowAgg
          Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: rpr_integ.id
                ->  Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
 
 -- Verify results are correct
 SELECT * FROM (
@@ -160,10 +162,11 @@ WINDOW
    ->  WindowAgg
          Window: w_rpr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: id
                ->  Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
 
 -- Verify both produce different results
 SELECT
@@ -218,13 +221,15 @@ WINDOW
  WindowAgg
    Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  WindowAgg
          Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: id
                ->  Seq Scan on rpr_integ
-(9 rows)
+(11 rows)
 
 -- Verify results differ
 SELECT
@@ -383,10 +388,11 @@ WHERE cnt > 0;
    ->  WindowAgg
          Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: rpr_integ.id
                ->  Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
 
 -- ============================================================
 -- A9. DEFINE expression non-propagation
@@ -415,12 +421,13 @@ WINDOW
          Output: id, val, count(*) OVER w_rpr
          Window: w_rpr AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Output: id, val
                Sort Key: rpr_integ.id
                ->  Seq Scan on public.rpr_integ
                      Output: id, val
-(12 rows)
+(13 rows)
 
 -- ============================================================
 -- A10. RPR + LIMIT
@@ -712,12 +719,13 @@ WINDOW w AS (ORDER BY id
  WindowAgg
    Window: w AS (ORDER BY rpr_part.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Sort
          Sort Key: rpr_part.id
          ->  Append
                ->  Seq Scan on rpr_part_1
                ->  Seq Scan on rpr_part_2
-(8 rows)
+(9 rows)
 
 DROP TABLE rpr_part;
 -- ============================================================
@@ -798,8 +806,9 @@ WINDOW w AS (ORDER BY id
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Index Scan using rpr_integ_id_idx on rpr_integ
-(4 rows)
+(5 rows)
 
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index e339edd7e91..5082cc2b5de 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -36,6 +36,7 @@
 --   Window Function Combinations
 --   DEFINE Expression Variations
 --   Large Scale Statistics Verification
+--   Nav Mark Lookback (tuplestore trim)
 -- ============================================================
 
 -- Filter function to normalize platform-dependent memory values (not NFA statistics).
@@ -2476,3 +2477,73 @@ SELECT * FROM (
     )
 ) t WHERE cnt > 0;
 
+-- ============================================================
+-- Nav Mark Lookback Tests
+-- Verifies planner-computed navigation offset for tuplestore trim.
+-- Lookback: how far back from currentpos (PREV/LAST).
+-- ============================================================
+
+-- Prepare statement for host variable offset test below
+PREPARE rpr_nav_offset_prep(int8) AS
+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 v > PREV(v, $1)
+);
+
+-- No navigation function: offset 0
+EXPLAIN (COSTS OFF) 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 v > 0
+);
+
+-- NEXT only: no backward navigation, offset 0
+EXPLAIN (COSTS OFF) 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 v < NEXT(v)
+);
+
+-- PREV(v): implicit offset 1
+EXPLAIN (COSTS OFF) 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 v > PREV(v)
+);
+
+-- PREV(v, 3): explicit constant offset 3
+EXPLAIN (COSTS OFF) 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 v > PREV(v, 3)
+);
+
+-- Two PREV with different offsets: max(1, 5) = 5
+EXPLAIN (COSTS OFF) 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, 1) < v AND PREV(v, 5) < v
+);
+
+-- Host variable offset: custom plan resolves $1=2 to constant 2
+EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
+
+-- Force generic plan: offset becomes "runtime" (Param node)
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
+RESET plan_cache_mode;
+DEALLOCATE rpr_nav_offset_prep;
+
-- 
2.50.1 (Apple Git-155)

From e5c7033b71a8c0302fb3fa48790157f6cd70585e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 5 Apr 2026 00:56:20 +0900
Subject: [PATCH] Implement FIRST/LAST and compound navigation for RPR

Add compound navigation: PREV(FIRST()), NEXT(FIRST()),
PREV(LAST()), NEXT(LAST()) per SQL standard 5.6.4.  The parser
flattens nested RPRNavExpr into a single compound node with two
offsets.  The executor computes the target position in two steps
with range validation at each step.  ruleutils restores the nested
syntax for deparsing.

Extend tuplestore trim to handle FIRST navigation via
hasFirstNav/navFirstOffset, allowing mark advance based on
oldest active context's matchStartRow.  Compound offsets are
computed by the unified nav_offset_walker in a single expression
tree walk.  Add "Nav Mark Lookahead" to EXPLAIN for FIRST-based
navigation.
---
 doc/src/sgml/func/func-window.sgml        |  53 ++
 src/backend/commands/explain.c            |  68 ++-
 src/backend/executor/execExpr.c           |  55 +-
 src/backend/executor/execExprInterp.c     | 155 ++++-
 src/backend/executor/execRPR.c            | 152 ++++-
 src/backend/executor/nodeWindowAgg.c      | 343 +++++++++---
 src/backend/nodes/nodeFuncs.c             |   3 +
 src/backend/optimizer/plan/createplan.c   | 387 +++++++++++--
 src/backend/optimizer/plan/rpr.c          |   6 +-
 src/backend/parser/parse_func.c           |  70 ++-
 src/backend/parser/parse_rpr.c            | 112 +++-
 src/backend/utils/adt/ruleutils.c         |  81 ++-
 src/backend/utils/adt/windowfuncs.c       |  56 ++
 src/include/catalog/pg_proc.dat           |  12 +
 src/include/executor/execExpr.h           |   8 +-
 src/include/nodes/execnodes.h             |  12 +-
 src/include/nodes/parsenodes.h            |  17 +-
 src/include/nodes/plannodes.h             |  30 +-
 src/include/nodes/primnodes.h             |  37 +-
 src/include/optimizer/rpr.h               |  12 +-
 src/test/regress/expected/rpr.out         | 652 +++++++++++++++++++++-
 src/test/regress/expected/rpr_base.out    | 257 ++++++++-
 src/test/regress/expected/rpr_explain.out | 425 +++++++++++++-
 src/test/regress/sql/rpr.sql              | 374 +++++++++++++
 src/test/regress/sql/rpr_base.sql         | 136 ++++-
 src/test/regress/sql/rpr_explain.sql      | 242 +++++++-
 src/tools/pgindent/typedefs.list          |   5 +
 27 files changed, 3509 insertions(+), 251 deletions(-)

diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index 1b9b993a817..ab80690f7be 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -337,10 +337,63 @@
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>first</primary>
+        </indexterm>
+        <function>first</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
+        <returnvalue>anyelement</returnvalue>
+       </para>
+       <para>
+        Returns the column value at the row <parameter>offset</parameter>
+        rows after the match start row;
+        returns NULL if the target row is beyond the current row.
+        <parameter>offset</parameter> defaults to 0 if omitted, referring to the
+        match start row itself.
+        <parameter>offset</parameter> must be a non-negative integer.
+        <parameter>offset</parameter> must not be NULL.
+        Can only be used in a <literal>DEFINE</literal> clause.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>last</primary>
+        </indexterm>
+        <function>last</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
+        <returnvalue>anyelement</returnvalue>
+       </para>
+       <para>
+        Returns the column value at the row <parameter>offset</parameter>
+        rows before the current row within the match;
+        returns NULL if the target row is before the match start row.
+        <parameter>offset</parameter> defaults to 0 if omitted, referring to the
+        current row itself.
+        <parameter>offset</parameter> must be a non-negative integer.
+        <parameter>offset</parameter> must not be NULL.
+        Can only be used in a <literal>DEFINE</literal> clause.
+       </para></entry>
+      </row>
+
      </tbody>
     </tgroup>
    </table>
 
+   <para>
+    <function>PREV</function> and <function>NEXT</function> may wrap
+    <function>FIRST</function> or <function>LAST</function> for compound
+    navigation. For example,
+    <literal>PREV(FIRST(val, 2), 3)</literal> fetches the value at
+    3 rows before the row that is 2 rows after the match start.
+    The reverse nesting (<function>FIRST</function>/<function>LAST</function>
+    wrapping <function>PREV</function>/<function>NEXT</function>) is not
+    permitted. Same-category nesting (e.g.,
+    <function>PREV</function> inside <function>PREV</function>) is also
+    prohibited.
+   </para>
+
   <note>
    <para>
     The SQL standard defines a <literal>FROM FIRST</literal> or <literal>FROM LAST</literal>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1848de9de7a..221d9a49e0d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3255,14 +3255,66 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 			pfree(patternStr);
 		}
 
-		/* Show navigation offsets for tuplestore trim */
-		if (wagg->navMaxOffset == RPR_NAV_OFFSET_RETAIN_ALL)
-			ExplainPropertyText("Nav Mark Lookback", "retain all", es);
-		else if (wagg->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
-			ExplainPropertyText("Nav Mark Lookback", "runtime", es);
-		else
-			ExplainPropertyInteger("Nav Mark Lookback", NULL,
-								   wagg->navMaxOffset, es);
+		/*
+		 * 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).
+		 */
+		{
+			RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
+			int64		maxOffset = wagg->navMaxOffset;
+			RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
+			int64		firstOffset = wagg->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;
+			}
+
+			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:
+						ExplainPropertyInteger("Nav Mark Lookahead", NULL,
+											   firstOffset, es);
+						break;
+					default:
+						elog(ERROR, "unrecognized RPR nav offset kind: %d",
+							 firstKind);
+						break;
+				}
+			}
+		}
 	}
 }
 
index dbed4f48a0f..6349a564a98 100644
@@ -1243,25 +1247,52 @@ ExecInitExprRec(Expr *node, ExprState *state,
 				scratch.d.rpr_nav.winstate = winstate;
 				scratch.d.rpr_nav.kind = nav->kind;
 
-				if (nav->offset_arg != NULL)
+				if (nav->kind >= RPR_NAV_PREV_FIRST)
 				{
 					/*
-					 * Allocate storage for the runtime offset value.  The
-					 * offset expression is compiled below so it runs before
-					 * EEOP_RPR_NAV_SET.
+					 * Compound navigation: allocate array of 2 for inner [0]
+					 * and outer [1] offsets.
 					 */
+					Datum	   *offset_values = palloc_array(Datum, 2);
+					bool	   *offset_isnulls = palloc_array(bool, 2);
+
+					/* Inner offset (default 0 for FIRST/LAST) */
+					if (nav->offset_arg != NULL)
+						ExecInitExprRec(nav->offset_arg, state,
+										&offset_values[0], &offset_isnulls[0]);
+					else
+					{
+						offset_values[0] = Int64GetDatum(0);
+						offset_isnulls[0] = false;
+					}
+
+					/* Outer offset (default 1 for PREV/NEXT) */
+					if (nav->compound_offset_arg != NULL)
+						ExecInitExprRec(nav->compound_offset_arg, state,
+										&offset_values[1], &offset_isnulls[1]);
+					else
+					{
+						offset_values[1] = Int64GetDatum(1);
+						offset_isnulls[1] = false;
+					}
+
+					scratch.d.rpr_nav.offset_value = offset_values;
+					scratch.d.rpr_nav.offset_isnull = offset_isnulls;
+				}
+				else if (nav->offset_arg != NULL)
+				{
+					/* Simple navigation with explicit offset */
 					Datum	   *offset_value = palloc_object(Datum);
 					bool	   *offset_isnull = palloc_object(bool);
 
-					/* Compile the offset expression into the temp storage */
 					ExecInitExprRec(nav->offset_arg, state,
 									offset_value, offset_isnull);
-
 					scratch.d.rpr_nav.offset_value = offset_value;
 					scratch.d.rpr_nav.offset_isnull = offset_isnull;
 				}
 				else
 				{
+					/* Simple navigation with default offset */
 					scratch.d.rpr_nav.offset_value = NULL;
 					scratch.d.rpr_nav.offset_isnull = NULL;
 				}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index e41faa95be3..2ec579732cc 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -5942,7 +5942,35 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
 }
 
 /*
- * Evaluate RPR PREV/NEXT navigation: swap slot to target row.
+ * Extract compound (outer) offset from step data.
+ * For compound nav, offset_value is an array: [0]=inner, [1]=outer.
+ * Returns the outer offset; errors on NULL or negative.
+ * Default is 1 (like PREV/NEXT implicit offset).
+ */
+static int64
+rpr_nav_get_compound_offset(ExprEvalStep *op)
+{
+	int64		val;
+
+	Assert(op->d.rpr_nav.offset_value != NULL);
+
+	if (op->d.rpr_nav.offset_isnull[1])
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("row pattern navigation offset must not be null")));
+
+	val = DatumGetInt64(op->d.rpr_nav.offset_value[1]);
+
+	if (val < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("row pattern navigation offset must not be negative")));
+
+	return val;
+}
+
+/*
+ * Evaluate RPR navigation (PREV/NEXT/FIRST/LAST): swap slot to target row.
  *
  * Saves the current outertuple into winstate for later restore, computes
  * the target row position, fetches the corresponding slot from the
@@ -5963,27 +5991,35 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	winstate->nav_saved_outertuple = econtext->ecxt_outertuple;
 
 	/*
+	 * Determine the inner offset.  NULL or negative offsets are errors per
+	 * the SQL standard.
+	 * FIRST/LAST and compound: 0 for inner, 1 for outer

 		if (offset < 0)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("PREV/NEXT offset must not be negative")));
+					 errmsg("row pattern navigation offset must not be negative")));
 	}
 	else
-		offset = 1;
+	{
+		/* Default offset: 1 for simple PREV/NEXT, 0 otherwise */
+		if (op->d.rpr_nav.kind == RPR_NAV_PREV ||
+			op->d.rpr_nav.kind == RPR_NAV_NEXT)
+			offset = 1;
+		else
+			offset = 0;
+	}
 
 	/*
 	 * Calculate target position based on navigation direction.  On overflow,
@@ -5999,8 +6035,107 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 			if (pg_add_s64_overflow(winstate->currentpos, offset, &target_pos))
 				target_pos = -1;
 			break;
+		case RPR_NAV_FIRST:
+			/* FIRST: offset from match_start, clamped to currentpos */
+			if (pg_add_s64_overflow(winstate->nav_match_start, offset, &target_pos))
+				target_pos = -1;
+			else if (target_pos > winstate->currentpos)
+				target_pos = -1;	/* beyond current match range */
+			break;
+		case RPR_NAV_LAST:
+			/* LAST: offset backward from currentpos, clamped to match_start */
+			if (pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos))
+				target_pos = -1;
+			else if (target_pos < winstate->nav_match_start)
+				target_pos = -1;	/* before match_start */
+			break;
+
+		case RPR_NAV_PREV_FIRST:
+		case RPR_NAV_NEXT_FIRST:
+			{
+				int64		compound_offset;
+				int64		inner_pos;
+
+				/* Inner: match_start + offset */
+				if (pg_add_s64_overflow(winstate->nav_match_start, offset, &inner_pos))
+				{
+					target_pos = -1;
+					break;
+				}
+				if (inner_pos > winstate->currentpos || inner_pos < 0)
+				{
+					target_pos = -1;
+					break;
+				}
+
+				/* Outer offset */
+				compound_offset = rpr_nav_get_compound_offset(op);
+
+				/* Apply outer: PREV subtracts, NEXT adds */
+				if (op->d.rpr_nav.kind == RPR_NAV_PREV_FIRST)
+				{
+					if (pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos))
+						target_pos = -1;
+				}
+				else
+				{
+					if (pg_add_s64_overflow(inner_pos, compound_offset, &target_pos))
+						target_pos = -1;
+				}
+			}
+			break;
+
+		case RPR_NAV_PREV_LAST:
+		case RPR_NAV_NEXT_LAST:
+			{
+				int64		compound_offset;
+				int64		inner_pos;
+
+				/* Inner: currentpos - offset */
+				if (pg_sub_s64_overflow(winstate->currentpos, offset, &inner_pos))
+				{
+					target_pos = -1;
+					break;
+				}
+				if (inner_pos < winstate->nav_match_start)
+				{
+					target_pos = -1;
+					break;
+				}
+
+				/* Outer offset */
+				compound_offset = rpr_nav_get_compound_offset(op);
+
+				/* Apply outer: PREV subtracts, NEXT adds */
+				if (op->d.rpr_nav.kind == RPR_NAV_PREV_LAST)
+				{
+					if (pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos))
+						target_pos = -1;
+				}
+				else
+				{
+					if (pg_add_s64_overflow(inner_pos, compound_offset, &target_pos))
+						target_pos = -1;
+				}
+			}
+			break;
+		default:
+			elog(ERROR, "unrecognized RPR navigation kind: %d",
+				 op->d.rpr_nav.kind);
+			break;
 	}
 
+	/*
 
@@ -6015,9 +6150,11 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 }
 
 /*
- * Evaluate RPR PREV/NEXT navigation: restore slot to original row.
+ * Evaluate RPR navigation: restore slot to original row.
  *
  * Restores econtext->ecxt_outertuple from the saved slot in winstate.
+ * When slot swap was elided (target == currentpos), this is a harmless
+ * no-op since saved and current slots are identical.
  * The caller is responsible for updating any local slot cache.
  */
 void
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 995acdd7be5..60f0d8b2fa1 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -48,6 +48,7 @@
  *     - src/include/nodes/plannodes.h           (plan node definitions)
  *     - src/include/nodes/execnodes.h           (execution state definitions)
  *     - src/include/optimizer/rpr.h             (types and constants)
+ *     - src/backend/optimizer/plan/createplan.c (nav offset computation)
  *
  * ============================================================================
  *
@@ -609,22 +610,69 @@
  *       result = ExecEvalExpr(defineClause[i])
  *       varMatched[i] = (not null and true)
  *
- * To support row navigation operators such as PREV() and NEXT(),
+ * To support row navigation operators (PREV, NEXT, FIRST, LAST),
  * a 1-slot model is used: only ecxt_outertuple is set to the current
- * row.  PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE
- * opcodes emitted during DEFINE expression compilation:
+ * row.  Navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
+ * emitted during DEFINE expression compilation:
  *
  *   NAV_SET:     save ecxt_outertuple, swap in target row via nav_slot
  *   (evaluate):  argument expression reads from swapped slot
  *   NAV_RESTORE: restore original ecxt_outertuple
  *
+ * Compound navigation (PREV(FIRST()), NEXT(FIRST()), PREV(LAST()),
+ * NEXT(LAST())) is flattened by the parser into a single RPRNavExpr
+ * with a compound kind (RPR_NAV_PREV_FIRST, etc.).  The executor
+ * computes the target position in two steps: first the inner reference
+ * point (match_start + N or currentpos - N) with match-range validation,
+ * then the outer adjustment (± M) with partition-range validation.
+ * If either step is out of range, the result is NULL.
+ *
  * nav_slot caches the last fetched position (nav_slot_pos) to avoid
- * redundant tuplestore lookups when multiple PREV/NEXT calls target
+ * redundant tuplestore lookups when multiple navigation calls target
  * the same row.
  *
  * The varMatched array is referenced later in Phase 1 (Match).
  *
- * VI-4. ExecRPRProcessRow(): 3-Phase Processing
+ * VI-4. Per-Context Re-evaluation (match_start-dependent variables)
+ *
+ * DEFINE variables that depend on match_start (those containing FIRST,
+ * LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST)
+ * are identified at plan time via defineMatchStartDependent.  The shared
+ * evaluation in nfa_evaluate_row() uses the head context's matchStartRow
+ * for FIRST/LAST base position.
+ *
+ * When processing a context whose matchStartRow differs from the shared
+ * value, nfa_reevaluate_dependent_vars() temporarily sets nav_match_start
+ * to that context's matchStartRow and re-evaluates only the dependent
+ * variables.  No restore is needed because contexts are ordered by
+ * matchStartRow (ascending), so no later context shares the head's value.
+ *
+ * VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
+ *
+ * Navigation functions require access to past rows via the tuplestore.
+ * To allow tuplestore_trim() to free rows that are no longer reachable,
+ * the planner computes two offsets (see compute_nav_offsets):
+ *
+ *   navMaxOffset (Nav Mark Lookback):
+ *     Maximum backward reach from currentpos.  Contributed by PREV,
+ *     LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
+ *     Mark position: currentpos - navMaxOffset.
+ *
+ *   navFirstOffset (Nav Mark Lookahead):
+ *     Minimum forward offset from match_start.  Contributed by FIRST
+ *     and compound PREV_FIRST/NEXT_FIRST.  Can be negative when
+ *     compound PREV_FIRST looks before match_start.
+ *     Mark position: oldest_context->matchStartRow + navFirstOffset.
+ *
+ * The actual mark is set to: min(lookback_mark, lookahead_mark).
+ * This ensures all rows reachable by any navigation function are retained.
+ *
+ * When offsets contain non-constant expressions (Param), the planner sets
+ * navMaxOffsetKind/navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL and the
+ * executor evaluates them at init time.  On overflow, the kind is set to
+ * RPR_NAV_OFFSET_RETAIN_ALL, disabling trim for that dimension.
+ *
+ * VI-6. ExecRPRProcessRow(): 3-Phase Processing
  *
  * NFA processing for a single row is divided into three phases:
  *
@@ -707,6 +755,21 @@
  *
  * VIII-3. Absorption Conditions
  *
+ * Planner-time prerequisites (all must hold for absorption to be enabled):
+ *
+ *   (a) SKIP PAST LAST ROW.  SKIP TO NEXT ROW creates overlapping
+ *       contexts that cannot be safely absorbed.
+ *   (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
+ *       FOLLOWING).  Limited frames apply differently to each context,
+ *       breaking the monotonicity principle.
+ *   (c) No match_start-dependent navigation in DEFINE.  FIRST,
+ *       LAST-with-offset, and compound navigation referencing match_start
+ *       (PREV_FIRST, NEXT_FIRST, PREV_LAST/NEXT_LAST with offset)
+ *       cause different contexts to evaluate to different values for the
+ *       same row, breaking monotonicity.
+ *
+ * Runtime conditions (evaluated per context pair):
+ *
  *   (1) The pattern is marked as isAbsorbable (see IV-5)
  *   (2) allStatesAbsorbable of the target context is true
  *   (3) An earlier context "covers" all states of the target
@@ -979,6 +1042,19 @@
  *   Only INITIAL is supported, searching only for matches starting at each
  *   row position pos.
  *
+ * X-4. Bounded Frame Handling
+ *
+ *   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
+ *   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
+ *   frameOffset indicating the upper bound.  After the advance phase,
+ *   any context whose match has exceeded the frame boundary
+ *   (currentPos - matchStartRow >= frameOffset + 1) is finalized early.
+ *   This prevents matches from extending beyond the window frame.
+ *
+ *   Note that bounded frames also disable context absorption at the
+ *   planner level (see VIII-3(b)), since the frame boundary breaks the
+ *   monotonicity assumption required for correct absorption.
+ *
  * Chapter XI  Worked Example: Full Execution Trace
  * ============================================================================
  *
@@ -1330,6 +1406,14 @@
  *   nfa_advance_var               execRPR.c             VAR handling
  *   nfa_add_state_unique          execRPR.c             Deduplication
  *   nfa_states_covered            execRPR.c             Absorption check
+ *   nfa_reevaluate_dependent_vars execRPR.c             Per-context re-eval
+ *   ExecRPRGetHeadContext         execRPR.c             Context lookup
+ *   ExecRPRFreeContext            execRPR.c             Context deallocation
+ *   ExecRPRCleanupDeadContexts    execRPR.c             Dead context cleanup
+ *   ExecRPRFinalizeAllContexts    execRPR.c             Partition-end finalize
+ *   ExecRPRRecordContextSuccess   execRPR.c             Stats: match success
+ *   ExecRPRRecordContextFailure   execRPR.c             Stats: match failure
+ *   compute_nav_offsets           createplan.c          Trim offset computation
  *
  * Appendix B. Data Structure Relationship Diagram
  * ============================================================================
@@ -2989,6 +3073,56 @@ ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
 	}
 }
 
+/*
  * ExecRPRProcessRow
@@ -3003,6 +3137,7 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 
 	/* Allow query cancellation once per row for simple/low-state patterns */
 	CHECK_FOR_INTERRUPTS();
@@ -3029,6 +3164,13 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 			}
 		}
 
+		/*
 		nfa_match(winstate, ctx, varMatched);
index 9787ef7756f..cdbe356abd7 100644
@@ -34,6 +34,7 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "common/int.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_aggregate.h"
 #include "catalog/pg_collation_d.h"
@@ -245,8 +246,8 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
 static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
 
 /* Forward declarations - navigation offset evaluation */
-static bool collect_prev_offset_walker(Node *node, List **offsets);
 static void eval_nav_max_offset(WindowAggState *winstate, List *defineClause);
+static void eval_nav_first_offset(WindowAggState *winstate, List *defineClause);
 
 /*
  * Not null info bit array consists of 2-bit items
@@ -932,28 +933,7 @@ eval_windowaggregates(WindowAggState *winstate)
 	 * head, so that tuplestore can discard unnecessary rows.
 	 */
 	if (agg_winobj->markptr >= 0)
-	{
-		int64		markpos = winstate->frameheadpos;
-
-		if (rpr_is_defined(winstate))
-		{
-			/*
-			 * If RPR is used, PREV may need to look at rows before the frame
-			 * head.  Adjust mark by navMaxOffset if known, otherwise retain
-			 * from position 0.
-			 */
-			if (winstate->navMaxOffset >= 0)
-			{
-				if (markpos > winstate->navMaxOffset)
-					markpos -= winstate->navMaxOffset;
-				else
-					markpos = 0;
-			}
-			else
-				markpos = 0;
-		}
-		WinSetMarkPosition(agg_winobj, markpos);
-	}
+		WinSetMarkPosition(agg_winobj, winstate->frameheadpos);
 
 	/*
 	 * Now restart the aggregates that require it.
@@ -1279,15 +1259,15 @@ prepare_tuplestore(WindowAggState *winstate)
 	if (winstate->nav_winobj)
 	{
 		/*
-		 * Allocate mark and read pointers for PREV/NEXT navigation.
+		 * Allocate mark and read pointers for RPR navigation.
 		 *
-		 * If navMaxOffset >= 0, we advance the mark to (currentpos -
-		 * navMaxOffset) as rows are processed, allowing tuplestore_trim() to
-		 * free rows that are no longer reachable.
+		 * If navMaxOffsetKind == RPR_NAV_OFFSET_FIXED, we advance the mark
+		 * based on (currentpos - navMaxOffset) and optionally
+		 * (nfaContext->matchStartRow + navFirstOffset), allowing
+		 * tuplestore_trim() to free rows that are no longer reachable.
 		 *
-		 * If navMaxOffset < 0 (RPR_NAV_OFFSET_NEEDS_EVAL or
-		 * RPR_NAV_OFFSET_RETAIN_ALL), the mark stays at 0, retaining the
-		 * entire partition in the tuplestore.
+		 * RPR_NAV_OFFSET_NEEDS_EVAL is resolved at executor init; by this
+		 * point it is either FIXED or RETAIN_ALL.
 		 */
 		winstate->nav_winobj->markptr =
 			tuplestore_alloc_read_pointer(winstate->buffer, 0);
@@ -2527,18 +2507,40 @@ ExecWindowAgg(PlanState *pstate)
 
 		/*
 		 * Advance RPR navigation mark pointer if possible, so that
-		 * tuplestore_trim() can free rows no longer reachable by PREV.
+		 * tuplestore_trim() can free rows no longer reachable by navigation.
 		 */
 		if (winstate->nav_winobj &&
 			winstate->rpPattern != NULL &&
-			winstate->navMaxOffset >= 0)
+			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 (winstate->navFirstOffset > -winstate->nfaContext->matchStartRow)
+					firstreach = winstate->nfaContext->matchStartRow
+						+ winstate->navFirstOffset;
+				else
+					firstreach = 0;
+				if (firstreach < navmarkpos)
+					navmarkpos = firstreach;
+			}
+
 			if (navmarkpos > winstate->nav_winobj->markpos)
 				WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
 		}
@@ -2779,6 +2781,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->nav_null_slot = ExecStoreAllNullTuple(winstate->nav_null_slot);

 	/*
@@ -2988,10 +2991,20 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->rpSkipTo = node->rpSkipTo;
 	/* Set up row pattern recognition PATTERN clause (compiled NFA) */
 	winstate->rpPattern = node->rpPattern;
-	/* Set up max PREV offset for tuplestore trim */
+	/* Set up nav offsets for tuplestore trim */
+	winstate->navMaxOffsetKind = node->navMaxOffsetKind;
 	winstate->navMaxOffset = node->navMaxOffset;
-	if (winstate->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
+	if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
 		eval_nav_max_offset(winstate, node->defineClause);
+	winstate->hasFirstNav = node->hasFirstNav;
+	winstate->navFirstOffsetKind = node->navFirstOffsetKind;
+	winstate->navFirstOffset = node->navFirstOffset;
+	if (winstate->hasFirstNav &&
+		winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
+		eval_nav_first_offset(winstate, node->defineClause);
+
+	/* Copy match_start dependency bitmapset for per-context evaluation */
+	winstate->defineMatchStartDependent = bms_copy(node->defineMatchStartDependent);
 
 	/* Calculate NFA state size and allocate cycle detection bitmap */
 	if (node->rpPattern != NULL)
@@ -3903,84 +3916,254 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
 }
 
 /*
- * collect_prev_offset_walker
- *		Walk expression tree to collect PREV offset_arg expressions.
+ * 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).
+ */
+static int64
+eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
+					   int64 defaultOffset)
+{
+	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	ExprState  *estate;
+	Datum		val;
+	bool		isnull;
+	int64		offset;
+
+	if (offset_expr == NULL)
+		return defaultOffset;
+
+	estate = ExecInitExpr(offset_expr, (PlanState *) winstate);
+	val = ExecEvalExprSwitchContext(estate, econtext, &isnull);
+
+	if (isnull)
+		return 0;
+
+	offset = DatumGetInt64(val);
+	if (offset < 0)
+		return 0;
+
+	return offset;
+}
+
+typedef struct
+{
+	WindowAggState *winstate;
+	int64		maxOffset;
+	bool		overflow;		/* true if overflow detected */
+} EvalNavMaxContext;
+
+/*
+ * eval_nav_max_offset_walker
+ *		Walk expression tree evaluating backward-reach offsets at runtime.
+ *
+ * Handles simple PREV, LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
  */
 static bool
-collect_prev_offset_walker(Node *node, List **offsets)
+eval_nav_max_offset_walker(Node *node, void *ctx)
 {
+	EvalNavMaxContext *context = (EvalNavMaxContext *) ctx;
+
 	if (node == NULL)
 		return false;
 
+	/* Short-circuit if overflow already detected */
+	if (context->overflow)
+		return false;
+
 	if (IsA(node, RPRNavExpr))
 	{
 		RPRNavExpr *nav = (RPRNavExpr *) node;
+		int64		reach = 0;
+
+		if (nav->kind == RPR_NAV_PREV)
+		{
+			reach = eval_nav_offset_helper(context->winstate,
+										   nav->offset_arg, 1);
+		}
+		else if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
+		{
+			reach = eval_nav_offset_helper(context->winstate,
+										   nav->offset_arg, 0);
+		}
+		else if (nav->kind == RPR_NAV_PREV_LAST ||
+				 nav->kind == RPR_NAV_NEXT_LAST)
+		{
+			int64		inner = eval_nav_offset_helper(context->winstate,
+													   nav->offset_arg, 0);
+			int64		outer = eval_nav_offset_helper(context->winstate,
+													   nav->compound_offset_arg, 1);
 
-		if (nav->kind == RPR_NAV_PREV && nav->offset_arg != NULL)
-			*offsets = lappend(*offsets, nav->offset_arg);
+			if (nav->kind == RPR_NAV_PREV_LAST)
+			{
+				if (pg_add_s64_overflow(inner, outer, &reach))
+				{
+					context->overflow = true;
+					return false;
+				}
+			}
+			else
+				reach = (inner > outer) ? inner - outer : 0;
+		}
 
-		/* Don't walk into RPRNavExpr children */
-		return false;
+		if (reach > context->maxOffset)
+			context->maxOffset = reach;
+
+		return false;			/* don't walk into children */
 	}
 
-	return expression_tree_walker(node, collect_prev_offset_walker, offsets);
+	return expression_tree_walker(node, eval_nav_max_offset_walker, ctx);
 }
 
 /*
  * eval_nav_max_offset
- *		Evaluate non-constant PREV offsets at executor init time.
+ *		Evaluate non-constant backward-reach offsets at executor init time.
+ *
+ * Called when the planner set navMaxOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
+ * because some offset in PREV, LAST-with-offset, or compound PREV_LAST/
+ * NEXT_LAST contains a parameter or non-foldable expression.
  *
- * Called when the planner set navMaxOffset to RPR_NAV_OFFSET_NEEDS_EVAL
- * because some PREV offset contains a parameter or non-foldable expression.
- * Walks the original defineClause expression trees, compiles and evaluates
- * each PREV offset_arg, and stores the maximum in winstate->navMaxOffset.
+ * On overflow, sets navMaxOffsetKind to RPR_NAV_OFFSET_RETAIN_ALL so that
+ * tuplestore trim is disabled for backward navigation.
  */
 static void
 eval_nav_max_offset(WindowAggState *winstate, List *defineClause)
 {
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
-	List	   *offsets = NIL;
+	EvalNavMaxContext ctx;
 	ListCell   *lc;
-	int64		maxOffset = 0;
 
-	/* Collect all PREV offset expressions from DEFINE clause */
+	ctx.winstate = winstate;
+	ctx.maxOffset = 0;
+	ctx.overflow = false;
+
 	foreach(lc, defineClause)
 	{
 		TargetEntry *te = (TargetEntry *) lfirst(lc);
 
-		collect_prev_offset_walker((Node *) te->expr, &offsets);
+		eval_nav_max_offset_walker((Node *) te->expr, &ctx);
 	}
 
-	/* Evaluate each offset and find maximum */
-	foreach(lc, offsets)
+	if (ctx.overflow)
 	{
-		Expr	   *offset_expr = (Expr *) lfirst(lc);
-		ExprState  *estate;
-		Datum		val;
-		bool		isnull;
-		int64		offset;
+		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
+		winstate->navMaxOffset = 0;
+	}
+	else
+	{
+		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+		winstate->navMaxOffset = ctx.maxOffset;
+	}
+}
 
-		estate = ExecInitExpr(offset_expr, (PlanState *) winstate);
-		val = ExecEvalExprSwitchContext(estate, econtext, &isnull);
+typedef struct
+{
+	WindowAggState *winstate;
+	int64		minOffset;
+	bool		found;
+} EvalNavFirstContext;
 
-		/*
-		 * NULL or negative offsets will cause a runtime error when PREV is
-		 * actually evaluated.  For trim purposes, treat them as 0.
-		 */
-		if (isnull)
-			continue;
+/*
+ * eval_nav_first_offset_walker
+ *		Walk expression tree evaluating forward-from-match_start offsets.
+ *
+ * Handles simple FIRST and compound PREV_FIRST/NEXT_FIRST.
+ */
+static bool
+eval_nav_first_offset_walker(Node *node, void *ctx)
+{
+	EvalNavFirstContext *context = (EvalNavFirstContext *) ctx;
 
-		offset = DatumGetInt64(val);
-		if (offset < 0)
-			continue;
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		RPRNavExpr *nav = (RPRNavExpr *) node;
+		int64		combined = INT64_MAX;
 
-		if (offset > maxOffset)
-			maxOffset = offset;
+		if (nav->kind == RPR_NAV_FIRST)
+		{
+			context->found = true;
+			combined = eval_nav_offset_helper(context->winstate,
+											  nav->offset_arg, 0);
+		}
+		else if (nav->kind == RPR_NAV_PREV_FIRST ||
+				 nav->kind == RPR_NAV_NEXT_FIRST)
+		{
+			int64		inner = eval_nav_offset_helper(context->winstate,
+													   nav->offset_arg, 0);
+			int64		outer = eval_nav_offset_helper(context->winstate,
+													   nav->compound_offset_arg, 1);
+
+			context->found = true;
+			if (nav->kind == RPR_NAV_PREV_FIRST)
+			{
+				/*
+				 * combined = inner - outer.  Both are non-negative, so the
+				 * result >= -INT64_MAX, which cannot underflow int64.
+				 */
+				combined = inner - outer;
+			}
+			else
+			{
+				/*
+				 * NEXT_FIRST: combined = inner + outer.  This can overflow,
+				 * but the result is always >= 0, so it never updates
+				 * minOffset (which tracks the minimum).  Clamp to INT64_MAX
+				 * on overflow.
+				 */
+				if (pg_add_s64_overflow(inner, outer, &combined))
+					combined = INT64_MAX;
+			}
+		}
+
+		if (combined < context->minOffset)
+			context->minOffset = combined;
+
+		return false;
 	}
 
-	winstate->navMaxOffset = maxOffset;
+	return expression_tree_walker(node, eval_nav_first_offset_walker, ctx);
+}
+
+/*
+ * eval_nav_first_offset
+ *		Evaluate non-constant forward-from-match_start offsets at executor
+ *		init time.
+ *
+ * Called when the planner set navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
+ * because some offset in FIRST or compound PREV_FIRST/NEXT_FIRST contains
+ * a parameter or non-foldable expression.
+ */
+static void
+eval_nav_first_offset(WindowAggState *winstate, List *defineClause)
+{
+	EvalNavFirstContext ctx;
+	ListCell   *lc;
+
+	ctx.winstate = winstate;
+	ctx.minOffset = INT64_MAX;
+	ctx.found = false;
+
+	foreach(lc, defineClause)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
 
-	list_free(offsets);
+		eval_nav_first_offset_walker((Node *) te->expr, &ctx);
+	}
+
+	if (ctx.found && ctx.minOffset < INT64_MAX)
+	{
+		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
+		winstate->navFirstOffset = ctx.minOffset;
+	}
+	else
+	{
+		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
+		winstate->navFirstOffset = 0;
+	}
 }
 
 /*
@@ -4210,8 +4393,14 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 
 		/*
 
 		/* No more rows in partition? Finalize all contexts */
@@ -4299,8 +4488,8 @@ register_result:
  * varMatched[i] = true if variable i matched at current row.
  *
  * Uses 1-slot model: only ecxt_outertuple is set to the current row.
- * PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
- * during expression evaluation, which temporarily swap the slot.
+ * PREV/NEXT/FIRST/LAST navigation is handled by EEOP_RPR_NAV_SET/RESTORE
+ * opcodes during expression evaluation, which temporarily swap the slot.
  */
 static bool
 nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index d2f19584070..66d37b78898 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2207,6 +2207,8 @@ expression_tree_walker_impl(Node *node,
 					return true;
 				if (expr->offset_arg && WALK(expr->offset_arg))
 					return true;
+				if (expr->compound_offset_arg && WALK(expr->compound_offset_arg))
+					return true;
 			}
 			break;
 		case T_SubscriptingRef:
@@ -3146,6 +3148,7 @@ expression_tree_mutator_impl(Node *node,
 				FLATCOPY(newnode, nav, RPRNavExpr);
 				MUTATE(newnode->arg, nav->arg, Expr *);
 				MUTATE(newnode->offset_arg, nav->offset_arg, Expr *);
+				MUTATE(newnode->compound_offset_arg, nav->compound_offset_arg, Expr *);
 				return (Node *) newnode;
 			}
 			break;
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 8ee3ccf6d0d..02d511269ab 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -16,6 +16,7 @@
  */
 #include "postgres.h"
 
+#include "common/int.h"
 #include "access/sysattr.h"
 #include "access/transam.h"
 #include "catalog/pg_class.h"
@@ -292,6 +293,7 @@ static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 List *runCondition, RPSkipTo rpSkipTo,
 								 RPRPattern *compiledPattern,
 								 List *defineClause,
+								 Bitmapset *defineMatchStartDependent,
 								 List *qual, bool topWindow,
 								 Plan *lefttree);
 static Group *make_group(List *tlist, List *qual, int numGroupCols,
@@ -2462,19 +2464,72 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 }
 
 /*
- * nav_max_offset_walker
- *		Walk expression tree to find the maximum PREV offset.
+ * NavOffsetContext - context for compute_nav_offsets walker.
  *
- * Only PREV is relevant for tuplestore trim since it looks backward;
- * NEXT looks forward and never references already-trimmed rows.
+ * Collects both backward reach (PREV, LAST-with-offset, compound
+ * PREV_LAST/NEXT_LAST) and forward-from-match-start reach (FIRST,
+ * compound PREV_FIRST/NEXT_FIRST) in a single tree walk.
+ */
+typedef struct NavOffsetContext
+{
+	int64		maxOffset;		/* max PREV/LAST backward offset (>= 0) */
+	bool		maxNeedsEval;	/* non-constant PREV/LAST offset found */
+	bool		maxOverflow;	/* constant offset overflow detected */
+	int64		firstOffset;	/* min FIRST offset (>= 0), or -1 if none */
+	bool		hasFirst;		/* any FIRST node found */
+	bool		firstNeedsEval; /* non-constant FIRST offset found */
+} NavOffsetContext;
+
+/*
+ * Helper: extract constant offset from an expression, handling NULL/negative.
+ * If expr is NULL, returns defaultOffset.
+ * Returns true if constant, false if non-constant (Param, cast, etc.).
+ */
+static bool
+extract_const_offset(Expr *expr, int64 defaultOffset, int64 *result)
+{
+	if (expr == NULL)
+	{
+		*result = defaultOffset;
+		return true;
+	}
+
+	if (IsA(expr, Const))
+	{
+		Const	   *c = (Const *) expr;
+
+		if (c->constisnull)
+			*result = 0;		/* runtime error; safe placeholder */
+		else
+		{
+			*result = DatumGetInt64(c->constvalue);
+			if (*result < 0)
+				*result = 0;	/* runtime error; safe placeholder */
+		}
+		return true;
+	}
+
+	return false;				/* non-constant */
+}
+
+/*
+ * nav_offset_walker
+ *		Expression tree walker for compute_nav_offsets.
+ *
+ * For each RPRNavExpr found, extract its constant offset(s) and update the
+ * NavOffsetContext with the maximum backward reach (maxOffset) and minimum
+ * forward reach (firstOffset).  Handles simple navigation (PREV, NEXT,
+ * FIRST, LAST) and compound forms (PREV_FIRST, NEXT_FIRST, PREV_LAST,
+ * NEXT_LAST) by combining inner and outer offsets.
  *
- * Returns true (to stop walking) if a non-constant PREV offset is found,
- * in which case *maxOffset is set to -1.  Otherwise accumulates the
- * maximum constant offset value.
+ * Non-constant offsets set maxNeedsEval or firstNeedsEval.  Overflow sets
+ * maxOverflow or firstOverflow for RETAIN_ALL fallback.
  */
 static bool
-nav_max_offset_walker(Node *node, int64 *maxOffset)
+nav_offset_walker(Node *node, void *ctx)
 {
+	NavOffsetContext *context = (NavOffsetContext *) ctx;
+
 	if (node == NULL)
 		return false;
 
@@ -2482,81 +2537,294 @@ nav_max_offset_walker(Node *node, int64 *maxOffset)
 	{
 		RPRNavExpr *nav = (RPRNavExpr *) node;
 
-		/* Only PREV looks backward; NEXT is irrelevant for trim */
-		if (nav->kind == RPR_NAV_PREV)
+		/*
+		 * Simple PREV(v, N) and LAST(v, N): backward reach from currentpos.
+		 * LAST without offset = currentpos, no backward reach. NEXT: forward
+		 * only, irrelevant for trim.
+		 */
+		if (nav->kind == RPR_NAV_PREV ||
+			(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
 		{
-			int64		offset;
-
-			if (nav->offset_arg == NULL)
+			if (!context->maxNeedsEval)
 			{
-				/* 1-arg form: implicit offset of 1 */
-				offset = 1;
+				int64		offset;
+
+				if (extract_const_offset(nav->offset_arg, 1, &offset))
+				{
+					if (offset > context->maxOffset)
+						context->maxOffset = offset;
+				}
+				else
+					context->maxNeedsEval = true;
 			}
-			else if (IsA(nav->offset_arg, Const))
+		}
+
+		/*
+		 * Simple FIRST(v, N): forward reach from match_start. Smaller N means
+		 * older rows needed.
+		 */
+		if (nav->kind == RPR_NAV_FIRST)
+		{
+			context->hasFirst = true;
+
+			if (!context->firstNeedsEval)
 			{
-				Const	   *c = (Const *) nav->offset_arg;
+				int64		offset;
 
-				if (c->constisnull)
+				if (extract_const_offset(nav->offset_arg, 0, &offset))
 				{
-					/*
-					 * NULL offset causes a runtime error, so this path is
-					 * never actually reached during execution.  Use 0 as a
-					 * safe placeholder for planning purposes.
-					 */
-					offset = 0;
+					if (offset < context->firstOffset)
+						context->firstOffset = offset;
 				}
 				else
+					context->firstNeedsEval = true;
+			}
+		}
+
+		/*
+		 * Compound PREV_LAST / NEXT_LAST: base = currentpos. PREV_LAST(v, N,
+		 * M): target = currentpos - N - M → lookback = N + M NEXT_LAST(v,
+		 * N, M): target = currentpos - N + M → lookback = max(N - M, 0)
+		 */
+		if (nav->kind == RPR_NAV_PREV_LAST ||
+			nav->kind == RPR_NAV_NEXT_LAST)
+		{
+			if (!context->maxNeedsEval)
+			{
+				int64		inner,
+							outer,
+							combined;
+
+				if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+					extract_const_offset(nav->compound_offset_arg, 1, &outer))
 				{
-					offset = DatumGetInt64(c->constvalue);
-					if (offset < 0)
-						offset = 0; /* negative offset causes runtime error */
+					if (nav->kind == RPR_NAV_PREV_LAST)
+					{
+						if (pg_add_s64_overflow(inner, outer, &combined))
+						{
+							context->maxOverflow = true;
+							return false;
+						}
+					}
+					else
+						combined = (inner > outer) ? inner - outer : 0;
+
+					if (combined > context->maxOffset)
+						context->maxOffset = combined;
 				}
+				else
+					context->maxNeedsEval = true;
 			}
-			else
+		}
+
+		/*
+		 * Compound PREV_FIRST / NEXT_FIRST: base = match_start. PREV_FIRST(v,
+		 * N, M): target = match_start + N - M NEXT_FIRST(v, N, M): target =
+		 * match_start + N + M The combined offset (N±M) from match_start can
+		 * be negative, meaning rows before match_start are needed.
+		 */
+		if (nav->kind == RPR_NAV_PREV_FIRST ||
+			nav->kind == RPR_NAV_NEXT_FIRST)
+		{
+			context->hasFirst = true;
+
+			if (!context->firstNeedsEval)
 			{
-				/*
-				 * Non-constant offset (Param, stable function, etc.). The
-				 * parser guarantees offset is a runtime constant, so it can
-				 * be evaluated at executor init time.
-				 */
-				*maxOffset = RPR_NAV_OFFSET_NEEDS_EVAL;
-				return true;	/* stop walking */
+				int64		inner,
+							outer,
+							combined;
+
+				if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+					extract_const_offset(nav->compound_offset_arg, 1, &outer))
+				{
+					if (nav->kind == RPR_NAV_PREV_FIRST)
+					{
+						/*
+						 * combined = inner - outer.  Both are non-negative,
+						 * so the result >= -INT64_MAX, which cannot underflow
+						 * int64.  No overflow check needed.
+						 */
+						combined = inner - outer;
+					}
+					else
+					{
+						/*
+						 * NEXT_FIRST: combined = inner + outer.  This can
+						 * overflow, but the result is always >= 0, so it
+						 * never updates firstOffset (which tracks the
+						 * minimum).  Clamp to INT64_MAX on overflow.
+						 */
+						if (pg_add_s64_overflow(inner, outer, &combined))
+							combined = INT64_MAX;
+					}
+
+					if (combined < context->firstOffset)
+						context->firstOffset = combined;
+				}
+				else
+					context->firstNeedsEval = true;
 			}
+		}
 
-			if (offset > *maxOffset)
-				*maxOffset = offset;
+		/* Don't walk into RPRNavExpr children */
+		return false;
+	}
+
+	return expression_tree_walker(node, nav_offset_walker, ctx);
+}
+
+/*
+ * compute_nav_offsets
+ *		Compute navigation offsets for tuplestore trim in a single pass.
+ *
+ * Walks all DEFINE clause expressions once, computing:
+ *   - maxOffset: max backward reach from PREV, LAST-with-offset,
+ *     compound PREV_LAST/NEXT_LAST
+ *   - hasFirst/firstOffset: min forward-from-match-start reach from
+ *     FIRST, compound PREV_FIRST/NEXT_FIRST
+ */
+static void
+compute_nav_offsets(List *defineClause,
+					RPRNavOffsetKind *maxKind, int64 *maxResult,
+					bool *hasFirst,
+					RPRNavOffsetKind *firstKind, int64 *firstResult)
+{
+	NavOffsetContext ctx;
+	ListCell   *lc;
+
+	ctx.maxOffset = 0;
+	ctx.maxNeedsEval = false;
+	ctx.maxOverflow = false;
+	ctx.firstOffset = INT64_MAX;	/* sentinel: no FIRST found yet */
+	ctx.hasFirst = false;
+	ctx.firstNeedsEval = false;
+
+	foreach(lc, defineClause)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+		nav_offset_walker((Node *) te->expr, &ctx);
+	}
+
+	/* Max backward offset */
+	if (ctx.maxOverflow)
+	{
+		*maxKind = RPR_NAV_OFFSET_RETAIN_ALL;
+		*maxResult = 0;
+	}
+	else if (ctx.maxNeedsEval)
+	{
+		*maxKind = RPR_NAV_OFFSET_NEEDS_EVAL;
+		*maxResult = 0;
+	}
+	else
+	{
+		*maxKind = RPR_NAV_OFFSET_FIXED;
+		*maxResult = ctx.maxOffset;
+	}
+
+	/* First offset (can be negative for compound PREV_FIRST) */
+	*hasFirst = ctx.hasFirst;
+	if (ctx.hasFirst)
+	{
+		if (ctx.firstNeedsEval)
+		{
+			*firstKind = RPR_NAV_OFFSET_NEEDS_EVAL;
+			*firstResult = 0;
+		}
+		else if (ctx.firstOffset == INT64_MAX)
+		{
+			*firstKind = RPR_NAV_OFFSET_FIXED;
+			*firstResult = 0;	/* only implicit FIRST(v) */
+		}
+		else
+		{
+			*firstKind = RPR_NAV_OFFSET_FIXED;
+			*firstResult = ctx.firstOffset; /* may be negative */
 		}
+	}
+	else
+	{
+		*firstKind = RPR_NAV_OFFSET_FIXED;
+		*firstResult = 0;
+	}
+}
 
-		/* Don't walk into RPRNavExpr children - offset_arg already handled */
+/*
+ * has_match_start_dependency
+ *		Check if an expression tree contains navigation that depends on
+ *		match_start: FIRST, LAST-with-offset, or compound PREV_FIRST/
+ *		NEXT_FIRST/PREV_LAST/NEXT_LAST with offset.  Such expressions
+ *		require per-context re-evaluation during NFA processing.
+ *
+ * LAST without offset always resolves to currentpos and is
+ * match_start-independent.
+ */
+static bool
+has_match_start_dependency(Node *node, void *context)
+{
+	if (node == NULL)
 		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		RPRNavExpr *nav = (RPRNavExpr *) node;
+
+		if (nav->kind == RPR_NAV_FIRST)
+			return true;
+		if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
+			return true;
+
+		/* Compound kinds with FIRST base depend on match_start */
+		if (nav->kind == RPR_NAV_PREV_FIRST ||
+			nav->kind == RPR_NAV_NEXT_FIRST)
+			return true;
+
+		/*
+		 * PREV_LAST/NEXT_LAST: inner is LAST, which uses currentpos.
+		 * match_start-dependent only if inner has offset (clamped to
+		 * match_start).
+		 */
+		if ((nav->kind == RPR_NAV_PREV_LAST ||
+			 nav->kind == RPR_NAV_NEXT_LAST) &&
+			nav->offset_arg != NULL)
+			return true;
+
+		/* Check children (arg may contain further nav expressions) */
+		return has_match_start_dependency((Node *) nav->arg, context);
 	}
 
-	return expression_tree_walker(node, nav_max_offset_walker, maxOffset);
+	return expression_tree_walker(node, has_match_start_dependency, NULL);
 }
 
 /*
- * compute_nav_max_offset
- *		Compute the maximum PREV offset from DEFINE clause expressions.
+ * compute_match_start_dependent
+ *		Build a Bitmapset of DEFINE variable indices whose expressions
+ *		depend on match_start (contain FIRST, LAST-with-offset, or
+ *		compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST with offset).
  *
- * Returns the maximum constant offset found, or -1 if any PREV offset
- * cannot be determined statically.  NEXT offsets are ignored since they
- * look forward and don't affect tuplestore trim.
+ * Variables in this set require per-context re-evaluation during NFA
+ * processing, because different contexts may have different match_start
+ * values.
  */
-static int64
-compute_nav_max_offset(List *defineClause)
+static Bitmapset *
+compute_match_start_dependent(List *defineClause)
 {
-	int64		maxOffset = 0;
+	Bitmapset  *result = NULL;
 	ListCell   *lc;
+	int			varIdx = 0;

-		if (nav_max_offset_walker((Node *) te->expr, &maxOffset))
-			return maxOffset;	/* NEEDS_EVAL or RETAIN_ALL */
+		if (has_match_start_dependency((Node *) te->expr, NULL))
+			result = bms_add_member(result, varIdx);
+
+		varIdx++;
 	}
 
-	return maxOffset;
+	return result;
 }
 
 /*
@@ -2586,6 +2854,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)

 	/*
@@ -2648,11 +2917,15 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		buildDefineVariableList(wc->defineClause, &defineVariableList);
 		filteredDefineClause = wc->defineClause;

 	/* And finally we can make the WindowAgg node */
@@ -2670,6 +2943,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  wc->rpSkipTo,
 						  compiledPattern,
 						  filteredDefineClause,
+						  matchStartDependent,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -6742,6 +7016,7 @@ make_windowagg(List *tlist, WindowClause *wc,
 			   List *runCondition, RPSkipTo rpSkipTo,
 			   RPRPattern *compiledPattern,
 			   List *defineClause,
+			   Bitmapset *defineMatchStartDependent,
 			   List *qual, bool topWindow, Plan *lefttree)
 {
 	WindowAgg  *node = makeNode(WindowAgg);
@@ -6776,8 +7051,14 @@ make_windowagg(List *tlist, WindowClause *wc,

-	/* Compute max PREV offset for tuplestore trim optimization */
-	node->navMaxOffset = compute_nav_max_offset(defineClause);
+	/* Store pre-computed match_start dependency bitmapset */
+	node->defineMatchStartDependent = defineMatchStartDependent;
+
+	/* Compute nav offsets for tuplestore trim optimization */
+	compute_nav_offsets(defineClause,
+						&node->navMaxOffsetKind, &node->navMaxOffset,
+						&node->hasFirstNav,
+						&node->navFirstOffsetKind, &node->navFirstOffset);

 

index d1e02e52e53..05070cb04bb 100644
@@ -429,14 +429,21 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
-	bool		has_nav;		/* RPRNavExpr found (nesting) */
+	int			nav_count;		/* number of RPRNavExpr nodes found */
-}			NavCheckResult;
+	RPRNavKind	inner_kind;		/* kind of first (outermost) nested RPRNavExpr */
+} NavCheckResult;
 
 static bool
 nav_check_walker(Node *node, void *context)
@@ -446,7 +453,11 @@ nav_check_walker(Node *node, void *context)
 	if (node == NULL)
 		return false;
 	if (IsA(node, RPRNavExpr))
-		result->has_nav = true;
+	{
+		if (result->nav_count == 0)
+			result->inner_kind = ((RPRNavExpr *) node)->kind;
+		result->nav_count++;
+	}
 	if (IsA(node, Var))
 		result->has_column_ref = true;
 
@@ -457,16 +468,93 @@ static void
 check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
 {
 	NavCheckResult result;
+	bool		outer_is_physical = (nav->kind == RPR_NAV_PREV ||
+									 nav->kind == RPR_NAV_NEXT);

-	if (result.has_nav)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("PREV and NEXT cannot be nested"),
-				 parser_errposition(pstate, nav->location)));
+	if (result.nav_count > 0)
+	{
+		bool		inner_is_physical = (result.inner_kind == RPR_NAV_PREV ||
+										 result.inner_kind == RPR_NAV_NEXT);
+
+		if (outer_is_physical && !inner_is_physical)
+		{
+			/*
+			 * PREV/NEXT wrapping FIRST/LAST: compound navigation per SQL
+			 * standard 5.6.4.  Flatten the nested RPRNavExpr into a single
+			 * compound node.  The inner RPRNavExpr must be the direct arg of
+			 * the outer; expressions like PREV(val + FIRST(v)) are not valid
+			 * compound navigation.
+			 */
+			RPRNavExpr *inner;
+
+			/* Reject triple-or-deeper nesting (e.g. PREV(FIRST(PREV(x)))) */
+			if (result.nav_count > 1)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("row pattern navigation cannot be nested more than two levels deep"),
+						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+						 parser_errposition(pstate, nav->location)));
+
+			if (!IsA(nav->arg, RPRNavExpr))
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+						 parser_errposition(pstate, nav->location)));
+			inner = (RPRNavExpr *) nav->arg;
+
+			/* Determine compound kind */
+			if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
+				nav->kind = RPR_NAV_PREV_FIRST;
+			else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
+				nav->kind = RPR_NAV_PREV_LAST;
+			else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
+				nav->kind = RPR_NAV_NEXT_FIRST;
+			else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
+				nav->kind = RPR_NAV_NEXT_LAST;
+
+			/* Move outer offset to compound_offset_arg */
+			nav->compound_offset_arg = nav->offset_arg;
+
+			/* Move inner offset and arg up */
+			nav->offset_arg = inner->offset_arg;
+			nav->arg = inner->arg;
+
+			/* No further nesting check needed - already validated */
+			return;
+		}
+		else if (!outer_is_physical && inner_is_physical)
+		{
+			/* FIRST/LAST wrapping PREV/NEXT: prohibited by standard */
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+					 parser_errposition(pstate, nav->location)));
+		}
+		else if (outer_is_physical && inner_is_physical)
+		{
+			/* PREV/NEXT wrapping PREV/NEXT: prohibited */
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+					 parser_errposition(pstate, nav->location)));
+		}
+		else
+		{
+			/* FIRST/LAST wrapping FIRST/LAST: prohibited */
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+					 parser_errposition(pstate, nav->location)));
+		}
+	}
 	if (!result.has_column_ref)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -483,7 +571,7 @@ check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
 			contain_volatile_functions((Node *) nav->offset_arg))
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("PREV/NEXT offset must be a run-time constant"),
+					 errmsg("row pattern navigation offset must be a run-time constant"),
 					 parser_errposition(pstate, nav->location)));
 	}
 }
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a4fe725646c..467736d9146 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10106,16 +10106,83 @@ get_rule_expr(Node *node, deparse_context *context,
 		case T_RPRNavExpr:
 			{
 				RPRNavExpr *nav = (RPRNavExpr *) node;
+				const char *outer_func = NULL;
+				const char *inner_func;
 
-				appendStringInfoString(buf,
-									   nav->kind == RPR_NAV_PREV ? "PREV(" : "NEXT(");
-				get_rule_expr((Node *) nav->arg, context, showimplicit);
-				if (nav->offset_arg != NULL)
+				switch (nav->kind)
 				{
-					appendStringInfoString(buf, ", ");
-					get_rule_expr((Node *) nav->offset_arg, context, showimplicit);
+					case RPR_NAV_PREV:
+						inner_func = "PREV(";
+						break;
+					case RPR_NAV_NEXT:
+						inner_func = "NEXT(";
+						break;
+					case RPR_NAV_FIRST:
+						inner_func = "FIRST(";
+						break;
+					case RPR_NAV_LAST:
+						inner_func = "LAST(";
+						break;
+					case RPR_NAV_PREV_FIRST:
+						outer_func = "PREV(";
+						inner_func = "FIRST(";
+						break;
+					case RPR_NAV_PREV_LAST:
+						outer_func = "PREV(";
+						inner_func = "LAST(";
+						break;
+					case RPR_NAV_NEXT_FIRST:
+						outer_func = "NEXT(";
+						inner_func = "FIRST(";
+						break;
+					case RPR_NAV_NEXT_LAST:
+						outer_func = "NEXT(";
+						inner_func = "LAST(";
+						break;
+					default:
+						elog(ERROR, "unrecognized RPR navigation kind: %d",
+							 nav->kind);
+						inner_func = NULL;	/* keep compiler quiet */
+						break;
+				}
+
+				if (outer_func != NULL)
+				{
+					/*
+					 * Compound: PREV(FIRST(arg [, inner_offset]) [,
+					 * outer_offset])
+					 */
+					appendStringInfoString(buf, outer_func);
+					appendStringInfoString(buf, inner_func);
+					get_rule_expr((Node *) nav->arg, context, showimplicit);
+					if (nav->offset_arg != NULL)
+					{
+						appendStringInfoString(buf, ", ");
+						get_rule_expr((Node *) nav->offset_arg, context,
+									  showimplicit);
+					}
+					appendStringInfoChar(buf, ')');
+					if (nav->compound_offset_arg != NULL)
+					{
+						appendStringInfoString(buf, ", ");
+						get_rule_expr((Node *) nav->compound_offset_arg,
+									  context, showimplicit);
+					}
+					appendStringInfoChar(buf, ')');
+				}
+				else
+				{
+					/* Simple: FUNC(arg [, offset]) */
+					appendStringInfoString(buf, inner_func);
+					get_rule_expr((Node *) nav->arg, context, showimplicit);
+					if (nav->offset_arg != NULL)
+					{
+						appendStringInfoString(buf, ", ");
+						get_rule_expr((Node *) nav->offset_arg, context,
+									  showimplicit);
+					}
+					appendStringInfoChar(buf, ')');
 				}
-				appendStringInfoChar(buf, ')');
 			}
 			break;

 # functions for range types
 { oid => '3832', descr => 'I/O',
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index fac37c96896..834800a4062 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -699,10 +699,10 @@ typedef struct ExprEvalStep
 		struct
 		{
 			WindowAggState *winstate;
-			RPRNavKind	kind;	/* PREV or NEXT */
-			Datum	   *offset_value;	/* 2-arg: runtime offset value, or
-										 * NULL */
-			bool	   *offset_isnull;	/* 2-arg: runtime offset null flag */
+			RPRNavKind	kind;	/* navigation kind (simple or compound) */
+			Datum	   *offset_value;	/* offset value(s), or NULL */
+			bool	   *offset_isnull;	/* offset null flag(s) */
+			/* For compound nav: offset_value[0] = inner, [1] = outer */
 		}			rpr_nav;
 
index ff6d7d70a60..602b72a6e0d 100644
@@ -2692,12 +2695,17 @@ typedef struct WindowAggState
 
 	/* RPR navigation */
-	int64		navMaxOffset;	/* max PREV offset; see RPR_NAV_OFFSET_* */
+	RPRNavOffsetKind navMaxOffsetKind;	/* status of navMaxOffset */
+	int64		navMaxOffset;	/* max backward nav offset (when FIXED) */
+	bool		hasFirstNav;	/* FIRST() present in DEFINE */
+	RPRNavOffsetKind navFirstOffsetKind;	/* status of navFirstOffset */
+	int64		navFirstOffset; /* min FIRST() offset (when FIXED) */
 	struct WindowObjectData *nav_winobj;	/* winobj for RPR nav fetch */
 	int64		nav_slot_pos;	/* position cached in nav_slot, or -1 */
-	TupleTableSlot *nav_slot;	/* slot for PREV/NEXT target row */
+	TupleTableSlot *nav_slot;	/* slot for PREV/NEXT/FIRST/LAST target row */
 	TupleTableSlot *nav_saved_outertuple;	/* saved slot during nav swap */
 	TupleTableSlot *nav_null_slot;	/* all NULL slot */
+	int64		nav_match_start;	/* match_start for FIRST/LAST nav */
 
 	/* RPR current match result */
 	bool		rpr_match_valid;	/* true if a match result is set */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b92663687a6..1fdf62575cb 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -586,9 +586,24 @@ typedef enum RPSkipTo
 	ST_NONE,					/* no AFTER MATCH clause; default for non-RPR
 								 * windows */
 	ST_NEXT_ROW,				/* SKIP TO NEXT ROW */
-	ST_PAST_LAST_ROW,			/* SKIP TO PAST LAST ROW */
+	ST_PAST_LAST_ROW			/* SKIP TO PAST LAST ROW */
 } RPSkipTo;
 
+/*
+ * RPRNavOffsetKind - status of navigation offset for tuplestore trim.
+ *
+ * The planner computes navMaxOffset/navFirstOffset for tuplestore mark
+ * optimization.  This enum tracks whether the value is a resolved constant,
+ * needs runtime evaluation, or cannot be determined (retain all rows).
+ */
+typedef enum RPRNavOffsetKind
+{
+	RPR_NAV_OFFSET_FIXED,		/* resolved constant; use the offset value */
+	RPR_NAV_OFFSET_NEEDS_EVAL,	/* non-constant offset; evaluate at executor
+								 * init */
+	RPR_NAV_OFFSET_RETAIN_ALL	/* cannot determine; retain all rows (no trim) */
+} RPRNavOffsetKind;
+
 /*
  * RPRPatternNodeType - Row Pattern Recognition pattern node types
  */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 27a2e7b48c7..93ce505f0d4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1387,14 +1387,34 @@ typedef struct WindowAgg
 	List	   *defineClause;
 
 	/*
-	 * Maximum PREV offset for tuplestore mark optimization. >= 0: statically
-	 * determined max offset (mark = currentpos - offset).
-	 * RPR_NAV_OFFSET_NEEDS_EVAL: has non-constant offset; evaluate at
-	 * executor init.  RPR_NAV_OFFSET_RETAIN_ALL: must retain entire partition
-	 * (no trim possible).
+	 * Bitmapset of DEFINE variable indices whose expressions depend on
+	 * match_start (contain FIRST, LAST-with-offset, or compound
+	 * PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST with offset).  Variables in
+	 * this set require per-context re-evaluation during NFA processing.
 	 */
+	Bitmapset  *defineMatchStartDependent;
+
+	/*
+	 * Navigation offset status and values for tuplestore mark optimization.
+	 * See RPRNavOffsetKind in nodes/parsenodes.h.
+	 *
+	 * navMaxOffset: maximum backward reach from currentpos (contributed by
+	 * PREV, LAST-with-offset, compound PREV_LAST/NEXT_LAST).  Only valid when
+	 * navMaxOffsetKind == RPR_NAV_OFFSET_FIXED.
+	 *
+	 * navFirstOffset: minimum forward offset from match_start (contributed by
+	 * FIRST, compound PREV_FIRST/NEXT_FIRST).  Can be negative for compound
+	 * PREV_FIRST.  Only valid when navFirstOffsetKind == RPR_NAV_OFFSET_FIXED
+	 * and hasFirstNav == true.
+	 */
+	RPRNavOffsetKind navMaxOffsetKind;
 	int64		navMaxOffset;
 
+	/* true if FIRST-based navigation (FIRST, PREV_FIRST, NEXT_FIRST) is used */
+	bool		hasFirstNav;
+	RPRNavOffsetKind navFirstOffsetKind;
+	int64		navFirstOffset;
+
 	/*
 	 * false for all apart from the WindowAgg that's closest to the root of
 	 * the plan
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 94723a3b909..0afcfacd5d2 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -651,27 +651,50 @@ typedef struct WindowFuncRunCondition
 /*
  * RPRNavExpr
  *
- * Represents a PREV() or NEXT() navigation call in an RPR DEFINE clause.
+ * Represents a PREV/NEXT/FIRST/LAST navigation call in an RPR DEFINE clause.
  * At expression compile time this is translated into EEOP_RPR_NAV_SET /
  * EEOP_RPR_NAV_RESTORE opcodes rather than a normal function call.
  *
- * kind:       RPR_NAV_PREV or RPR_NAV_NEXT
- * arg:        the expression to evaluate against the target row
- * offset_arg: optional explicit offset expression (2-arg form); NULL for
- *             the 1-arg form which uses an implicit offset of 1
+ * Simple navigation (PREV/NEXT/FIRST/LAST):
+ *   kind:       RPR_NAV_PREV, RPR_NAV_NEXT, RPR_NAV_FIRST, or RPR_NAV_LAST
+ *   arg:        the expression to evaluate against the target row
+ *   offset_arg: optional explicit offset expression (2-arg form); NULL for
+ *               the 1-arg form (implicit offset: 1 for PREV/NEXT, 0 for
+ *               FIRST/LAST)
+ *
+ * Compound navigation (PREV/NEXT wrapping FIRST/LAST):
+ *   kind:              RPR_NAV_PREV_FIRST, PREV_LAST, NEXT_FIRST, NEXT_LAST
+ *   arg:               the expression to evaluate against the final target row
+ *   offset_arg:        inner offset (FIRST/LAST), NULL = implicit default
+ *   compound_offset_arg: outer offset (PREV/NEXT), NULL = implicit default
+ *
+ * Compound target computation:
+ *   PREV_FIRST: (match_start + inner) - outer
+ *   NEXT_FIRST: (match_start + inner) + outer
+ *   PREV_LAST:  (currentpos  - inner) - outer
+ *   NEXT_LAST:  (currentpos  - inner) + outer
  */
 typedef enum RPRNavKind
 {
 	RPR_NAV_PREV,
 	RPR_NAV_NEXT,
+	RPR_NAV_FIRST,
+	RPR_NAV_LAST,
+	/* compound: outer(inner(arg)) */
+	RPR_NAV_PREV_FIRST,
+	RPR_NAV_PREV_LAST,
+	RPR_NAV_NEXT_FIRST,
+	RPR_NAV_NEXT_LAST
 } RPRNavKind;
 
 typedef struct RPRNavExpr
 {
 	Expr		xpr;
-	RPRNavKind	kind;			/* PREV or NEXT */
+	RPRNavKind	kind;			/* navigation kind */
 	Expr	   *arg;			/* argument expression */
-	Expr	   *offset_arg;		/* offset expression, or NULL for 1-arg form */
+	Expr	   *offset_arg;		/* offset expression, or NULL for default */
+	Expr	   *compound_offset_arg;	/* outer offset for compound nav, or
+										 * NULL if simple */
 	Oid			resulttype;		/* result type (same as arg's type) */
 	/* OID of collation of result */
 	Oid			resultcollid pg_node_attr(query_jumble_ignore);
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 00a28abe2b4..0a14cfad79b 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -55,19 +55,11 @@
 #define RPRElemIsFin(e)			((e)->varId == RPR_VARID_FIN)
 #define RPRElemCanSkip(e)		((e)->min == 0)
 
-/*
- * navMaxOffset sentinel values.
- * Non-negative values represent a statically determined maximum PREV offset.
- */
-#define RPR_NAV_OFFSET_NEEDS_EVAL	(-1)	/* has non-constant PREV offset;
-											 * evaluate at executor init */
-#define RPR_NAV_OFFSET_RETAIN_ALL	(-2)	/* must retain entire partition
-											 * (e.g., future FIRST/LAST) */
-
 extern List *collectPatternVariables(RPRPatternNode *pattern);
 extern void buildDefineVariableList(List *defineClause,
 									List **defineVariableList);
 extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
-								   RPSkipTo rpSkipTo, int frameOptions);
+								   RPSkipTo rpSkipTo, int frameOptions,
+								   bool hasMatchStartDependent);
 
 #endif							/* OPTIMIZER_RPR_H */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index c02dbd4c08d..04ec25d4cf5 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1040,9 +1040,10 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > PREV(PREV(price))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  PREV and NEXT cannot contain PREV or NEXT
 LINE 7:     DEFINE A AS price > PREV(PREV(price))
                                 ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
 -- Nested NEXT
 SELECT price FROM stock
 WINDOW w AS (
@@ -1052,9 +1053,10 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > NEXT(NEXT(price))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  PREV and NEXT cannot contain PREV or NEXT
 LINE 7:     DEFINE A AS price > NEXT(NEXT(price))
                                 ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
 -- PREV nested inside NEXT
 SELECT price FROM stock
 WINDOW w AS (
@@ -1064,9 +1066,10 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > NEXT(PREV(price))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  PREV and NEXT cannot contain PREV or NEXT
 LINE 7:     DEFINE A AS price > NEXT(PREV(price))
                                 ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
 -- PREV nested inside expression inside NEXT
 SELECT price FROM stock
 WINDOW w AS (
@@ -1076,9 +1079,10 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > NEXT(price * PREV(price))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  PREV and NEXT cannot contain PREV or NEXT
 LINE 7:     DEFINE A AS price > NEXT(price * PREV(price))
                                 ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
 -- Triple nesting: error reported at outermost PREV
 SELECT price FROM stock
 WINDOW w AS (
@@ -1088,9 +1092,10 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS price > PREV(PREV(PREV(price)))
 );
-ERROR:  PREV and NEXT cannot be nested
+ERROR:  PREV and NEXT cannot contain PREV or NEXT
 LINE 7:     DEFINE A AS price > PREV(PREV(PREV(price)))
                                 ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
 -- No column reference in PREV/NEXT argument
 -- PREV(1): constant only, no column reference
 SELECT price FROM stock
@@ -1137,7 +1142,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS PREV(price, price) > 0
 );
-ERROR:  PREV/NEXT offset must be a run-time constant
+ERROR:  row pattern navigation offset must be a run-time constant
 LINE 7:     DEFINE A AS PREV(price, price) > 0
                         ^
 -- Non-constant offset: volatile function as offset
@@ -1149,7 +1154,7 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS PREV(price, random()::int) > 0
 );
-ERROR:  PREV/NEXT offset must be a run-time constant
+ERROR:  row pattern navigation offset must be a run-time constant
 LINE 7:     DEFINE A AS PREV(price, random()::int) > 0
                         ^
 -- Non-constant offset: subquery as offset
@@ -1442,7 +1447,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price, -1) IS NOT NULL
 );
-ERROR:  PREV/NEXT offset must not be negative
+ERROR:  row pattern navigation offset must not be negative
 -- 2-arg PREV/NEXT: NULL offset (typed)
 SELECT company, tdate, price, first_value(price) OVER w
 FROM stock
@@ -1452,7 +1457,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price, NULL::int8) IS NOT NULL
 );
-ERROR:  PREV/NEXT offset must not be null
+ERROR:  row pattern navigation offset must not be null
 -- 2-arg PREV/NEXT: NULL offset (untyped)
 SELECT company, tdate, price, first_value(price) OVER w
 FROM stock
@@ -1462,7 +1467,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price, NULL) IS NOT NULL
 );
-ERROR:  PREV/NEXT offset must not be null
+ERROR:  row pattern navigation offset must not be null
 -- 2-arg PREV/NEXT: host variable negative and NULL
 PREPARE test_prev_offset(int8) AS
 SELECT company, tdate, price, first_value(price) OVER w
@@ -1474,9 +1479,9 @@ WINDOW w AS (
     DEFINE A AS price > PREV(price, $1)
 );
 EXECUTE test_prev_offset(-1);
-ERROR:  PREV/NEXT offset must not be negative
+ERROR:  row pattern navigation offset must not be negative
 EXECUTE test_prev_offset(NULL);
-ERROR:  PREV/NEXT offset must not be null
+ERROR:  row pattern navigation offset must not be null
 DEALLOCATE test_prev_offset;
 -- 2-arg PREV/NEXT: host variable with expression (0 + $1)
 PREPARE test_prev_offset(int8) AS
@@ -1489,9 +1494,9 @@ WINDOW w AS (
     DEFINE A AS price > PREV(price, 0 + $1)
 );
 EXECUTE test_prev_offset(-1);
-ERROR:  PREV/NEXT offset must not be negative
+ERROR:  row pattern navigation offset must not be negative
 EXECUTE test_prev_offset(NULL);
-ERROR:  PREV/NEXT offset must not be null
+ERROR:  row pattern navigation offset must not be null
 DEALLOCATE test_prev_offset;
 -- 2-arg PREV/NEXT: host variable with positive value
 -- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path
@@ -1630,6 +1635,604 @@ WINDOW w AS (
  company2 | 07-10-2023 |  1300 |             |            |     0
 (20 rows)
 
+--
+-- FIRST/LAST navigation
+--
+-- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val)
+-- at specific positions.
+CREATE TEMP TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10);
+-- FIRST(val) = constant: B matches when match_start has val=10
+-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 → match {1,2}
+-- match_start=3(30): A=id3, B=id4, FIRST(val)=30≠10 → no match
+-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 → match {4,5}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS FIRST(val) = 10
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  2
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |  4 |  5
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- LAST(val): always equals current row's val (offset 0 default)
+-- Equivalent to: B AS val > 15
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS LAST(val) > 15
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  2
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |  4 |  5
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where
+-- first and last rows have the same val.
+-- match_start=1(10): reluctant tries B early:
+--   id2(20≠10), id3(30≠10), id4(10=10) → match {1,2,3,4}
+-- match_start=5(50): id6(10≠50) → no match
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+? B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  4
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |    |   
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- Greedy A+ with FIRST(val) = LAST(val): find longest match where
+-- first and last rows have the same val.
+-- match_start=1(10): greedy A eats all, B tries last:
+--   id6(10=10) → match {1,2,3,4,5,6}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  6
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |    |   
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts.
+-- With ONE ROW PER MATCH, each row shows only its first match result.
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+? B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml 
+----+-----+----+----
+  1 |  10 |  1 |  4
+  2 |  20 |    |   
+  3 |  30 |    |   
+  4 |  10 |  4 |  6
+  5 |  50 |    |   
+  6 |  10 |    |   
+(6 rows)
+
+-- FIRST/LAST 2-arg offset form
+--
+-- FIRST(val, 0) = FIRST(val): match_start row
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS FIRST(val, 0) = 10
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |  1 |   6
+  2 |  20 |    |   0
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- FIRST(val, 1): match_start + 1 row (second row of match)
+-- match_start=1(10): FIRST(val,1)=20, B needs val=20 → id2(20) match, id3(30) no
+-- match_start=3(30): FIRST(val,1)=10, B needs val=10 → id4(10) match
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS val = FIRST(val, 1)
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |  1 |   2
+  2 |  20 |    |   0
+  3 |  30 |  3 |   2
+  4 |  10 |    |   0
+  5 |  50 |  5 |   2
+  6 |  10 |    |   0
+(6 rows)
+
+-- FIRST(val, 99): offset beyond match range → NULL, no match
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS FIRST(val, 99) IS NOT NULL
+);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  10 |   0
+  5 |  50 |   0
+  6 |  10 |   0
+(6 rows)
+
+-- LAST(val, 0) = LAST(val): current row
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS LAST(val, 0) > 15
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |  1 |   3
+  2 |  20 |    |   0
+  3 |  30 |    |   0
+  4 |  10 |  4 |   2
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- LAST(val, 1): one row back from current (previous match row)
+-- At B evaluation on id2: LAST(val,1) = val at id1 = 10
+-- B matches when previous row val < 30
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS LAST(val, 1) < 30
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |  1 |   3
+  2 |  20 |    |   0
+  3 |  30 |    |   0
+  4 |  10 |  4 |   2
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- LAST(val, 99): offset before match_start → NULL
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS LAST(val, 99) IS NOT NULL
+);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  10 |   0
+  5 |  50 |   0
+  6 |  10 |   0
+(6 rows)
+
+-- Error: NULL offset
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS FIRST(val, NULL::int8) IS NULL
+);
+ERROR:  row pattern navigation offset must not be null
+-- Error: negative offset
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    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:  first can only be used in a DEFINE clause
+LINE 1: SELECT first(val) FROM rpr_nav;
+               ^
+SELECT last(val) FROM rpr_nav;
+ERROR:  last can only be used in a DEFINE clause
+LINE 1: SELECT last(val) FROM rpr_nav;
+               ^
+SELECT first(val, 1) FROM rpr_nav;
+ERROR:  first can only be used in 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');
+SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f;
+ prev | next | first | last 
+------+------+-------+------
+    1 |    2 | Joe   | Blow
+(1 row)
+
+DROP TABLE rpr_names;
+-- Compound navigation: PREV(FIRST(val), M)
+-- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10)
+-- PREV(FIRST(val), 1): target = match_start + 0 - 1 = match_start - 1
+-- At match_start=1: target=0 → out of range → NULL
+-- At match_start=3: target=2(val=20) → 20 > 0 → true
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |    |   0
+  2 |  20 |  2 |   5
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2
+-- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 → true
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 1) > 0
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |  1 |   6
+  2 |  20 |    |   0
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- PREV(LAST(val), 2): target = currentpos - 0 - 2 = currentpos - 2
+-- Same backward reach as PREV(val, 2)
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(LAST(val), 2) IS NOT NULL
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |    |   0
+  2 |  20 |  2 |   5
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- NEXT(LAST(val, 1), 2): target = currentpos - 1 + 2 = currentpos + 1
+-- Looks 1 row ahead: same as NEXT(val, 1)
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(LAST(val, 1), 2) IS NOT NULL
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |  1 |   5
+  2 |  20 |    |   0
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- Compound: outer offset beyond partition (PREV far back)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val), 99) IS NOT NULL
+);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  10 |   0
+  5 |  50 |   0
+  6 |  10 |   0
+(6 rows)
+
+-- Compound: outer offset beyond partition (NEXT far forward)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(FIRST(val), 99) IS NOT NULL
+);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  10 |   0
+  5 |  50 |   0
+  6 |  10 |   0
+(6 rows)
+
+-- Compound: inner offset beyond match range (FIRST offset too large)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val, 99), 1) IS NOT NULL
+);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  10 |   0
+  5 |  50 |   0
+  6 |  10 |   0
+(6 rows)
+
+-- Compound: inner offset beyond match range (LAST offset too large)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(LAST(val, 99), 1) IS NOT NULL
+);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  10 |   0
+  5 |  50 |   0
+  6 |  10 |   0
+(6 rows)
+
+-- Compound: NULL outer offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(FIRST(val), NULL::int8) IS NULL
+);
+ERROR:  row pattern navigation offset must not be null
+-- Compound: negative outer offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(LAST(val), -1) IS NULL
+);
+ERROR:  row pattern navigation offset must not be negative
+-- Compound: default offsets on both sides
+-- PREV(FIRST(val)): inner=0 (match_start), outer=1 → target = match_start - 1
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val)) IS NOT NULL
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |    |   0
+  2 |  20 |  2 |   5
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- NEXT(LAST(val)): inner=0 (currentpos), outer=1 → target = currentpos + 1
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(LAST(val)) IS NOT NULL
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |  1 |   5
+  2 |  20 |    |   0
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+-- Compound: inner NULL offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(FIRST(val, NULL::int8), 1) IS NULL
+);
+ERROR:  row pattern navigation offset must not be null
+-- Compound: inner negative offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(LAST(val, -1), 1) IS NULL
+);
+ERROR:  row pattern navigation offset must not be negative
+-- Compound + host variable offsets
+PREPARE test_compound_offset(int8, int8) AS
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val, $1), $2) IS NOT NULL
+);
+EXECUTE test_compound_offset(0, 1);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |    |   0
+  2 |  20 |  2 |   5
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+EXECUTE test_compound_offset(1, 1);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |  1 |   6
+  2 |  20 |    |   0
+  3 |  30 |    |   0
+  4 |  10 |    |   0
+  5 |  50 |    |   0
+  6 |  10 |    |   0
+(6 rows)
+
+DEALLOCATE test_compound_offset;
+-- Compound + SKIP TO NEXT ROW: overlapping matches with PREV(FIRST())
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+ id | val | mf | cnt 
+----+-----+----+-----
+  1 |  10 |    |   0
+  2 |  20 |  2 |   5
+  3 |  30 |  3 |   4
+  4 |  10 |  4 |   3
+  5 |  50 |  5 |   2
+  6 |  10 |    |   0
+(6 rows)
+
+-- Compound + multiple partitions
+CREATE TEMP TABLE rpr_nav_part (gid int, id int, val int);
+INSERT INTO rpr_nav_part VALUES
+    (1,1,10),(1,2,20),(1,3,30),
+    (2,1,40),(2,2,50),(2,3,60);
+SELECT gid, id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav_part WINDOW w AS (
+    PARTITION BY gid ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(FIRST(val), 1) > 0
+);
+ gid | id | val | mf | cnt 
+-----+----+-----+----+-----
+   1 |  1 |  10 |  1 |   3
+   1 |  2 |  20 |    |   0
+   1 |  3 |  30 |    |   0
+   2 |  1 |  40 |  1 |   3
+   2 |  2 |  50 |    |   0
+   2 |  3 |  60 |    |   0
+(6 rows)
+
+DROP TABLE rpr_nav_part;
+-- Reverse nesting: FIRST wrapping PREV is prohibited
+SELECT id, val FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0
+);
+ERROR:  FIRST and LAST cannot contain PREV or NEXT
+LINE 5:     DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0
+                                   ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+-- Reverse nesting: LAST wrapping NEXT is prohibited
+SELECT id, val FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS LAST(NEXT(val)) > 0
+);
+ERROR:  FIRST and LAST cannot contain PREV or NEXT
+LINE 5:     DEFINE A AS TRUE, B AS LAST(NEXT(val)) > 0
+                                   ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+DROP TABLE rpr_nav;
 --
 -- SKIP TO / Backtracking / Frame boundary
 --
@@ -2247,6 +2850,27 @@ FROM result WHERE match_len > 0;
             1 |         99999
 (1 row)
 
+RESET jit_above_cost;
+RESET jit;
+-- JIT compound navigation test
+SET jit = on;
+SET jit_above_cost = 0;
+SELECT count(*) AS matched_rows
+FROM (
+ SELECT v, count(*) OVER w AS match_len
+ FROM generate_series(1, 1000) AS t(v)
+ WINDOW w AS (
+  DEFINE A AS TRUE, B AS PREV(FIRST(v), 1) > 0
+) sub WHERE match_len > 0;
+ matched_rows 
+--------------
+            1
 RESET jit_above_cost;
 RESET jit;
index 1e450a07ced..0845316965e 100644
@@ -2227,6 +2302,150 @@ SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
    d AS (val > 30) );
 (1 row)
 
+-- Navigation function serialization: PREV with offset
+CREATE VIEW rpr_serial_nav1 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS val > PREV(val, 2));
+SELECT pg_get_viewdef('rpr_serial_nav1'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a b+)                                                            +
+   DEFINE                                                                    +
+   a AS true,                                                                +
+   b AS (val > PREV(val, (2)::bigint)) );
+(1 row)
+
+-- Navigation function serialization: FIRST and LAST
+CREATE VIEW rpr_serial_nav2 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS FIRST(val) < LAST(val, 1));
+SELECT pg_get_viewdef('rpr_serial_nav2'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a b+)                                                            +
+   DEFINE                                                                    +
+   a AS true,                                                                +
+   b AS (FIRST(val) < LAST(val, (1)::bigint)) );
+(1 row)
+
+-- Navigation function serialization: compound PREV(FIRST())
+CREATE VIEW rpr_serial_nav3 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS PREV(FIRST(val, 1), 2) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav3'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a b+)                                                            +
+   DEFINE                                                                    +
+   a AS true,                                                                +
+   b AS (PREV(FIRST(val, (1)::bigint), (2)::bigint) > 0) );
+(1 row)
+
+-- Navigation function serialization: compound NEXT(LAST())
+CREATE VIEW rpr_serial_nav4 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS NEXT(LAST(val), 2) IS NOT NULL);
+SELECT pg_get_viewdef('rpr_serial_nav4'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a b+)                                                            +
+   DEFINE                                                                    +
+   a AS true,                                                                +
+   b AS (NEXT(LAST(val), (2)::bigint) IS NOT NULL) );
+(1 row)
+
+-- Navigation function serialization: compound PREV(LAST())
+CREATE VIEW rpr_serial_nav5 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS PREV(LAST(val, 1), 2) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav5'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a b+)                                                            +
+   DEFINE                                                                    +
+   a AS true,                                                                +
+   b AS (PREV(LAST(val, (1)::bigint), (2)::bigint) > 0) );
+(1 row)
+
+-- Navigation function serialization: compound NEXT(FIRST())
+CREATE VIEW rpr_serial_nav6 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a b+)                                                            +
+   DEFINE                                                                    +
+   a AS true,                                                                +
+   b AS (NEXT(FIRST(val), (3)::bigint) > 0) );
+(1 row)
+
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
@@ -3021,6 +3240,42 @@ ORDER BY id;
 (3 rows)
 
 DROP TABLE rpr_null;
+-- Compound navigation: inner nav must be direct arg (not nested in expression)
+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 operation must be a direct argument of the outer navigation
+LINE 6:     DEFINE A AS PREV(v + FIRST(v)) > 0
+                        ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+-- FIRST/LAST wrapping FIRST/LAST: prohibited
+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 FIRST(FIRST(v)) > 0
+);
+ERROR:  FIRST and LAST cannot contain FIRST or LAST
+LINE 6:     DEFINE A AS FIRST(FIRST(v)) > 0
+                        ^
+HINT:  Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+-- Triple nesting: prohibited (3-level deep navigation)
+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(PREV(v))) > 0
+);
+ERROR:  row pattern navigation cannot be nested 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.
 -- ============================================================
 -- Window Deduplication Tests
 -- ============================================================
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 77ab25a2289..dc3522f930f 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -36,7 +36,7 @@
 --   Window Function Combinations
 --   DEFINE Expression Variations
 --   Large Scale Statistics Verification
---   Nav Mark Lookback (tuplestore trim)
+--   Nav Mark Lookback/Lookahead (tuplestore trim)
 -- ============================================================
 -- Filter function to normalize platform-dependent memory values (not NFA statistics).
 -- NFA statistics should not change between platforms; if they do, it could
@@ -1131,6 +1131,127 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=42.00 loops=1)
 (10 rows)
 
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: 0
+   Nav Mark Lookback: 0
+-- No absorption with compound PREV(FIRST()) (match_start-dependent)
+CREATE VIEW rpr_ev_ctx_no_absorb_compound AS
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: -1
+   NFA: 10 matched (len 4/5/4.9), 1 mismatched (len 5/5/5.0)
+   NFA: 0 absorbed, 39 skipped (len 1/4/2.5)
@@ -4343,9 +4464,10 @@ SELECT * FROM (
 (9 rows)
 
 -- ============================================================
--- Nav Mark Lookback Tests
--- Verifies planner-computed navigation offset for tuplestore trim.
--- Lookback: how far back from currentpos (PREV/LAST).
+-- Nav Mark Lookback/Lookahead Tests
+-- Verifies planner-computed navigation offsets for tuplestore trim.
+-- Lookback: how far back from currentpos (PREV, LAST, compound PREV_LAST/NEXT_LAST).
+-- Lookahead: how far forward from match_start (FIRST, compound PREV_FIRST/NEXT_FIRST).
 -- ============================================================
 -- Prepare statement for host variable offset test below
 PREPARE rpr_nav_offset_prep(int8) AS
@@ -4466,3 +4588,298 @@ EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
 
 RESET plan_cache_mode;
 DEALLOCATE rpr_nav_offset_prep;
+-- FIRST(v): retain all (references match_start row)
+EXPLAIN (COSTS OFF) 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 v > FIRST(v)
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: 0
+   ->  Function Scan on generate_series s
+(6 rows)
+
+-- LAST(v, 1): backward reach 1, same as PREV(v, 1)
+EXPLAIN (COSTS OFF) 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 LAST(v, 1) > 0
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 1
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- LAST(v) without offset + PREV(v): no match_start dependency, offset 1
+EXPLAIN (COSTS OFF) 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 LAST(v) > PREV(v)
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: 1
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- Compound PREV(FIRST(val, 1), 2): lookback from match_start, firstOffset = 1-2 = -1
+EXPLAIN (COSTS OFF) 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, 1), 2) > 0
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: -1
+   ->  Function Scan on generate_series s
+(6 rows)
+
+-- Compound NEXT(FIRST(val), 3): firstOffset = 0+3 = 3
+EXPLAIN (COSTS OFF) 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(FIRST(v), 3) > 0
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: 3
+   ->  Function Scan on generate_series s
+(6 rows)
+
+-- Compound PREV(LAST(val), 2): lookback = 0+2 = 2
+EXPLAIN (COSTS OFF) 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(LAST(v), 2) > 0
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+"
+   Nav Mark Lookback: 2
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- Compound NEXT(LAST(val, 1), 3): lookback = max(1-3, 0) = 0
+EXPLAIN (COSTS OFF) 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(LAST(v, 1), 3) > 0
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 0
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- Compound PREV(LAST(val, N), M): constant near-overflow (N+M just fits int64)
+EXPLAIN (COSTS OFF) 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(LAST(v, 4611686018427387903), 4611686018427387903) IS NOT NULL
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 9223372036854775806
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- Compound PREV(LAST(val, N), M): constant overflow -> retain all
+EXPLAIN (COSTS OFF) 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(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: retain all
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
+-- N + M overflows int64, but target is forward from match_start so it never
+-- constrains trim.  Lookahead remains at default (0).
+EXPLAIN (COSTS OFF) 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(FIRST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: 0
+   ->  Function Scan on generate_series s
+(6 rows)
+
+-- Compound PREV(LAST(val, $1), $2): parameter lookback overflow -> retain all
+-- EXPLAIN shows "runtime" (plan-level); EXPLAIN ANALYZE shows "retain all"
+-- (executor-resolved).
+PREPARE test_overflow_lookback(int8, int8) AS
+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(LAST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: runtime
+   ->  Function Scan on generate_series s
+(5 rows)
+
+EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
+    EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=10.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: retain all
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 1 peak, 11 total, 0 merged
+   NFA Contexts: 2 peak, 11 total, 10 pruned
+   NFA: 0 matched, 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=10.00 loops=1)
+(9 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE test_overflow_lookback;
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+PREPARE test_overflow_lookahead(int8, int8) AS
+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(FIRST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
+    EXECUTE test_overflow_lookahead(4611686018427387904, 4611686018427387904);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=10.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: 0
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 1 peak, 11 total, 0 merged
+   NFA Contexts: 2 peak, 11 total, 10 pruned
+   NFA: 0 matched, 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=10.00 loops=1)
+(10 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE test_overflow_lookahead;
+-- PREV(v) + PREV(v, $1): NEEDS_EVAL path must account for implicit lookback=1
+-- Previously, eval_nav_max_offset_walker skipped PREV(v) when offset_arg was
+-- NULL, causing maxOffset=0 when $1=0, which would trim the row needed by
+-- PREV(v).  Verify this executes without "cannot fetch row before mark" error.
+PREPARE test_prev_implicit_offset(int8) AS
+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) IS NOT NULL AND PREV(v, $1) IS NOT NULL
+);
+EXECUTE test_prev_implicit_offset(0);
+ count 
+-------
+     0
+     9
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+(10 rows)
+
+DEALLOCATE test_prev_implicit_offset;
+-- Runtime error: negative offset at execution time
+PREPARE test_runtime_neg_offset(int8) AS
+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, $1) IS NOT NULL
+);
+EXECUTE test_runtime_neg_offset(-1);
+ERROR:  row pattern navigation offset must not be negative
+DEALLOCATE test_runtime_neg_offset;
+-- Runtime error: null offset at execution time
+PREPARE test_runtime_null_offset(int8) AS
+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, $1) IS NOT NULL
+);
+EXECUTE test_runtime_null_offset(NULL);
+ERROR:  row pattern navigation offset must not be null
+DEALLOCATE test_runtime_null_offset;
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 47f33904690..a05b429ce74 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -776,6 +776,363 @@ WINDOW w AS (
     DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
 );
 
+--
+-- FIRST/LAST navigation
+--
+
+-- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val)
+-- at specific positions.
+CREATE TEMP TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10);
+
+-- FIRST(val) = constant: B matches when match_start has val=10
+-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 → match {1,2}
+-- match_start=3(30): A=id3, B=id4, FIRST(val)=30≠10 → no match
+-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 → match {4,5}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS FIRST(val) = 10
+);
+
+-- LAST(val): always equals current row's val (offset 0 default)
+-- Equivalent to: B AS val > 15
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS LAST(val) > 15
+);
+
+-- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where
+-- first and last rows have the same val.
+-- match_start=1(10): reluctant tries B early:
+--   id2(20≠10), id3(30≠10), id4(10=10) → match {1,2,3,4}
+-- match_start=5(50): id6(10≠50) → no match
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+? B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- Greedy A+ with FIRST(val) = LAST(val): find longest match where
+-- first and last rows have the same val.
+-- match_start=1(10): greedy A eats all, B tries last:
+--   id6(10=10) → match {1,2,3,4,5,6}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts.
+-- With ONE ROW PER MATCH, each row shows only its first match result.
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+? B)
+    DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- FIRST/LAST 2-arg offset form
+--
+-- FIRST(val, 0) = FIRST(val): match_start row
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS FIRST(val, 0) = 10
+);
+
+-- FIRST(val, 1): match_start + 1 row (second row of match)
+-- match_start=1(10): FIRST(val,1)=20, B needs val=20 → id2(20) match, id3(30) no
+-- match_start=3(30): FIRST(val,1)=10, B needs val=10 → id4(10) match
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS val = FIRST(val, 1)
+);
+
+-- FIRST(val, 99): offset beyond match range → NULL, no match
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS FIRST(val, 99) IS NOT NULL
+);
+
+-- LAST(val, 0) = LAST(val): current row
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS LAST(val, 0) > 15
+);
+
+-- LAST(val, 1): one row back from current (previous match row)
+-- At B evaluation on id2: LAST(val,1) = val at id1 = 10
+-- B matches when previous row val < 30
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS LAST(val, 1) < 30
+);
+
+-- LAST(val, 99): offset before match_start → NULL
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS LAST(val, 99) IS NOT NULL
+);
+
+-- Error: NULL offset
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS FIRST(val, NULL::int8) IS NULL
+);
+
+-- Error: negative offset
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    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');
+SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f;
+DROP TABLE rpr_names;
+
+-- Compound navigation: PREV(FIRST(val), M)
+-- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10)
+-- PREV(FIRST(val), 1): target = match_start + 0 - 1 = match_start - 1
+-- At match_start=1: target=0 → out of range → NULL
+-- At match_start=3: target=2(val=20) → 20 > 0 → true
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+
+-- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2
+-- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 → true
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 1) > 0
+);
+
+-- PREV(LAST(val), 2): target = currentpos - 0 - 2 = currentpos - 2
+-- Same backward reach as PREV(val, 2)
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(LAST(val), 2) IS NOT NULL
+);
+
+-- NEXT(LAST(val, 1), 2): target = currentpos - 1 + 2 = currentpos + 1
+-- Looks 1 row ahead: same as NEXT(val, 1)
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(LAST(val, 1), 2) IS NOT NULL
+);
+
+-- Compound: outer offset beyond partition (PREV far back)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val), 99) IS NOT NULL
+);
+
+-- Compound: outer offset beyond partition (NEXT far forward)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(FIRST(val), 99) IS NOT NULL
+);
+
+-- Compound: inner offset beyond match range (FIRST offset too large)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val, 99), 1) IS NOT NULL
+);
+
+-- Compound: inner offset beyond match range (LAST offset too large)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(LAST(val, 99), 1) IS NOT NULL
+);
+
+-- Compound: NULL outer offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(FIRST(val), NULL::int8) IS NULL
+);
+
+-- Compound: negative outer offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(LAST(val), -1) IS NULL
+);
+
+-- Compound: default offsets on both sides
+-- PREV(FIRST(val)): inner=0 (match_start), outer=1 → target = match_start - 1
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val)) IS NOT NULL
+);
+
+-- NEXT(LAST(val)): inner=0 (currentpos), outer=1 → target = currentpos + 1
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(LAST(val)) IS NOT NULL
+);
+
+-- Compound: inner NULL offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(FIRST(val, NULL::int8), 1) IS NULL
+);
+
+-- Compound: inner negative offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(LAST(val, -1), 1) IS NULL
+);
+
+-- Compound + host variable offsets
+PREPARE test_compound_offset(int8, int8) AS
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val, $1), $2) IS NOT NULL
+);
+EXECUTE test_compound_offset(0, 1);
+EXECUTE test_compound_offset(1, 1);
+DEALLOCATE test_compound_offset;
+
+-- Compound + SKIP TO NEXT ROW: overlapping matches with PREV(FIRST())
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+
+-- Compound + multiple partitions
+CREATE TEMP TABLE rpr_nav_part (gid int, id int, val int);
+INSERT INTO rpr_nav_part VALUES
+    (1,1,10),(1,2,20),(1,3,30),
+    (2,1,40),(2,2,50),(2,3,60);
+SELECT gid, id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav_part WINDOW w AS (
+    PARTITION BY gid ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(FIRST(val), 1) > 0
+);
+DROP TABLE rpr_nav_part;
+
+-- Reverse nesting: FIRST wrapping PREV is prohibited
+SELECT id, val FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0
+);
+
+-- Reverse nesting: LAST wrapping NEXT is prohibited
+SELECT id, val FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS TRUE, B AS LAST(NEXT(val)) > 0
+);
+
+DROP TABLE rpr_nav;
+
 --
 -- SKIP TO / Backtracking / Frame boundary
 --
@@ -1129,6 +1486,23 @@ FROM result WHERE match_len > 0;
 RESET jit_above_cost;
 RESET jit;
 
+-- JIT compound navigation test
+SET jit = on;
+SET jit_above_cost = 0;
+SELECT count(*) AS matched_rows
+FROM (
+ SELECT v, count(*) OVER w AS match_len
+ FROM generate_series(1, 1000) AS t(v)
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  PATTERN (A B+)
+  DEFINE A AS TRUE, B AS PREV(FIRST(v), 1) > 0
+ )
+) sub WHERE match_len > 0;
+RESET jit_above_cost;
+RESET jit;
+
 --
 -- IGNORE NULLS
 --
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 3accecb73ba..f9d5aa89d7a 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1202,7 +1202,7 @@ WINDOW w AS (
 DROP TABLE rpr_bounds;

 
 
 -- ============================================================
@@ -1548,6 +1595,66 @@ WINDOW w AS (
 SELECT * FROM rpr_serial_v8 ORDER BY id;
 SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
 
+-- Navigation function serialization: PREV with offset
+CREATE VIEW rpr_serial_nav1 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS val > PREV(val, 2));
+SELECT pg_get_viewdef('rpr_serial_nav1'::regclass);
+
+-- Navigation function serialization: FIRST and LAST
+CREATE VIEW rpr_serial_nav2 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS FIRST(val) < LAST(val, 1));
+SELECT pg_get_viewdef('rpr_serial_nav2'::regclass);
+
+-- Navigation function serialization: compound PREV(FIRST())
+CREATE VIEW rpr_serial_nav3 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS PREV(FIRST(val, 1), 2) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav3'::regclass);
+
+-- Navigation function serialization: compound NEXT(LAST())
+CREATE VIEW rpr_serial_nav4 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS NEXT(LAST(val), 2) IS NOT NULL);
+SELECT pg_get_viewdef('rpr_serial_nav4'::regclass);
+
+-- Navigation function serialization: compound PREV(LAST())
+CREATE VIEW rpr_serial_nav5 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS PREV(LAST(val, 1), 2) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav5'::regclass);
+
+-- Navigation function serialization: compound NEXT(FIRST())
+CREATE VIEW rpr_serial_nav6 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
+
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
@@ -2121,6 +2228,33 @@ ORDER BY id;
 
 DROP TABLE rpr_null;
 
+-- Compound navigation: inner nav must be direct arg (not nested in expression)
+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
+);
+
+-- FIRST/LAST wrapping FIRST/LAST: prohibited
+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 FIRST(FIRST(v)) > 0
+);
+
+-- Triple nesting: prohibited (3-level deep navigation)
+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(PREV(v))) > 0
+);
+
 -- ============================================================
 -- Window Deduplication Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 5082cc2b5de..a3789e92631 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -36,7 +36,7 @@
 --   Window Function Combinations
 --   DEFINE Expression Variations
 --   Large Scale Statistics Verification
---   Nav Mark Lookback (tuplestore trim)
+--   Nav Mark Lookback/Lookahead (tuplestore trim)
 -- ============================================================
 
 -- Filter function to normalize platform-dependent memory values (not NFA statistics).
@@ -705,6 +705,76 @@ WINDOW w AS (
            C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
 );');
 
+-- No absorption with compound PREV(FIRST()) (match_start-dependent)
+CREATE VIEW rpr_ev_ctx_no_absorb_compound AS
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL
@@ -2478,9 +2548,10 @@ SELECT * FROM (
 ) t WHERE cnt > 0;
 
 -- ============================================================
--- Nav Mark Lookback Tests
--- Verifies planner-computed navigation offset for tuplestore trim.
--- Lookback: how far back from currentpos (PREV/LAST).
+-- Nav Mark Lookback/Lookahead Tests
+-- Verifies planner-computed navigation offsets for tuplestore trim.
+-- Lookback: how far back from currentpos (PREV, LAST, compound PREV_LAST/NEXT_LAST).
+-- Lookahead: how far forward from match_start (FIRST, compound PREV_FIRST/NEXT_FIRST).
 -- ============================================================
 
 -- Prepare statement for host variable offset test below
@@ -2547,3 +2618,166 @@ EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
 RESET plan_cache_mode;
 DEALLOCATE rpr_nav_offset_prep;
 
+-- FIRST(v): retain all (references match_start row)
+EXPLAIN (COSTS OFF) 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 v > FIRST(v)
+);
+
+-- LAST(v, 1): backward reach 1, same as PREV(v, 1)
+EXPLAIN (COSTS OFF) 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 LAST(v, 1) > 0
+);
+
+-- LAST(v) without offset + PREV(v): no match_start dependency, offset 1
+EXPLAIN (COSTS OFF) 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 LAST(v) > PREV(v)
+);
+
+-- Compound PREV(FIRST(val, 1), 2): lookback from match_start, firstOffset = 1-2 = -1
+EXPLAIN (COSTS OFF) 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, 1), 2) > 0
+);
+
+-- Compound NEXT(FIRST(val), 3): firstOffset = 0+3 = 3
+EXPLAIN (COSTS OFF) 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(FIRST(v), 3) > 0
+);
+
+-- Compound PREV(LAST(val), 2): lookback = 0+2 = 2
+EXPLAIN (COSTS OFF) 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(LAST(v), 2) > 0
+);
+
+-- Compound NEXT(LAST(val, 1), 3): lookback = max(1-3, 0) = 0
+EXPLAIN (COSTS OFF) 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(LAST(v, 1), 3) > 0
+);
+
+-- Compound PREV(LAST(val, N), M): constant near-overflow (N+M just fits int64)
+EXPLAIN (COSTS OFF) 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(LAST(v, 4611686018427387903), 4611686018427387903) IS NOT NULL
+);
+
+-- Compound PREV(LAST(val, N), M): constant overflow -> retain all
+EXPLAIN (COSTS OFF) 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(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
+);
+
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
+-- N + M overflows int64, but target is forward from match_start so it never
+-- constrains trim.  Lookahead remains at default (0).
+EXPLAIN (COSTS OFF) 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(FIRST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
+);
+
+-- Compound PREV(LAST(val, $1), $2): parameter lookback overflow -> retain all
+-- EXPLAIN shows "runtime" (plan-level); EXPLAIN ANALYZE shows "retain all"
+-- (executor-resolved).
+PREPARE test_overflow_lookback(int8, int8) AS
+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(LAST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904);
+EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
+    EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904);
+RESET plan_cache_mode;
+DEALLOCATE test_overflow_lookback;
+
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+PREPARE test_overflow_lookahead(int8, int8) AS
+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(FIRST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
+    EXECUTE test_overflow_lookahead(4611686018427387904, 4611686018427387904);
+RESET plan_cache_mode;
+DEALLOCATE test_overflow_lookahead;
+
+-- PREV(v) + PREV(v, $1): NEEDS_EVAL path must account for implicit lookback=1
+-- Previously, eval_nav_max_offset_walker skipped PREV(v) when offset_arg was
+-- NULL, causing maxOffset=0 when $1=0, which would trim the row needed by
+-- PREV(v).  Verify this executes without "cannot fetch row before mark" error.
+PREPARE test_prev_implicit_offset(int8) AS
+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) IS NOT NULL AND PREV(v, $1) IS NOT NULL
+);
+EXECUTE test_prev_implicit_offset(0);
+DEALLOCATE test_prev_implicit_offset;
+
+-- Runtime error: negative offset at execution time
+PREPARE test_runtime_neg_offset(int8) AS
+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, $1) IS NOT NULL
+);
+EXECUTE test_runtime_neg_offset(-1);
+DEALLOCATE test_runtime_neg_offset;
+
+-- Runtime error: null offset at execution time
+PREPARE test_runtime_null_offset(int8) AS
+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, $1) IS NOT NULL
+);
+EXECUTE test_runtime_null_offset(NULL);
+DEALLOCATE test_runtime_null_offset;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 16de1421302..a3b1a855cdc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -747,6 +747,8 @@ ErrorData
 ErrorSaveContext
 EstimateDSMForeignScan_function
 EstimationInfo
+EvalNavFirstContext
+EvalNavMaxContext
 EventTriggerCacheEntry
 EventTriggerCacheItem
 EventTriggerCacheStateType
@@ -1801,6 +1803,8 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
+NavCheckResult
+NavOffsetContext
 NestLoop
 NestLoopParam
 NestLoopState
@@ -2479,6 +2483,7 @@ QueuePosition
 QuitSignalReason
 RPRNavExpr
 RPRNavKind
+RPRNavOffsetKind
 RBTNode
 RBTOrderControl
 RBTree
-- 
2.50.1 (Apple Git-155)

From d5c5eb7b1795eecdae73dbb23df7c12bbddc3a36 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 13:31:44 +0900
Subject: [PATCH] Guard against int64 overflow in RPR bounded frame end
 computation

---
 src/backend/executor/execRPR.c | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 60f0d8b2fa1..ee4c67c9597 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -22,6 +22,7 @@
  */
 #include "postgres.h"
 
+#include "common/int.h"
 #include "executor/execRPR.h"
 #include "executor/executor.h"
 #include "miscadmin.h"
@@ -1046,10 +1047,11 @@
  *
  *   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
  *   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
- *   frameOffset indicating the upper bound.  After the advance phase,
+ *   frameOffset indicating the upper bound.  Before the match phase,
  *   any context whose match has exceeded the frame boundary
- *   (currentPos - matchStartRow >= frameOffset + 1) is finalized early.
- *   This prevents matches from extending beyond the window frame.
+ *   (currentPos >= matchStartRow + frameOffset + 1) is finalized early
+ *   by forcing a mismatch.  This prevents matches from extending beyond
+ *   the window frame.  The sum is clamped to PG_INT64_MAX on overflow.
  *
  *   Note that bounded frames also disable context absorption at the
  *   planner level (see VIII-3(b)), since the frame boundary breaks the
@@ -3154,7 +3156,12 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		/* Check frame boundary - finalize if exceeded */
 		if (hasLimitedFrame)
 		{
-			int64		ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
+			int64		ctxFrameEnd;
+
+			/* Clamp to INT64_MAX on overflow */
+			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset + 1,
+									&ctxFrameEnd))
+				ctxFrameEnd = PG_INT64_MAX;
 
 			if (currentPos >= ctxFrameEnd)
 			{
@@ -3204,6 +3211,7 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		 * context here must be within its frame boundary.
 		 */
 		Assert(!hasLimitedFrame ||
+			   ctx->matchStartRow > PG_INT64_MAX - frameOffset - 1 ||
 			   currentPos < ctx->matchStartRow + frameOffset + 1);
 
 		nfa_advance(winstate, ctx, currentPos);
-- 
2.50.1 (Apple Git-155)

From c3dc38316266bddc6d0d17aff47235bcfc79b303 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 11:15:12 +0900
Subject: [PATCH] Fix RPR error message style: hint format, terminology,
 capitalization

Remove colon in errhint "Use: ROWS instead" -> "Use ROWS instead."
and add missing trailing period.  Shorten "row pattern definition
variable name" to "DEFINE variable" for consistency with other
error messages.  Capitalize navigation function names in stub
error messages (prev -> PREV, etc.) to match SQL standard keyword
style used elsewhere in the parser.
---
 src/backend/parser/parse_rpr.c         |  6 +++---
 src/backend/utils/adt/windowfuncs.c    | 16 ++++++++--------
 src/test/regress/expected/rpr_base.out | 12 ++++++------
 3 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 05070cb04bb..8fbe12e1518 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -78,7 +78,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		ereport(ERROR,
 				(errcode(ERRCODE_WINDOWING_ERROR),
 				 errmsg("FRAME option GROUPS is not permitted when row pattern recognition is used"),
-				 errhint("Use: ROWS instead"),
+				 errhint("Use ROWS instead."),
 				 parser_errposition(pstate,
 									windef->frameLocation >= 0 ?
 									windef->frameLocation : windef->location)));
@@ -86,7 +86,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		ereport(ERROR,
 				(errcode(ERRCODE_WINDOWING_ERROR),
 				 errmsg("FRAME option RANGE is not permitted when row pattern recognition is used"),
-				 errhint("Use: ROWS instead"),
+				 errhint("Use ROWS instead."),
 				 parser_errposition(pstate,
 									windef->frameLocation >= 0 ?
 									windef->frameLocation : windef->location)));
@@ -329,7 +329,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			if (!strcmp(n, name))
 				ereport(ERROR,
 						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("row pattern definition variable name \"%s\" appears more than once in DEFINE clause",
+						 errmsg("DEFINE variable \"%s\" appears more than once",
 								name),
 						 parser_errposition(pstate, exprLocation((Node *) r))));
 		}
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 420a4962395..fb966cae43c 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -740,7 +740,7 @@ window_prev(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("prev() can only be used in a DEFINE clause")));
+			 errmsg("PREV() can only be used in a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -754,7 +754,7 @@ window_next(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("next() can only be used in a DEFINE clause")));
+			 errmsg("NEXT() can only be used in a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -768,7 +768,7 @@ window_prev_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("prev() can only be used in a DEFINE clause")));
+			 errmsg("PREV() can only be used in a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -782,7 +782,7 @@ window_next_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("next() can only be used in a DEFINE clause")));
+			 errmsg("NEXT() can only be used in a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -796,7 +796,7 @@ window_first(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("first() can only be used in a DEFINE clause")));
+			 errmsg("FIRST() can only be used in a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -810,7 +810,7 @@ window_last(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("last() can only be used in a DEFINE clause")));
+			 errmsg("LAST() can only be used in a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -824,7 +824,7 @@ window_first_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("first() can only be used in a DEFINE clause")));
+			 errmsg("FIRST() can only be used in a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -838,6 +838,6 @@ window_last_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("last() can only be used in a DEFINE clause")));
+			 errmsg("LAST() can only be used in a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 0845316965e..912bd7b7c77 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -232,7 +232,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS id > 0, A AS id < 10
 );
-ERROR:  row pattern definition variable name "a" appears more than once in DEFINE clause
+ERROR:  DEFINE variable "a" appears more than once
 LINE 7:     DEFINE A AS id > 0, A AS id < 10
                    ^
 -- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
@@ -469,7 +469,7 @@ WINDOW w AS (
 ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
 LINE 5:     RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
             ^
-HINT:  Use: ROWS instead
+HINT:  Use ROWS instead.
 -- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
 -- GROUPS frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
@@ -483,7 +483,7 @@ WINDOW w AS (
 ERROR:  FRAME option GROUPS is not permitted when row pattern recognition is used
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
-HINT:  Use: ROWS instead
+HINT:  Use ROWS instead.
 -- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
@@ -640,7 +640,7 @@ ORDER BY id;
 ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
 LINE 5:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
-HINT:  Use: ROWS instead
+HINT:  Use ROWS instead.
 -- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -656,7 +656,7 @@ ORDER BY id;
 ERROR:  FRAME option GROUPS is not permitted when row pattern recognition is used
 LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
             ^
-HINT:  Use: ROWS instead
+HINT:  Use ROWS instead.
 -- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
 DROP TABLE rpr_frame;
 -- ============================================================
@@ -705,7 +705,7 @@ ORDER BY id;
 ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
 LINE 6:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
-HINT:  Use: ROWS instead
+HINT:  Use ROWS instead.
 -- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
 DROP TABLE rpr_partition;
 -- ============================================================
-- 
2.50.1 (Apple Git-155)

From c48c70f494fb656f45e2923bc4260ad8c82f5787 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 10:09:41 +0900
Subject: [PATCH] Fix comment typos, grammar, and inaccuracies in RPR code

---
 src/backend/executor/execRPR.c          |  7 +++----
 src/backend/executor/nodeWindowAgg.c    | 20 ++++++++++----------
 src/backend/optimizer/plan/createplan.c |  3 ++-
 3 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index ee4c67c9597..dede2dfab0d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -416,7 +416,6 @@
  * the normal loop-back (which cycle detection will eventually kill) and
  * a fast-forward exit clone that bypasses the loop entirely.
  * (See IX-4(c) for detailed runtime behavior.)
- *     - Empty match is impossible since body is not nullable
  *
  * IV-5. Absorbability Analysis (RPR_ELEM_ABSORBABLE)
  *
@@ -645,8 +644,8 @@
  * When processing a context whose matchStartRow differs from the shared
  * value, nfa_reevaluate_dependent_vars() temporarily sets nav_match_start
  * to that context's matchStartRow and re-evaluates only the dependent
- * variables.  No restore is needed because contexts are ordered by
- * matchStartRow (ascending), so no later context shares the head's value.
+ * variables.  The original nav_match_start and currentpos are saved and
+ * restored after re-evaluation.
  *
  * VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
  *
@@ -2715,7 +2714,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		bool		reluctant = RPRElemIsReluctant(elem);
 
 		/*
-		 * Clone state for the second-priority path. For greedy, clone is the
+		 * Clone state for the first-priority path. For greedy, clone is the
 		 * loop state; for reluctant, clone is the exit state.
 		 */
 		if (reluctant)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index cdbe356abd7..849ebf8abb0 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3705,8 +3705,8 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
 	{
 		/*
 		 * Early check if row could be out of reduced frame.  When RPR is
-		 * enabled, EXCUDE clause cannot be specified and the frame is always
-		 * contiguous.  So we can do the check followings safely. Note,
+		 * enabled, EXCLUDE clause cannot be specified and the frame is always
+		 * contiguous.  So we can safely perform the following checks. Note,
 		 * however, it is possible that a row is out of reduced frame if
 		 * there's a NULL in the middle. So we need to check it in the
 		 * following do loop.
@@ -4168,7 +4168,7 @@ eval_nav_first_offset(WindowAggState *winstate, List *defineClause)
 
 /*
  * rpr_is_defined
- * return true if Row pattern recognition is defined.
+ * Return true if row pattern recognition is defined.
  */
 static bool
 rpr_is_defined(WindowAggState *winstate)
@@ -4182,14 +4182,14 @@ rpr_is_defined(WindowAggState *winstate)
  * Determine whether a row is in the current row's reduced window frame
  * according to row pattern matching
  *
- * The row must has been already determined that it is in a full window frame
- * and fetched it into slot.
+ * The row must have already been determined to be in a full window frame
+ * and fetched into the slot.
  *
  * Returns:
  * = 0, RPR is not defined.
  * >0, if the row is the first in the reduced frame. Return the number of rows
  * in the reduced frame.
- * -1, if the row is unmatched row
+ * -1, if the row is an unmatched row
  * -2, if the row is in the reduced frame but needed to be skipped because of
  * AFTER MATCH SKIP PAST LAST ROW
  * -----------------
@@ -4204,8 +4204,8 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
 	if (!rpr_is_defined(winstate))
 	{
 		/*
-		 * RPR is not defined. Assume that we are always in the the reduced
-		 * window frame.
+		 * RPR is not defined. Assume that we are always in the reduced window
+		 * frame.
 		 */
 		rtn = 0;
 		return rtn;
@@ -4938,8 +4938,8 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
  * isout: output argument, set to indicate whether target row position
  *		is out of frame (can pass NULL if caller doesn't care about this)
  *
- * Returns 0 if we successfully got the slot. false if out of frame.
- * (also isout is set)
+ * Returns 0 if we successfully got the slot, or nonzero if out of frame.
+ * (isout is also set in the latter case.)
  */
 static int
 WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 02d511269ab..50668f3b7ab 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2475,7 +2475,8 @@ typedef struct NavOffsetContext
 	int64		maxOffset;		/* max PREV/LAST backward offset (>= 0) */
 	bool		maxNeedsEval;	/* non-constant PREV/LAST offset found */
 	bool		maxOverflow;	/* constant offset overflow detected */
-	int64		firstOffset;	/* min FIRST offset (>= 0), or -1 if none */
+	int64		firstOffset;	/* min FIRST offset (may be negative for
+								 * PREV_FIRST) */
 	bool		hasFirst;		/* any FIRST node found */
 	bool		firstNeedsEval; /* non-constant FIRST offset found */
 } NavOffsetContext;
-- 
2.50.1 (Apple Git-155)

From d3590794004c6200dee5178795a8d983f02d4375 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 10:50:36 +0900
Subject: [PATCH] Fix RPR documentation: synopsis, grammar, and terminology

Remove erroneous comma in PATTERN synopsis.  Fix typos in
advanced.sgml (">=;" stray semicolon, "with the a row",
"For example following").  Correct PREV/NEXT description
from "within the window frame" to "within the partition"
and add missing "DEFINE clause only" note.  Capitalize
"Row Pattern Recognition" consistently across SGML files.

Fix numerous missing articles and grammar errors in
select.sgml: "after a match found" -> "after a match is
found", "do not necessarily" -> "does not necessarily",
add missing "the" before clause references.
---
 doc/src/sgml/advanced.sgml         | 14 +++++++-------
 doc/src/sgml/func/func-window.sgml | 14 ++++++++------
 doc/src/sgml/ref/select.sgml       | 28 ++++++++++++++--------------
 3 files changed, 29 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 0caf9fdaff6..11c2416df51 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -553,8 +553,8 @@ WHERE pos &lt; 3;
    </para>
 
    <para>
-    Row pattern common syntax can be used to perform row pattern recognition
-    in a query. The row pattern common syntax includes two sub
+    Row Pattern Common Syntax can be used to perform Row Pattern Recognition
+    in a query. The Row Pattern Common Syntax includes two sub
     clauses: <literal>DEFINE</literal>
     and <literal>PATTERN</literal>. <literal>DEFINE</literal> defines
     row pattern variables along with an expression. The expression must be a
@@ -584,12 +584,12 @@ DEFINE
     Once <literal>DEFINE</literal> exists, <literal>PATTERN</literal> can be
     used. <literal>PATTERN</literal> defines a sequence of rows that satisfies
     conditions defined in the <literal>DEFINE</literal> clause.  For example
-    following <literal>PATTERN</literal> defines a sequence of rows starting
-    with the a row satisfying "LOWPRICE", then one or more rows satisfying
+    the following <literal>PATTERN</literal> defines a sequence of rows starting
+    with a row satisfying "LOWPRICE", then one or more rows satisfying
     "UP" and finally one or more rows satisfying "DOWN". Pattern variables can
     be followed by quantifiers: "+" means one or more matches, "*" means zero
     or more matches, "?" means zero or one match, "{n}" (n &gt; 0) means exactly
-    n matches, "{n,}" (n &gt;=; 0) means at least n matches, "{,m}" (m &gt; 0) means
+    n matches, "{n,}" (n &gt;= 0) means at least n matches, "{,m}" (m &gt; 0) means
     at most m matches, and "{n,m}" (0 &lt;= n &lt;= m, 0 &lt; m) means between n and m
     matches.  Patterns can be grouped using parentheses and combined using
     alternation (the vertical bar "|" for OR). For example, "(UP DOWN)+"
@@ -642,7 +642,7 @@ FROM stock
    </para>
 
    <para>
-    Row pattern recognition internally uses a nondeterministic finite
+    Row Pattern Recognition internally uses a nondeterministic finite
     automaton (NFA) to match patterns. For patterns with unbounded
     quantifiers (e.g., <literal>A+</literal> or <literal>(A B)+</literal>),
     the NFA may need to track many active matching contexts simultaneously,
@@ -676,7 +676,7 @@ FROM stock
    </para>
 
    <para>
-    When examining query plans for row pattern recognition with
+    When examining query plans for Row Pattern Recognition with
     <command>EXPLAIN</command>, the pattern output may include special
     markers that indicate optimization opportunities. A double quote
     <literal>"</literal> marks where pattern absorption can occur,
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index ab80690f7be..d109a2d22bc 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -279,9 +279,9 @@
   </para>
 
   <para>
-   Row pattern recognition navigation functions are listed in
+   Row Pattern Recognition navigation functions are listed in
    <xref linkend="functions-rpr-navigation-table"/>.  These functions
-   can be used to describe DEFINE clause of Row pattern recognition.
+   can be used to describe the DEFINE clause of Row Pattern Recognition.
   </para>
 
    <table id="functions-rpr-navigation-table">
@@ -309,12 +309,13 @@
        </para>
        <para>
         Returns the column value at the row <parameter>offset</parameter>
-        rows before the current row within the window frame;
-        returns NULL if the target row is outside the window frame.
+        rows before the current row within the partition;
+        returns NULL if the target row is outside the partition.
         <parameter>offset</parameter> defaults to 1 if omitted.
         <parameter>offset</parameter> must be a non-negative integer;
         an offset of 0 refers to the current row itself.
         <parameter>offset</parameter> must not be NULL.
+        Can only be used in a <literal>DEFINE</literal> clause.
        </para></entry>
       </row>
 
@@ -328,12 +329,13 @@
        </para>
        <para>
         Returns the column value at the row <parameter>offset</parameter>
-        rows after the current row within the window frame;
-        returns NULL if the target row is outside the window frame.
+        rows after the current row within the partition;
+        returns NULL if the target row is outside the partition.
         <parameter>offset</parameter> defaults to 1 if omitted.
         <parameter>offset</parameter> must be a non-negative integer;
         an offset of 0 refers to the current row itself.
         <parameter>offset</parameter> must not be NULL.
+        Can only be used in a <literal>DEFINE</literal> clause.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 5e4ba9d3cc6..5272d6c0bfa 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1133,34 +1133,34 @@ EXCLUDE NO OTHERS
    <para>
     The
     optional <replaceable class="parameter">row_pattern_common_syntax</replaceable>
-    defines the <firstterm>row pattern recognition condition</firstterm> for
+    defines the <firstterm>Row Pattern Recognition condition</firstterm> for
     this
     window. <replaceable class="parameter">row_pattern_common_syntax</replaceable>
-    includes following subclauses.
+    includes the following subclauses.
 
 <synopsis>
 [ { AFTER MATCH SKIP PAST LAST ROW | AFTER MATCH SKIP TO NEXT ROW } ]
 [ INITIAL | SEEK ]
-PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable> [ <replaceable>quantifier</replaceable> ] [, ...] )
+PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable> [ <replaceable>quantifier</replaceable> ] [ ... ] )
 DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...]
 </synopsis>
     <literal>AFTER MATCH SKIP PAST LAST ROW</literal> or <literal>AFTER MATCH
-    SKIP TO NEXT ROW</literal> controls how to proceed to next row position
-    after a match found. With <literal>AFTER MATCH SKIP PAST LAST
-    ROW</literal> (the default) next row position is next to the last row of
-    previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT
-    ROW</literal> next row position is next to the first row of previous
-    match. <literal>INITIAL</literal> or <literal>SEEK</literal> defines how a
-    successful pattern matching starts from which row in a
-    frame. If <literal>INITIAL</literal> is specified, the match must start
+    SKIP TO NEXT ROW</literal> controls how to proceed to the next row position
+    after a match is found. With <literal>AFTER MATCH SKIP PAST LAST
+    ROW</literal> (the default) the next row position is next to the last row of
+    the previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT
+    ROW</literal> the next row position is next to the first row of the previous
+    match. <literal>INITIAL</literal> or <literal>SEEK</literal> specifies from
+    which row in the frame pattern matching begins.
+    If <literal>INITIAL</literal> is specified, the match must start
     from the first row in the frame. If <literal>SEEK</literal> is specified,
-    the set of matching rows do not necessarily start from the first row. The
+    the set of matching rows does not necessarily start from the first row. The
     default is <literal>INITIAL</literal>. Currently
     only <literal>INITIAL</literal> is supported. <literal>DEFINE</literal>
     defines definition variables along with a boolean
     expression. <literal>PATTERN</literal> defines a sequence of rows that
     satisfies certain conditions using variables defined
-    in <literal>DEFINE</literal> clause (an empty <literal>PATTERN()</literal>
+    in the <literal>DEFINE</literal> clause (an empty <literal>PATTERN()</literal>
     is not supported). Each pattern variable can be followed by a quantifier
     to specify how many times it should match:
     <literal>*</literal> (zero or more),
@@ -1198,7 +1198,7 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
 
    <para>
     Note that the maximum number of unique pattern variables
-    used in <literal>PATTERN</literal> clause is 251.
+    used in the <literal>PATTERN</literal> clause is 251.
     If this limit is exceeded, an error will be raised.
     Additionally, the maximum nesting depth of pattern groups
     (parentheses) is 253 levels.
-- 
2.50.1 (Apple Git-155)

From 6a716cd3dc1f808438dba393603ec955cae63e65 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 13:59:29 +0900
Subject: [PATCH] Fix nav_slot pass-by-ref dangling pointer in RPR navigation

When a DEFINE expression contains multiple navigation calls targeting
different positions (e.g., PREV(x,1) > PREV(x,2)), the second call
re-fetches nav_slot, freeing the previous tuple via pfree.  Any
pass-by-ref datum extracted from the first navigation becomes a
dangling pointer.  Fix by copying pass-by-ref results into per-tuple
memory in the RESTORE step.
---
 src/backend/executor/execExpr.c       |  5 ++
 src/backend/executor/execExprInterp.c | 20 +++++++
 src/include/executor/execExpr.h       |  2 +
 src/test/regress/expected/rpr.out     | 80 +++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql          | 34 ++++++++++++
 5 files changed, 141 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 6349a564a98..8d8da67e79f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1304,7 +1304,12 @@ ExecInitExprRec(Expr *node, ExprState *state,
 
 				/* Emit RESTORE opcode: restore original slot */
 				scratch.opcode = EEOP_RPR_NAV_RESTORE;
+				scratch.resvalue = resv;
+				scratch.resnull = resnull;
 				scratch.d.rpr_nav.winstate = winstate;
+				get_typlenbyval(nav->resulttype,
+								&scratch.d.rpr_nav.resulttyplen,
+								&scratch.d.rpr_nav.resulttypbyval);
 				ExprEvalPushStep(state, &scratch);
 				break;
 			}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 2ec579732cc..e2d41c3098f 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -6156,6 +6156,13 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
  * When slot swap was elided (target == currentpos), this is a harmless
  * no-op since saved and current slots are identical.
  * The caller is responsible for updating any local slot cache.
+ *
+ * For pass-by-reference result types, the result datum points into
+ * nav_slot's tuple memory.  If a subsequent navigation in the same
+ * expression re-fetches nav_slot for a different position, the old
+ * tuple is freed, leaving a dangling pointer.  We prevent this by
+ * copying pass-by-ref results into per-tuple memory, which survives
+ * until the next ResetExprContext.
  */
 void
 ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
@@ -6164,4 +6171,17 @@ ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
 	WindowAggState *winstate = op->d.rpr_nav.winstate;
 
 	econtext->ecxt_outertuple = winstate->nav_saved_outertuple;
+
+	/* Stabilize pass-by-ref result against nav_slot re-fetch */
+	if (!op->d.rpr_nav.resulttypbyval &&
+		!*op->resnull)
+	{
+		MemoryContext oldContext;
+
+		oldContext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
+		*op->resvalue = datumCopy(*op->resvalue,
+								  false,
+								  op->d.rpr_nav.resulttyplen);
+		MemoryContextSwitchTo(oldContext);
+	}
 }
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 834800a4062..e6b2ab30406 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -703,6 +703,8 @@ typedef struct ExprEvalStep
 			Datum	   *offset_value;	/* offset value(s), or NULL */
 			bool	   *offset_isnull;	/* offset null flag(s) */
 			/* For compound nav: offset_value[0] = inner, [1] = outer */
+			int16		resulttyplen;	/* RESTORE: result type length */
+			bool		resulttypbyval; /* RESTORE: result pass-by-value? */
 		}			rpr_nav;
 
 		/* for EEOP_AGG_*DESERIALIZE */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 04ec25d4cf5..32aa8bc3722 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1635,6 +1635,86 @@ WINDOW w AS (
  company2 | 07-10-2023 |  1300 |             |            |     0
 (20 rows)
 
+-- Pass-by-ref types: two PREV calls targeting different positions.
+-- Verifies that datumCopy in RESTORE prevents dangling pointers when
+-- nav_slot is re-fetched for the second navigation.
+-- tdate::text gives distinct text values per row (e.g. '07-01-2023').
+-- B matches when 1-back date text > 2-back date text (always true for
+-- ascending dates), so B+ extends the full partition after A.
+SELECT company, tdate, tdate::text AS tdate_text,
+       first_value(tdate::text) OVER w, last_value(tdate::text) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS PREV(tdate::text, 1) > PREV(tdate::text, 2)
+);
+ company  |   tdate    | tdate_text | first_value | last_value | count 
+----------+------------+------------+-------------+------------+-------
+ company1 | 07-01-2023 | 07-01-2023 |             |            |     0
+ company1 | 07-02-2023 | 07-02-2023 | 07-02-2023  | 07-10-2023 |     9
+ company1 | 07-03-2023 | 07-03-2023 |             |            |     0
+ company1 | 07-04-2023 | 07-04-2023 |             |            |     0
+ company1 | 07-05-2023 | 07-05-2023 |             |            |     0
+ company1 | 07-06-2023 | 07-06-2023 |             |            |     0
+ company1 | 07-07-2023 | 07-07-2023 |             |            |     0
+ company1 | 07-08-2023 | 07-08-2023 |             |            |     0
+ company1 | 07-09-2023 | 07-09-2023 |             |            |     0
+ company1 | 07-10-2023 | 07-10-2023 |             |            |     0
+ company2 | 07-01-2023 | 07-01-2023 |             |            |     0
+ company2 | 07-02-2023 | 07-02-2023 | 07-02-2023  | 07-10-2023 |     9
+ company2 | 07-03-2023 | 07-03-2023 |             |            |     0
+ company2 | 07-04-2023 | 07-04-2023 |             |            |     0
+ company2 | 07-05-2023 | 07-05-2023 |             |            |     0
+ company2 | 07-06-2023 | 07-06-2023 |             |            |     0
+ company2 | 07-07-2023 | 07-07-2023 |             |            |     0
+ company2 | 07-08-2023 | 07-08-2023 |             |            |     0
+ company2 | 07-09-2023 | 07-09-2023 |             |            |     0
+ company2 | 07-10-2023 | 07-10-2023 |             |            |     0
+(20 rows)
+
+-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2)
+-- B matches when price 1-back > price 2-back (ascending pair).
+SELECT company, tdate, price::numeric AS nprice,
+       first_value(price::numeric) OVER w, last_value(price::numeric) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS PREV(price::numeric, 1) > PREV(price::numeric, 2)
+);
+ company  |   tdate    | nprice | first_value | last_value | count 
+----------+------------+--------+-------------+------------+-------
+ company1 | 07-01-2023 |    100 |             |            |     0
+ company1 | 07-02-2023 |    200 |         200 |        150 |     2
+ company1 | 07-03-2023 |    150 |             |            |     0
+ company1 | 07-04-2023 |    140 |             |            |     0
+ company1 | 07-05-2023 |    150 |         150 |         90 |     2
+ company1 | 07-06-2023 |     90 |             |            |     0
+ company1 | 07-07-2023 |    110 |         110 |        120 |     3
+ company1 | 07-08-2023 |    130 |             |            |     0
+ company1 | 07-09-2023 |    120 |             |            |     0
+ company1 | 07-10-2023 |    130 |             |            |     0
+ company2 | 07-01-2023 |     50 |             |            |     0
+ company2 | 07-02-2023 |   2000 |        2000 |       1500 |     2
+ company2 | 07-03-2023 |   1500 |             |            |     0
+ company2 | 07-04-2023 |   1400 |             |            |     0
+ company2 | 07-05-2023 |   1500 |        1500 |         60 |     2
+ company2 | 07-06-2023 |     60 |             |            |     0
+ company2 | 07-07-2023 |   1100 |        1100 |       1200 |     3
+ company2 | 07-08-2023 |   1300 |             |            |     0
+ company2 | 07-09-2023 |   1200 |             |            |     0
+ company2 | 07-10-2023 |   1300 |             |            |     0
+(20 rows)
+
 --
 -- FIRST/LAST navigation
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index a05b429ce74..724d460b2da 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -776,6 +776,40 @@ WINDOW w AS (
     DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
 );
 
+-- Pass-by-ref types: two PREV calls targeting different positions.
+-- Verifies that datumCopy in RESTORE prevents dangling pointers when
+-- nav_slot is re-fetched for the second navigation.
+-- tdate::text gives distinct text values per row (e.g. '07-01-2023').
+-- B matches when 1-back date text > 2-back date text (always true for
+-- ascending dates), so B+ extends the full partition after A.
+SELECT company, tdate, tdate::text AS tdate_text,
+       first_value(tdate::text) OVER w, last_value(tdate::text) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS PREV(tdate::text, 1) > PREV(tdate::text, 2)
+);
+
+-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2)
+-- B matches when price 1-back > price 2-back (ascending pair).
+SELECT company, tdate, price::numeric AS nprice,
+       first_value(price::numeric) OVER w, last_value(price::numeric) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+    PARTITION BY company ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS PREV(price::numeric, 1) > PREV(price::numeric, 2)
+);
+
 --
 -- FIRST/LAST navigation
 --
-- 
2.50.1 (Apple Git-155)

From d09908f16025319da7771c40892a3a66df5fdb7e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 20:49:27 +0900
Subject: [PATCH] Add inline comments for complex RPR algorithms and design
 notes

Document END chain traversal in nfa_match(), fast-forward paths
in nfa_advance_end(), absorption safety rules with navigation
lookup table, per-context evaluation strategy table, fixed-length
group unrolling rationale, and BEGIN/END pointer layout diagram.
---
 src/backend/executor/execRPR.c   | 97 ++++++++++++++++++++++++++------
 src/backend/optimizer/plan/rpr.c | 41 ++++++++++++--
 2 files changed, 118 insertions(+), 20 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index dede2dfab0d..037d3b2e232 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -440,7 +440,14 @@
  *   Case 2: GROUP+ with fixed-length children (min == max, recursively)
  *           e.g., (A B)+, (A B{2})+, ((A (B C){2}){2})+
  *           -> ABSORBABLE_BRANCH on all elements within the group,
- *             ABSORBABLE | ABSORBABLE_BRANCH on END
+ *              ABSORBABLE | ABSORBABLE_BRANCH on END
+ *
+ *           Why this is safe: when every child has min == max, the group
+ *           is semantically equivalent to unrolling its body into {1,1}
+ *           elements.  E.g., (A B{2})+ behaves like (A B B)+.  Each
+ *           iteration consumes a fixed number of rows, so an earlier
+ *           context's count always dominates a later one's (monotonicity).
+ *
  *   Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
  *           -> Recurses from BEGIN into the body, applying Case 1.
  *             ABSORBABLE | ABSORBABLE_BRANCH set on A.
@@ -647,6 +654,19 @@
  * variables.  The original nav_match_start and currentpos are saved and
  * restored after re-evaluation.
  *
+ * Summary of evaluation strategy by navigation content:
+ *
+ *   Navigation content               evaluation
+ *   -------------------------------------------------------
+ *   No navigation                    shared (once per row)
+ *   PREV/NEXT only                   shared (once per row)
+ *   LAST (no offset)                 shared (once per row)
+ *   LAST (with offset)               per-context
+ *   FIRST (any)                      per-context
+ *   Compound (inner FIRST)           per-context
+ *   Compound (inner LAST, no off.)   shared (once per row)
+ *   Compound (inner LAST, w/off.)    per-context
+ *
  * VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
  *
  * Navigation functions require access to past rows via the tuplestore.
@@ -762,11 +782,26 @@
  *   (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
  *       FOLLOWING).  Limited frames apply differently to each context,
  *       breaking the monotonicity principle.
- *   (c) No match_start-dependent navigation in DEFINE.  FIRST,
- *       LAST-with-offset, and compound navigation referencing match_start
- *       (PREV_FIRST, NEXT_FIRST, PREV_LAST/NEXT_LAST with offset)
- *       cause different contexts to evaluate to different values for the
- *       same row, breaking monotonicity.
+ *   (c) No match_start-dependent navigation in DEFINE.
+ *
+ *       Mechanism: each context has a different matchStartRow, so FIRST
+ *       resolves to a different row for each context at the same
+ *       currentpos.  An earlier context's DEFINE result no longer
+ *       subsumes a later one's, making count-dominance comparison
+ *       invalid.  Rather than comparing matchStartRow at runtime
+ *       (which would complicate the absorb path), any match_start
+ *       dependency disables absorption entirely.
+ *
+ *       Navigation content              match_start dep.  absorption
+ *       ------------------------------------------------------------
+ *       No navigation                   none              safe
+ *       PREV/NEXT only                  none              safe
+ *       LAST (no offset)                none              safe
+ *       LAST (with offset)              boundary check    unsafe
+ *       FIRST (any)                     direct            unsafe
+ *       Compound (inner FIRST)          direct            unsafe
+ *       Compound (inner LAST, no off.)  none              safe
+ *       Compound (inner LAST, w/off.)   boundary chk      unsafe
  *
  * Runtime conditions (evaluated per context pair):
  *
@@ -2260,7 +2295,13 @@ nfa_absorb_contexts(WindowAggState *winstate)
  * nfa_eval_var_match
  *
  * Evaluate if a VAR element matches the current row.
- * Undefined variables (varId >= defineVariableList length) default to TRUE.
+ *
+ * varMatched is a pre-evaluated boolean array indexed by varId, computed
+ * once per row by evaluating all DEFINE expressions.  NULL means no DEFINE
+ * clauses exist (only possible during early development/testing).
+ *
+ * Per SQL:2016 R020, pattern variables not listed in DEFINE are implicitly
+ * TRUE -- they match every row.  This is checked via varId >= list_length.
  */
 static bool
 nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
@@ -2337,9 +2378,20 @@ 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 judgment point.  Only
 				 * deterministic exits (count >= max, max finite) are handled;
 				 * unbounded VARs stay for advance phase.
+				 *
+				 * In nested patterns like ((A B){2}){3}, a VAR reaching its
+				 * max triggers an exit cascade: inner END increments inner
+				 * group count, which may itself reach max, requiring an exit
+				 * 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).
 				 */
 				if (RPRElemIsAbsorbableBranch(elem) &&
 					!RPRElemIsAbsorbable(elem) &&
@@ -2561,12 +2613,25 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		RPRPatternElement *jumpElem;
 		RPRNFAState *ffState = NULL;
 
-		/* Snapshot state for ff path before modifying for loop-back */
+		/*
+		 * Two paths are explored in parallel when the group body is
+		 * nullable (RPR_ELEM_EMPTY_LOOP):
+		 *
+		 * 1. Primary path: loop back and attempt real matches in the
+		 *    next iteration (state, modified below).
+		 *
+		 * 2. Fast-forward path: skip directly to after the group,
+		 *    treating all remaining required iterations as empty
+		 *    matches (ffState, handled after the primary path).
+		 *
+		 * The snapshot must be taken BEFORE modifying state for the
+		 * loop-back, since both paths diverge from the same point.
+		 */
 		if (RPRElemCanEmptyLoop(elem))
 			ffState = nfa_state_create(winstate, state->elemIdx,
 									   state->counts, state->isAbsorbable);
 
-		/* Loop back for real matches (primary path) */
+		/* Primary path: loop back for real matches */
 		for (int d = depth + 1; d < pattern->maxDepth; d++)
 			state->counts[d] = 0;
 		state->elemIdx = elem->jump;
@@ -2575,12 +2640,12 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 						  currentPos);
 
 		/*
-		 * Fast-forward fallback for nullable bodies.  E.g. (A?){2,3} when A
-		 * doesn't match: the loop-back produces empty iterations that cycle
-		 * detection would kill.  Instead, exit directly treating all
-		 * remaining required iterations as empty.  Route to elem->next (not
-		 * nfa_advance_end) to avoid creating competing greedy/reluctant loop
-		 * states.
+		 * Fast-forward path for nullable bodies.  E.g. (A?){2,3} when
+		 * A doesn't match: the primary loop-back produces empty
+		 * iterations that cycle detection would kill.  Instead, exit
+		 * directly with count satisfied.  Route to elem->next (not
+		 * nfa_advance_end) to avoid creating competing greedy/reluctant
+		 * loop states.
 		 */
 		if (ffState != NULL)
 		{
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 767a214016c..754fcd53099 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -478,6 +478,19 @@ mergeConsecutiveAlts(List *children)
  * mergeGroupPrefixSuffix
  *		Merge sequence prefix/suffix into GROUP with matching children.
  *
+ * When a GROUP's children appear as a prefix before and/or suffix after
+ * the GROUP in a SEQ, absorb them by incrementing the GROUP's quantifier.
+ * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
+ *
+ * Algorithm:
+ *   For each GROUP encountered in the sequence:
+ *   1. PREFIX phase: compare the last N elements already in the result
+ *      list against the GROUP's children.  On match, remove them from
+ *      result and increment the GROUP's min/max.  Repeat until no match.
+ *   2. SUFFIX phase: compare the next N elements in the input against
+ *      the GROUP's children.  On match, skip them (via skipUntil) and
+ *      increment min/max.  Repeat until no match.
+ *
  * Examples:
  *   A B (A B)+ -> (A B){2,}
  *   (A B)+ A B -> (A B){2,}
@@ -813,8 +826,16 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 	}
 
 	/*
-	 * Case 2/3: Safe when child is finite AND (outer is exact OR child is
-	 * {1,1})
+	 * Case 2: Outer exact (min == max): (A{2,3}){4} -> A{8,12}.
+	 *         Safe because every iteration produces the same range.
+	 *
+	 * Case 3: Child {1,1}: (A){2,5} -> A{2,5}.
+	 *         Safe because the child contributes exactly one per
+	 *         iteration, so the outer range maps directly.
+	 *
+	 * Unsafe example: (A{2}){2,3} produces counts 4 or 6 only, not
+	 * the full range 4..6, so we cannot flatten when child has a
+	 * non-trivial range AND outer is also a range.
 	 */
 	if (child->max != RPR_QUANTITY_INF &&
 		(pattern->min == pattern->max ||
@@ -824,6 +845,7 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 		if (new_min_64 >= RPR_QUANTITY_INF)
 			return pattern;
 
+		/* Outer unbounded: result is unbounded regardless of child */
 		if (pattern->max == RPR_QUANTITY_INF)
 			new_max_64 = RPR_QUANTITY_INF;
 		else
@@ -1186,8 +1208,19 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
  * fillRPRPatternGroup
  *		Fill a GROUP pattern and its children.
  *
- * Creates elements for group content at increased depth, plus an END marker
- * if the group has a non-trivial quantifier.
+ * Creates elements for group content at increased depth, plus BEGIN/END
+ * marker pair if the group has a non-trivial quantifier (not {1,1}).
+ *
+ * Element layout for (A B){2,3}:
+ *
+ *   [BEGIN]  [A]  [B]  [END]  [next element...]
+ *     |                  |          ^
+ *     |                  +-- jump --+ (loop back to first child)
+ *     +---- jump -------------------+ (skip to after END)
+ *
+ * BEGIN.jump points past END (skip path when count >= max or min == 0).
+ * END.jump points to the first child (loop-back path).
+ * BEGIN.next and END.next are set later by finalizeRPRPattern().
  *
  * Returns true if this group is nullable.  A group is nullable when its
  * min is 0 (can be skipped entirely) or its body is nullable (every path
-- 
2.50.1 (Apple Git-155)

From 29e8aaa01bb321da9eb2228832cc259725ac7841 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 20:52:13 +0900
Subject: [PATCH] Remove unused include and fix header ordering in RPR files

---
 src/backend/executor/execExprInterp.c | 2 +-
 src/backend/executor/nodeWindowAgg.c  | 3 +--
 src/backend/parser/parse_rpr.c        | 3 +--
 3 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index e2d41c3098f..58b6693ed75 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -56,8 +56,8 @@
  */
 #include "postgres.h"
 
-#include "common/int.h"
index 849ebf8abb0..02f17e5472c 100644
@@ -34,10 +34,9 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
-#include "common/int.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_aggregate.h"
-#include "catalog/pg_collation_d.h"
+#include "common/int.h"
 #include "catalog/pg_proc.h"
 #include "executor/executor.h"
 #include "executor/execRPR.h"
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 8fbe12e1518..8864b20e6cf 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -30,9 +30,8 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
 #include "optimizer/rpr.h"
-#include "parser/parse_clause.h"
-#include "parser/parse_collate.h"
 #include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
 #include "parser/parse_rpr.h"
 #include "parser/parse_target.h"
-- 
2.50.1 (Apple Git-155)

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 14 Apr 2026 07:52:25 +0900 (JST)

Hi Henson,

> your guidance and support along the way.

I had no idea too :-) I was struggled with RPR develpment issues,
especially on an NFA engine and navigation operations (PREV/NEXT
etc.). You have solved both of the issues.

>   0001-0007: Unchanged from the previous set.

Here are the review for 0001-0007. I will continue to review rest of
the patches.

0001 was already reviewed (LGTM).
0002-0003 looks good to me.
0004 was already reviewed (LGTM).
0005 thanks for review.
0006

As "RPR" already appears in the prevous comment block above, the "RPR"
appearing in the fist comment block should be "RPR (Row Pattern
Recognition)" and "RPR" here should be without "(Row Pattern
Recognition)".

0007:

For these 2 lines I feel a little bit hard to parse. Probably better
to be written as complete sentences, rather than bullet points. For
example,

+-- Non-RPR case: frame can be optimized (RANGE -> ROWS conversion, etc.)
+-- RPR case: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)

Better to have a blanck line here for readability.

Better to comment what we want to test (or expect).

Better to comment what we want to test (or expect).

Better to comment what we want to test (or expect).

Better to comment what we want to test (or expect).

No incremental sort in this plan?

Can you elaborate how the test below confirm random() is not optimized
away?

0008 and beyond will be reviewd in separate emails.

Ok.

> shift from adding features to raising overall

Agreed.

> If there are directions you think should be explored, I am happy to
> work on them. Otherwise, I plan to focus on code review, test
> coverage, and stabilization, and will continue to submit bug fixes
> and test additions as I find them.

One thing I want to discuss is, how to deal with the cases when
volatile functions are used in the DEFINE clause. As far as I know,
the SQL standard does not prohibit to use volatile functions there,
but we may need to prohibit to use it for our implementation reasons.

For reference, I studied how non RPR part in WindowAggs.c handles
volatile functions.

1. When searching for identical window functions, if they are volatile
functions, they are considered different window functions.

2. When evaluating the frame offset expression, it is evaluated only once
each time a scan/rescan occurs.

3. When checking whether moving aggregation is available, the
volatility of the window function is checked. If it is volatile,
moving aggregation is disabled.

> address it now than leave it for after commit.

Ok, I will let you know when I find anything.

> about which interactions to try. Does that seem like a

Sounds like a good direction.

> fixed -- at any time -- please let me know.

Sure, I will.

Regards,

---

From: Tatsuo Ishii <[email-masked]>
Date: Fri, 17 Apr 2026 17:10:22 +0900 (JST)

Here is a review for 0008.

The patch does not apply using git apply.
$ git apply /usr/local/src/pgsql/RPR/make_rpr_patches/v47/input_patches/nocfbot-0008-Replace-reduced-frame-map-with-single-match-result.txt 
error: patch failed: src/backend/executor/nodeWindowAgg.c:247
error: src/backend/executor/nodeWindowAgg.c: patch does not apply
error: patch failed: src/include/nodes/execnodes.h:2694
error: src/include/nodes/execnodes.h: patch does not apply

I applied it using patch command.

$ patch -p1 < /usr/local/src/pgsql/RPR/make_rpr_patches/v47/input_patches/nocfbot-0008-Replace-reduced-frame-map-with-single-match-result.txt 
patching file src/backend/executor/execRPR.c
patching file src/backend/executor/nodeWindowAgg.c
Hunk #1 succeeded at 235 with fuzz 2 (offset -12 lines).
Hunk #2 succeeded at 1017 (offset -15 lines).
Hunk #3 succeeded at 1041 (offset -15 lines).
Hunk #4 succeeded at 1060 (offset -15 lines).
Hunk #5 succeeded at 1340 (offset 4 lines).
Hunk #6 succeeded at 1571 (offset 11 lines).
Hunk #7 succeeded at 2355 (offset 11 lines).
Hunk #8 succeeded at 2464 (offset 11 lines).
Hunk #9 succeeded at 2991 (offset 33 lines).
Hunk #10 succeeded at 3600 (offset -37 lines).
Hunk #11 succeeded at 3900 (offset -37 lines).
Hunk #12 succeeded at 3914 (offset -37 lines).
Hunk #13 succeeded at 3925 (offset -37 lines).
Hunk #14 succeeded at 3938 (offset -37 lines).
Hunk #15 succeeded at 3948 (offset -37 lines).
Hunk #16 succeeded at 4044 (offset -37 lines).
Hunk #17 succeeded at 4065 (offset -37 lines).
Hunk #18 succeeded at 4141 (offset -37 lines).
Hunk #19 succeeded at 4657 (offset -35 lines).
patching file src/include/nodes/execnodes.h
Hunk #2 succeeded at 2698 with fuzz 2 (offset 2 lines).
patching file src/test/regress/expected/rpr_explain.out

From: Henson Choi <[email-masked]>
Subject: Re: Row pattern recognition
Date: Sun, 12 Apr 2026 16:27:26 +0900
Message-ID: <CAAAe_zB7rAEJtT6hXgF85=[email-masked]>

> at a time, replace it with four scalar fields: valid, matched,

You are right, we don't need to have the map as an array.

> from unmatched rows via RF_EMPTY_MATCH, counted as matched in NFA

Good enhancement.

> since it returns rpr_match_length which is int64.

Good point.

> +++ b/src/backend/executor/execRPR.c

"When valid is true" sounds a little bit confusing for readers. I
think "When rpr_match_valid is true" is cleaner, although it adds more
characters. Same thing can be said to "matchded", "valid" and "length".

> + * row's status against the current match result.

This may be misleading since get_reduced_frame_status() is not an
exposed public API to a window function. What about something like
"Row's status against the current match result can be obtained by
calling get_reduced_frame_status()"?

In execRPR.c, documentation occupies 1445 lines out of 3052 lines. I
don't see any other .[ch] files, half of it is taken up by
documentation. I suggest to create something like "README.rpr" and
move the documentation part to it.

> +++ b/src/backend/executor/nodeWindowAgg.c

No comment to nodeWindowAgg.c patch. This part looks good to me.

Regards,

---

From: Tatsuo Ishii <[email-masked]>
Date: Fri, 17 Apr 2026 19:13:03 +0900 (JST)

Hi Henson,

Note that applying 0001-0006 produced compiler warning.

execExprInterp.c: In function ‘ExecEvalRPRNavSet’:
execExprInterp.c:6005:23: warning: ‘target_pos’ may be used uninitialized [-Wmaybe-uninitialized]
 6005 |         target_slot = ExecRPRNavGetSlot(winstate, target_pos);
      |                       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
execExprInterp.c:5959:25: note: ‘target_pos’ was declared here
 5959 |         int64           target_pos;
      |                         ^~~~~~~~~~

But regression test was ok.
However, after applying 0007, regression test failed.

not ok 196   + rpr_integration                           514 ms
# (test process exited with exit code 2)
# parallel group (9 tests):  jsonpath jsonpath_encoding jsonb_jsonpath sqljson_queryfuncs json_encoding json sqljson_jsontable jsonb sqljson
not ok 197   + json                                       29 ms
# (test process exited with exit code 2)
not ok 198   + jsonb                                      29 ms
# (test process exited with exit code 2)
[snip]

From rpr_integration.out:

-- Verify both produce different results
SELECT
    id, val,
    count(*) OVER w_rpr AS rpr_cnt,
    count(*) OVER w_normal AS normal_cnt
FROM rpr_integ
WINDOW
    w_rpr AS (ORDER BY id
        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
        PATTERN (A B+)
        DEFINE B AS val > PREV(val)),
    w_normal AS (ORDER BY id
        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
ORDER BY id;
server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
connection to server was lost

Regards,

---

From: Henson Choi <[email-masked]>
Date: Fri, 17 Apr 2026 21:08:12 +0900

Hi Tatsuo,

Thank you for the feedback.

>  6005 |         target_slot = ExecRPRNavGetSlot(winstate, target_pos);

`ExecRPRNavGetSlot()`, along with the enclosing `ExecEvalRPRNavSet()`
and its `int64 target_pos` declaration, is introduced by nocfbot-0017
(Implement 1-slot PREV/NEXT navigation for RPR), not at the 0001-0006
level.  Could you double-check the actual applied range on your side?
The function simply does not exist at 0001-0006.

Since the function is not present at 0001-0006, I suspect the
applied patch set on your side is in a different state than
expected.  Could you verify the range and contents of the patches
actually applied?

> # (test process exited with exit code 2)

nocfbot-0006 was written specifically to prevent the crashes that
0007's tests expose, so with 0001-0007 applied in order the issue
should already be resolved.  On my authoritative base (cfbot/4460) all
rpr_integration tests pass through 0008.  I had been routinely
building only on macOS ARM64 recently, so I just re-verified on
Rocky 9 and Ubuntu 24 AMD64 as well; no build warning and no
rpr_integration failure on either.

For reference, here is my working tree layout.  Titles only for my
post-base commits (hashes on my side won't match yours); base
hashes are public and directly comparable on your side:

  [post-base, newest first]
   31. Remove unused include and fix header ordering in RPR files
(origin/RPR)
   30. Add inline comments for complex RPR algorithms and design notes
   29. Fix nav_slot pass-by-ref dangling pointer in RPR navigation
   28. Fix RPR documentation: synopsis, grammar, and terminology
   27. Fix comment typos, grammar, and inaccuracies in RPR code
   26. Fix RPR error message style: hint format, terminology, capitalization
   25. Guard against int64 overflow in RPR bounded frame end computation
   24. Implement FIRST/LAST and compound navigation for RPR
   23. Add Nav Mark Lookback to EXPLAIN and fix compute_nav_max_offset()
   22. Add 2-arg PREV/NEXT test for row pattern navigation with host
variable
   21. Enable JIT compilation for PREV/NEXT navigation tests in RPR
   20. Update RPR code comments to reflect 1-slot navigation model
   19. Add tuplestore trim optimization for RPR PREV navigation
   18. Add JIT compilation support for RPR PREV/NEXT navigation
   17. Implement 1-slot PREV/NEXT navigation for RPR
   16. Normalize RPR element flag macros to return bool
   15. Narrow variable scope in ExecInitWindowAgg DEFINE clause loop
   14. Add CHECK_FOR_INTERRUPTS to RPR context cleanup and finalize loops
   13. Remove unused force_colno parameter from RPR deparse functions
   12. Fix execRPR.o ordering in executor Makefile to match meson.build
   11. Fix quote_identifier() for RPR pattern variable name deparse
   10. Rename rpr_explain test views from sequential numbers to descriptive
names
    9. Add fixed-length group absorption for RPR
    8. Replace reduced frame map with single match result
    7. Add RPR planner integration tests
    6. Fix DEFINE expression handling in RPR window planning
    5. Fix mark handling for last_value() under RPR
    4. Fix in-place modification of defineClause TargetEntry in setrefs.c
    3. Add CHECK_FOR_INTERRUPTS() to nfa_try_absorb_context() loop
    2. Add CHECK_FOR_INTERRUPTS() to nfa_add_state_unique() for state
explosion patterns
    1. Remove unused regex/regex.h include from nodeWindowAgg.c

  [base, public hashes]
  732acf9b7c6 (cfbot/cf/4460) [CF 4460] v46 - Implement row pattern
recognition feature
  6eb8841afaf Row pattern recognition patch (typedefs.list).
  5213dbb3c4f Row pattern recognition patch (tests: expected).
  bdc3a17982b Row pattern recognition patch (tests: SQL, data).
  596bcc434b5 Row pattern recognition patch (docs).
  e02872fd59c Row pattern recognition patch (executor and commands).
  742c6eb673e Row pattern recognition patch (planner).
  8514ca64f2d Row pattern recognition patch (rewriter).
  d26741d4f1d Row pattern recognition patch (parse/analysis).
  f85d2ee8f4a Row pattern recognition patch for raw parser.
  e484b0eea61 (origin/RPR-base, RPR-base) Fix two issues in fast-path FK
check introduced by commit 2da86c1ef9

Could you send me the equivalent `git log` for your working tree
covering the full v46 application and the commit immediately
preceding it?  In particular, could you also check whether the
recent master commit titled "Fix integer overflow in
nodeWindowAgg.c" (backpatched to 14) is included in your base?  It
touches the same ROWS/GROUPS frame boundary code path that RPR
interacts with, so its presence or absence will help narrow things
down.  Once I can match the same base locally, I will reproduce and
include the fix in the next revision.

A quick status update:

  - nocfbot-0006: self-review complete, not yet sent (comment tweak per
review)
  - nocfbot-0007: under self-review (comment details per review)
  - nocfbot-0008: in progress

I would like to resolve the crash first before sending the next
revision, so that the full set goes out together once our bases are
aligned and A3 passes on your side.

One process question: so far I have been folding review feedback
into the relevant existing patches (i.e. amending nocfbot-0008 to
reflect your comments on it).  Would you prefer that I instead
append the review fixes as new patches at the end of the series
(e.g. nocfbot-0032, 0033, ...), so the history of reviewed changes
stays visible?  Happy to follow whichever workflow you find easier
to review.

Regards,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Fri, 17 Apr 2026 22:47:08 +0900 (JST)

Hi Henson,

> rpr_integration failure on either.

You are right. I checked and found the 0006 patch I applied was wrong
one.

nocfbot-0006-Implement-1-slot-PREV-NEXT-navigation-for-RPR.txt:

This should have been:
nocfbot-0006-Fix-DEFINE-expression-handling-RPR-window-planning.txt

Sorry for confusion.

> preceding it?

Here it is:

t-ishii$ git log --oneline -10|cat
6dd0765463a Row pattern recognition patch (typedefs.list).
009b38f4d2b Row pattern recognition patch (tests: expected).
07864e40e36 Row pattern recognition patch (tests: SQL, data).
55b467849eb Row pattern recognition patch (docs).
28a1ff7e744 Row pattern recognition patch (executor and commands).
728b1fdf106 Row pattern recognition patch (planner).
c388e3e7eb0 Row pattern recognition patch (rewriter).
232725a3e95 Row pattern recognition patch (parse/analysis).
c71940f9e21 Row pattern recognition patch for raw parser.
322bab79744 Move declarations related to locktags from lock.h to new locktag.h

> In particular, could you also check whether the

No, the commit is not in my working tree.

So I guess I should have rebased v46 tree so that the commit is in the
work tree before applying your patches. Am I correct?

> aligned and A3 passes on your side.

Let me check the crash first. Since apparently the crash was caused by
my mis operation.

> reflect your comments on it).

I prefer this way.

> Would you prefer that I instead

Regards,

---

From: Henson Choi <[email-masked]>
Date: Fri, 17 Apr 2026 23:24:37 +0900

Hi Tatsuo,

> Sorry for confusion.

No worries. Having 31 patches in the series is bound to cause
confusion — that is on me for sending such a large set at once.

> 322bab79744 Move declarations related to locktags from lock.h to new

Thank you for the log. I can see that your base is the original v46
application, while mine is the cfbot rebase from April 1
(732acf9b7c6).

> work tree before applying your patches. Am I correct?

Since the root cause was the patch file mix-up, a rebase would not
have been necessary in this case. That said, the related fix that
went into master ("Fix integer overflow in nodeWindowAgg.c") touches
the same file as several of my patches, so it may cause conflicts
when applying v47. Please let me know if anything is difficult to
resolve.

> my mis operation.

Once confirmed, I will send the corrected patches first.

Regarding the README.rpr suggestion from the 0008 review: the
documentation in execRPR.c has dependencies spread across the patch
series, so separating it mid-review would be disruptive. I plan to
split it out as part of the final patch list once all 31 patches have
been reviewed.

I prefer this way.

Good — I will continue folding review feedback into the relevant
existing patches. Patch numbers and subjects will stay stable across
revisions.

Regards,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Sun, 19 Apr 2026 19:18:01 +0900 (JST)

> when applying v47. Please let me know if anything is difficult to

Ok, I will not rebase current v46 and continue to apply your
incremental patches and review them, until we agree to ship v47.  I
may see conflicts while creating v47 patch sets. I will ask help if if
your assistance needed. Thanks in advance.

> Once confirmed, I will send the corrected patches first.

I confirmed 0001-0008 applied cleanly and see no compile
warning. Regression test passed, no crash.

Ok.

> I prefer this way.
> 
> 
> Good ― I will continue folding review feedback into the relevant

Looking forward to seeing revised incremental patch sets.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Sun, 19 Apr 2026 22:52:26 +0900

Hi Tatsuo,

Thank you for the careful review and for confirming the build on
your side.

> your assistance needed. Thanks in advance.

Understood.  Please let me know whenever a conflict comes up while
assembling v47 and I will help resolve it on my side.

> warning. Regression test passed, no crash.

Thank you for confirming.

> > split it out as part of the final patch list once all 31 patches have

Thank you.  I will prepare the README.rpr split as a follow-up patch
after the current 0031 once the series review is complete.

Attached is the revised v47 series, with the review comments on
0006, 0007, and 0008 incorporated.  Numbering and subjects are
unchanged from the previous send.

Since the edits in 0006-0008 propagate into the later patches --
mostly as context-line shifts, and in 0017/0023 as updates to the
rpr_integration expected output -- I have attached the full
0001-0031 set rather than only the updated patches.  Applying this
set on top of v46 should give the same final tree as before, just
with the review adjustments folded in.

Summary of changes from the previous v47:

  - 0006 (Fix DEFINE expression handling): per your comment, the
    first occurrence of "RPR" now carries the "(Row Pattern
    Recognition)" expansion and the later occurrence is the bare
    abbreviation.

  - 0007 (Add RPR planner integration tests): overall
    strengthening of the tests and their comments.  Each block
    now carries an intent comment describing what the block is
    trying to cover and what the expected plan or result is,
    the "Non-RPR / RPR" comment pair is reworded as complete
    sentences, and B8 is reworked so that the EXPLAIN plan
    actually exercises Incremental Sort.

    Because some blocks cover features that are only
    introduced later in the series, a few tests in 0007
    temporarily produce error output.  Concretely, B4
    (RPR + prepared statements) uses the 2-argument form
    PREV(val, $1), which is not yet available at the 0007
    point: it is introduced in 0017 (1-slot PREV/NEXT
    navigation), and the accompanying "Nav Mark Lookback"
    EXPLAIN property referenced in the B4 comments is added
    in 0023.  The rpr_integration expected-output file is
    updated by 0017 (to clear the PREV/prepared-statement
    errors) and then again by 0023 (to add the
    "Nav Mark Lookback" lines to the EXPLAIN output), so that
    by the end of the series the output is clean.

    XXX notes have been added in three places where a test
    records behaviour that may be revisited later:

      (1) The subquery-output test documents why the column
          guard in allpaths.c has to be conservative: a column
          referenced only by DEFINE is indistinguishable from
          an exposed column at the targetlist level, so
          remove_unused_subquery_outputs() cannot apply its
          NULL-replacement selectively without risking DEFINE
          evaluation on NULL inputs.  The XXX flags this as a
          candidate for a precise follow-up optimization.

      (2) The B7 Recursive CTE test carries an XXX noting that
          it is unclear whether this case falls under the
          ISO/IEC 9075-2 4.18.5 / 6.17.5 prohibition, and even
          if it does not, whether a query that does trigger
          the prohibition can be constructed at all.  The
          disposition is left to the community.

      (3) The B9 volatile-in-DEFINE test records current
          behaviour using "random() >= 0.0" (structurally
          equivalent to TRUE, so the expected output stays
          deterministic).  The XXX flags that if we decide to
          reject volatile functions in DEFINE, this test must
          be converted into an error-case test.  See the
          dedicated section at the end of this mail.

  - 0008 (Replace reduced frame map with single match result):
      * Comment block X-1 now uses the full field names
        rpr_match_valid / rpr_match_matched / rpr_match_length.
      * get_reduced_frame_status() sentence rephrased in the
        passive voice, as you suggested:
          "Row's status against the current match result can be
           obtained by calling get_reduced_frame_status()."
      * README.rpr split deferred per the above.

  - 0009-0031: no semantic code changes.  Most patches only
    carry context-line shifts from the edits above.  The two
    exceptions are 0017 and 0023, whose rpr_integration.out
    hunks are updated to match the revised B4 test in 0007
    (clearing the temporary errors and adding the
    "Nav Mark Lookback" lines, respectively).

On volatile functions in DEFINE (your 2026-04-13 mail): the SQL
standard itself is relatively narrow here -- the only explicit
restriction it places is that a navigation function's offset must
be a runtime constant.  Volatile expressions in the DEFINE
predicate are not forbidden by the standard.

That said, once we move into the pattern-matching machinery, the
observable semantics of a volatile DEFINE become quite murky.
Depending on the situation, the DEFINE predicate may be evaluated
once per row, or separately per active matching context, or some
combination of both as the NFA explores alternatives.  A user
writing "random() > 0.5" in DEFINE would have a hard time
predicting, let alone relying on, the evaluation count -- and I
have not been able to think of a realistic use case that actually
benefits from this flexibility.

Given the gap between "permitted by the standard" and "meaningful
to the user," my own leaning is toward prohibiting volatile
functions in DEFINE at transformation time, and -- if we go that
way -- also examining whether the new check would make the
existing runtime-constant check on the navigation-function offset
redundant, or whether the two could be folded into a single check
pass.  That said, this is ultimately a policy call, and I would rather defer
the final decision to your judgment as a senior member of the
community: does prohibition seem like the right direction, or
would you prefer to keep the current behaviour in place?

On sequencing, if we do take it up, I would suggest handling it
after the 31-patch set, alongside the README.rpr split as
follow-up work on top of 0031.  Whether it ultimately lands
inside v47 or as a separate piece on top does not need to be
decided right now -- there is still room to discuss it as the
review progresses, and I am happy to adjust either way based on
your direction.

Regards,
Henson

---

---

---

---

 	/*

 
 out_of_frame:

From b0f0184ef082dbd0a4744d08f6b0b7d5366c2733 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 11:55:02 +0900
Subject: [PATCH] Fix DEFINE expression handling in RPR window planning

index f42a2bae14a..470029e42e0 100644
 
+		/*
+		 * If any RPR (Row Pattern Recognition) window clause references this
+		 * column in its DEFINE clause, don't remove it.  The DEFINE
+		 * expression needs these columns in the tuplestore slot for pattern
+		 * matching evaluation, even if the outer query doesn't reference
+		 * them.
+		 */
+		if (IsA(texpr, Var))
+		{
+			Var		   *var = (Var *) texpr;
+			ListCell   *wlc;
+			bool		needed_by_define = false;
+
+			foreach(wlc, subquery->windowClause)
+			{
+				WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+				if (wc->defineClause != NIL)
+				{
+					List	   *vars = pull_var_clause((Node *) wc->defineClause, 0);
+					ListCell   *vlc;
+
+					foreach(vlc, vars)
+					{
+						Var		   *dvar = (Var *) lfirst(vlc);
+
+						if (dvar->varattno == var->varattno)
+						{
+							needed_by_define = true;
+							break;
+						}
+					}
+					list_free(vars);
+					if (needed_by_define)
+						break;
+				}
+			}
+			if (needed_by_define)
+				continue;
+		}
+
+		/*
+		 * If it's a window function referencing a window clause with RPR,
+		 * don't remove it.  Even when the window function result is unused by
+		 * the outer query, the RPR pattern matching (frame reduction via
+		 * DEFINE/PATTERN) must still execute.  Replacing this with NULL would
+		 * leave no active window functions for the WindowClause, causing the
+		 * planner to omit the WindowAgg node entirely.
+		 */
+		if (IsA(texpr, WindowFunc))
+		{
+			WindowFunc *wfunc = (WindowFunc *) texpr;
+			ListCell   *wlc;
+
+			foreach(wlc, subquery->windowClause)
+			{
+				WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+				if (wc->winref == wfunc->winref &&
+					wc->defineClause != NIL)
+				{
+					break;
+				}
+			}
+			if (wlc != NULL)
+				continue;
+		}
+
 		/*
 		 * OK, we don't need it.  Replace the expression with a NULL constant.
 		 * Preserve the exposed type of the expression, in case something
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 55283ab4bbe..db1309ca311 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -28,6 +28,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
 #include "optimizer/rpr.h"
 #include "parser/parse_clause.h"
 #include "parser/parse_collate.h"
@@ -310,9 +311,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	restargets = NIL;
 	foreach(lc, windef->rpCommonSyntax->rpDefs)
 	{
-		TargetEntry *te,
-				   *teDefine;
-		int			defineExprLocation;
+		TargetEntry *teDefine;

 		/*
 
-		/*

-		/* -------------------
 
-		/*

From ee4eee935abcc5d0e089b19e88a81e8ecac52607 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 14:09:53 +0900
Subject: [PATCH] Add RPR planner integration tests

Add rpr_integration.sql covering planner optimization interactions
with RPR windows: frame optimization, run condition pushdown, window
deduplication, unused window removal, inverse transition, cost
estimation, subquery flattening, DEFINE expression propagation, and
LIMIT.  Also covers integration scenarios with partitioned tables,
LATERAL, recursive CTEs, incremental sort, volatile functions, and
correlated subqueries.
---
 src/test/regress/expected/rpr_integration.out | 1449 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/rpr_integration.sql      |  934 +++++++++++
 3 files changed, 2384 insertions(+), 1 deletion(-)
 create mode 100644 src/test/regress/expected/rpr_integration.out
 create mode 100644 src/test/regress/sql/rpr_integration.sql

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
new file mode 100644
index 00000000000..0ee612b74fb
--- /dev/null
+++ b/src/test/regress/expected/rpr_integration.out
@@ -0,0 +1,1449 @@
+-- ============================================================
+-- RPR Integration Tests
+-- Planner optimization interaction tests for Row Pattern Recognition
+-- ============================================================
+--
+-- Verifies that each planner optimization correctly handles RPR windows.
+-- Even if individual optimizations are tested elsewhere, this file
+-- provides a single checkpoint for all planner/RPR interactions.
+--
+-- A. Planner Optimization Protection Tests
+--    A1. Frame optimization bypass
+--    A2. Run condition pushdown bypass
+--    A3. Window dedup prevention (RPR vs non-RPR)
+--    A4. Window dedup prevention (same PATTERN, different DEFINE)
+--    A5. Unused window removal prevention
+--    A6. Inverse transition bypass
+--    A7. Cost estimation RPR awareness
+--    A8. Subquery flattening prevention
+--    A9. DEFINE expression non-propagation
+--    A10. RPR + LIMIT
+--
+-- B. Integration Scenario Tests
+--    B1. RPR + CTE
+--    B2. RPR + JOIN
+--    B3. RPR + Set operations
+--    B4. RPR + Prepared statements
+--    B5. RPR + Partitioned table
+--    B6. RPR + LATERAL
+--    B7. RPR + Recursive CTE
+--    B8. RPR + Incremental sort
+--    B9. RPR + Volatile function in DEFINE
+--    B10. RPR + Correlated subquery
+--
+CREATE TABLE rpr_integ (id INT, val INT);
+INSERT INTO rpr_integ VALUES
+    (1, 10), (2, 20), (3, 15), (4, 25), (5, 5),
+    (6, 30), (7, 35), (8, 20), (9, 40), (10, 45);
+-- ============================================================
+-- A1. Frame optimization bypass
+-- ============================================================
+-- optimize_window_clauses() must not apply frame optimization to RPR windows.
+-- Non-RPR case: frame can be optimized (RANGE -> ROWS conversion, etc.).
+-- RPR case: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND
+-- UNBOUNDED FOLLOWING).
+-- Non-RPR window with default frame -> frame optimization applied
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id);
+            QUERY PLAN             
+-----------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id)
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(5 rows)
+
+-- RPR window -> frame optimization must NOT change the frame
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(6 rows)
+
+-- ============================================================
+-- A2. Run condition pushdown bypass
+-- ============================================================
+-- Verify that find_window_run_conditions() does not push a monotonic
+-- filter down as a Run Condition on RPR windows.  RPR match counts are
+-- determined by pattern matching rather than by a monotonic
+-- accumulation over the frame, so a filter such as "cnt > 0" cannot be
+-- used to stop evaluating the window function early.
+-- Non-RPR baseline: the filter is expected to appear as a Run Condition.
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+) t WHERE cnt > 0;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on t
+   ->  WindowAgg
+         Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Run Condition: (count(*) OVER w > 0)
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(7 rows)
+
+-- RPR case: the filter must appear as a Filter above the WindowAgg,
+-- not as a Run Condition.
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on t
+   Filter: (t.cnt > 0)
+   ->  WindowAgg
+         Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(8 rows)
+
+-- Verify that the RPR query still returns every row whose match count is
+-- greater than zero, confirming the filter is evaluated above the
+-- WindowAgg rather than cutting off pattern matching prematurely.
+SELECT * FROM (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  3 |  15 |   2
+  5 |   5 |   3
+  8 |  20 |   3
+(4 rows)
+
+-- ============================================================
+-- A3. Window dedup prevention (RPR vs non-RPR)
+-- ============================================================
+-- Verify that PostgreSQL does not merge an RPR window with a non-RPR
+-- window even when both share the same ORDER BY and frame
+-- specification.  RPR pattern matching produces results that are
+-- semantically different from a plain frame-based aggregate, so the
+-- two windows must remain as separate WindowAgg nodes.  Inline window
+-- specs are used throughout this section because only inline windows
+-- are subject to the dedup path; distinct named windows are always
+-- kept separate regardless of equivalence.
+-- Non-RPR baseline: two inline windows with identical spec are
+-- deduped by the planner into a single WindowAgg node, confirming
+-- that the dedup path is active for non-RPR windows.
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS cnt,
+    sum(val)  OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS total
+FROM rpr_integ;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ WindowAgg
+   Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(5 rows)
+
+-- An inline RPR window and an inline non-RPR window share the same
+-- ORDER BY and frame but must remain as distinct WindowAgg nodes.
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS rpr_cnt,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt
+FROM rpr_integ;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ WindowAgg
+   Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   ->  WindowAgg
+         Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: id
+               ->  Seq Scan on rpr_integ
+(8 rows)
+
+-- Verify that the two windows return independent counts per row,
+-- confirming they were not merged into a single WindowAgg.
+SELECT
+    id, val,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS rpr_cnt,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt
+FROM rpr_integ
+ORDER BY id;
+ id | val | rpr_cnt | normal_cnt 
+----+-----+---------+------------
+  1 |  10 |       2 |         10
+  2 |  20 |       0 |          9
+  3 |  15 |       2 |          8
+  4 |  25 |       0 |          7
+  5 |   5 |       3 |          6
+  6 |  30 |       0 |          5
+  7 |  35 |       0 |          4
+  8 |  20 |       3 |          3
+  9 |  40 |       0 |          2
+ 10 |  45 |       0 |          1
+(10 rows)
+
+-- ============================================================
+-- A4. Window dedup prevention (same PATTERN, different DEFINE)
+-- ============================================================
+-- Verify that inline-window dedup does not merge two RPR windows
+-- that share the same PATTERN structure but have different DEFINE
+-- conditions.  Even though the ORDER BY, frame, and PATTERN coincide,
+-- the differing DEFINE expressions classify rows differently and
+-- 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.
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS cnt,
+    sum(val)  OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS total
+FROM rpr_integ;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ WindowAgg
+   Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(6 rows)
+
+-- Two inline RPR windows with the same PATTERN but opposite DEFINE
+-- conditions must remain as separate WindowAgg nodes.
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS cnt_up,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val < PREV(val)) AS cnt_down
+FROM rpr_integ;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ WindowAgg
+   Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  WindowAgg
+         Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: id
+               ->  Seq Scan on rpr_integ
+(9 rows)
+
+-- Verify that the two windows return different counts per row,
+-- confirming the DEFINE conditions were not collapsed by dedup.
+SELECT
+    id, val,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS cnt_up,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val < PREV(val)) AS cnt_down
+FROM rpr_integ
+ORDER BY id;
+ id | val | cnt_up | cnt_down 
+----+-----+--------+----------
+  1 |  10 |      2 |        0
+  2 |  20 |      0 |        2
+  3 |  15 |      2 |        0
+  4 |  25 |      0 |        2
+  5 |   5 |      3 |        0
+  6 |  30 |      0 |        0
+  7 |  35 |      0 |        2
+  8 |  20 |      3 |        0
+  9 |  40 |      0 |        0
+ 10 |  45 |      0 |        0
+(10 rows)
+
+-- ============================================================
+-- 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.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM (
+    SELECT count(*) OVER w FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Aggregate
+   ->  WindowAgg
+         Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a+"
+         ->  Seq Scan on rpr_integ
+(5 rows)
+
+SELECT count(*) FROM (
+    SELECT count(*) OVER w FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+ count 
+-------
+    10
+(1 row)
+
+-- The DEFINE expression references PREV(val), so the window must be
+-- preserved even if the outer query only aggregates over the count.
+-- The plan must still contain a WindowAgg with the PATTERN/DEFINE
+-- intact.
+EXPLAIN (COSTS OFF)
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Aggregate
+   ->  WindowAgg
+         Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a+"
+         ->  Seq Scan on rpr_integ
+(5 rows)
+
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+ count | sum 
+-------+-----
+    10 |   6
+(1 row)
+
+-- The DEFINE expression contains no navigation, but the RPR window
+-- must still be preserved because the match structure itself affects
+-- the count.  The plan must retain the WindowAgg.
+EXPLAIN (COSTS OFF)
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS TRUE)
+) t;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Aggregate
+   ->  WindowAgg
+         Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a+"
+         ->  Seq Scan on rpr_integ
+(5 rows)
+
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS TRUE)
+) t;
+ count | sum 
+-------+-----
+    10 |  10
+(1 row)
+
+-- XXX: "val" is non-resjunk in the subquery output and is not
+-- referenced by the outer query.  Without a guard,
+-- remove_unused_subquery_outputs() would replace it with NULL in
+-- the subquery output, and that replacement propagates to the
+-- scan's targetlist -- DEFINE would then evaluate with NULL
+-- inputs.  The targetlist has no way to distinguish "exposed to
+-- the outer query" from "referenced only by DEFINE", so the
+-- optimization cannot be applied selectively.  The column guard
+-- in allpaths.c blocks this replacement for any column referenced
+-- by an RPR DEFINE clause, keeping the WindowAgg with DEFINE
+-- active in the plan.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM (
+    SELECT val, count(*) OVER w FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Aggregate
+   ->  WindowAgg
+         Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(7 rows)
+
+SELECT count(*) FROM (
+    SELECT val, count(*) OVER w FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t;
+ count 
+-------
+    10
+(1 row)
+
+-- ============================================================
+-- A6. Inverse transition bypass
+-- ============================================================
+-- Verify that RPR windows do not use the moving aggregate (inverse
+-- transition) optimization.  Moving aggregates maintain state by
+-- adding arriving rows and subtracting leaving rows, but an RPR
+-- reduced frame is not a sliding window; the set of rows included in
+-- the frame is determined by pattern matching and cannot be derived
+-- incrementally from the previous frame.
+-- sum() would normally be eligible for the moving aggregate
+-- optimization; under RPR it must be computed from scratch over each
+-- reduced frame, and the returned values must match the pattern.
+-- Note: inverse-transition selection is not exposed in the plan, so
+-- there is no direct EXPLAIN assertion for it.  The structural
+-- guarantee is that RPR uses its own navigation mark, distinct from
+-- the moving-aggregate mark, so the inverse-transition path is
+-- never reached on the RPR side.  This test verifies that
+-- separation indirectly: if inverse transition leaked into the RPR
+-- path, state would mix across match boundaries and pattern_sum
+-- would diverge from the expected output, failing the regression.
+SELECT id, val,
+    sum(val) OVER w AS pattern_sum
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | pattern_sum 
+----+-----+-------------
+  1 |  10 |          30
+  2 |  20 |            
+  3 |  15 |          40
+  4 |  25 |            
+  5 |   5 |          70
+  6 |  30 |            
+  7 |  35 |            
+  8 |  20 |         105
+  9 |  40 |            
+ 10 |  45 |            
+(10 rows)
+
+-- ============================================================
+-- A7. Cost estimation RPR awareness
+-- ============================================================
+-- cost_windowagg() must account for DEFINE expression evaluation cost.
+-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost.
+CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$
+DECLARE
+    plan json;
+    cost numeric;
+BEGIN
+    EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan;
+    cost := (plan->0->'Plan'->>'Total Cost')::numeric;
+    RETURN cost;
+END;
+$$ LANGUAGE plpgsql;
+SELECT get_windowagg_cost(
+    'SELECT count(*) OVER w FROM rpr_integ
+     WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                  PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))')
+    >
+    get_windowagg_cost(
+    'SELECT count(*) OVER w FROM rpr_integ
+     WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)')
+    AS rpr_cost_is_higher;
+ rpr_cost_is_higher 
+--------------------
+ t
+(1 row)
+
+DROP FUNCTION get_windowagg_cost(text);
+-- ============================================================
+-- A8. Subquery flattening prevention
+-- ============================================================
+-- Verify that a subquery containing an RPR window is not flattened
+-- into the outer query.  is_simple_subquery() already blocks pullup
+-- for subqueries with window functions in general; this test confirms
+-- the rule continues to apply to RPR windows, so EXPLAIN must still
+-- show a Subquery Scan above the RPR WindowAgg.
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) sub
+WHERE cnt > 0;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on sub
+   Filter: (sub.cnt > 0)
+   ->  WindowAgg
+         Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(8 rows)
+
+-- ============================================================
+-- A9. DEFINE expression non-propagation
+-- ============================================================
+-- Verify that DEFINE expressions are not propagated into the
+-- targetlist of any upper WindowAgg node.  Only the column references
+-- consumed by DEFINE should be passed up; the full DEFINE expression
+-- is meaningful only inside the RPR WindowAgg that owns it.
+-- 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.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+                                            QUERY PLAN                                             
+---------------------------------------------------------------------------------------------------
+ WindowAgg
+   Output: (count(*) OVER w_rpr), count(*) OVER w_normal, id, val
+   Window: w_normal AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   ->  WindowAgg
+         Output: id, val, count(*) OVER w_rpr
+         Window: w_rpr AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Output: id, val
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on public.rpr_integ
+                     Output: id, val
+(12 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
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY rpr_cnt DESC, normal_cnt DESC;
+ rpr_cnt | normal_cnt 
+---------+------------
+       3 |          6
+       3 |          3
+       2 |         10
+       2 |          8
+       0 |          9
+       0 |          7
+       0 |          5
+       0 |          4
+       0 |          2
+       0 |          1
+(10 rows)
+
+-- ============================================================
+-- A10. RPR + LIMIT
+-- ============================================================
+-- LIMIT must not interfere with RPR pattern matching.  The Limit
+-- node must sit above the WindowAgg so that pattern matching runs
+-- on the full partition first; the result is then a prefix of the
+-- un-LIMITed output.  Pushing Limit below the WindowAgg would
+-- truncate input before matching and silently drop valid matches.
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+LIMIT 5;
+                                     QUERY PLAN                                      
+-------------------------------------------------------------------------------------
+ Limit
+   ->  WindowAgg
+         Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: a b+
+         ->  Sort
+               Sort Key: id
+               ->  Seq Scan on rpr_integ
+(7 rows)
+
+-- Reference: un-LIMITed result against which the LIMIT 5 result is
+-- compared.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- LIMIT 5 case; the first five rows must match the reference above.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+LIMIT 5;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+(5 rows)
+
+-- ============================================================
+-- B1. RPR + CTE
+-- ============================================================
+-- Verify that an RPR window embedded inside a CTE behaves the same as
+-- a direct RPR query:
+--   (1) A single-reference CTE is inlined by the planner and yields
+--       per-row results identical to the direct RPR query.
+--   (2) A multi-reference CTE is materialized (CTE Scan appears in
+--       the plan) so pattern matching runs once, and every reference
+--       observes the same match results.
+-- Baseline: direct RPR produces the per-row reference output.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- Single-reference CTE: plan has no "CTE rpr_result" scope, showing
+-- the CTE was inlined into the surrounding query.
+EXPLAIN (COSTS OFF)
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT id, val, cnt FROM rpr_result ORDER BY id;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Sort
+         Sort Key: rpr_integ.id
+         ->  Seq Scan on rpr_integ
+(6 rows)
+
+-- Result must match the baseline row-for-row.
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT id, val, cnt FROM rpr_result ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- Multi-reference CTE (self-join): plan has a "CTE rpr_result" scope
+-- and CTE Scan nodes on both sides, showing the CTE was materialized
+-- and pattern matching ran only once.
+EXPLAIN (COSTS OFF)
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+                                           QUERY PLAN                                            
+-------------------------------------------------------------------------------------------------
+ Merge Join
+   Merge Cond: ((r2.id = r1.id) AND (r2.cnt = r1.cnt))
+   CTE rpr_result
+     ->  WindowAgg
+           Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+           Pattern: a b+
+           ->  Sort
+                 Sort Key: rpr_integ.id
+                 ->  Seq Scan on rpr_integ
+   ->  Incremental Sort
+         Sort Key: r2.id, r2.cnt
+         Presorted Key: r2.id
+         ->  CTE Scan on rpr_result r2
+   ->  Sort
+         Sort Key: r1.id, r1.cnt
+         ->  CTE Scan on rpr_result r1
+               Filter: (cnt > 0)
+(17 rows)
+
+-- Result: both references see the same match counts, so the self-join
+-- preserves all matched rows from the baseline.
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+ id | cnt 
+----+-----
+  1 |   2
+  3 |   2
+  5 |   3
+  8 |   3
+(4 rows)
+
+-- ============================================================
+-- B2. RPR + JOIN
+-- ============================================================
+-- Verify that an RPR subquery can be joined with another relation.
+-- Two aspects are checked against a non-RPR baseline:
+--   (1) Flattening: a non-RPR subquery is pulled up by the planner
+--       (no Subquery Scan in the plan); an RPR subquery is kept
+--       un-flattened (Subquery Scan above WindowAgg).
+--   (2) Join correctness: the join aligns each RPR match row with
+--       the dimension-table row on the same key.
+CREATE TABLE rpr_integ2 (id INT, label TEXT);
+INSERT INTO rpr_integ2 VALUES
+    (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'),
+    (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j');
+-- Baseline: a non-RPR subquery is flattened by the planner.  No
+-- Subquery Scan node appears; the inner SELECT is merged into the
+-- outer join.
+EXPLAIN (COSTS OFF)
+SELECT r.id, r.val, j.label
+FROM (SELECT id, val FROM rpr_integ) r
+JOIN rpr_integ2 j ON r.id = j.id
+ORDER BY r.id;
+              QUERY PLAN              
+--------------------------------------
+ Merge Join
+   Merge Cond: (j.id = rpr_integ.id)
+   ->  Sort
+         Sort Key: j.id
+         ->  Seq Scan on rpr_integ2 j
+   ->  Sort
+         Sort Key: rpr_integ.id
+         ->  Seq Scan on rpr_integ
+(8 rows)
+
+-- RPR subquery JOIN: the Subquery Scan is preserved above the
+-- WindowAgg, confirming the RPR subquery is not flattened.
+EXPLAIN (COSTS OFF)
+SELECT r.id, r.cnt, j.label
+FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+                                             QUERY PLAN                                              
+-----------------------------------------------------------------------------------------------------
+ Merge Join
+   Merge Cond: (r.id = j.id)
+   ->  Subquery Scan on r
+         Filter: (r.cnt > 0)
+         ->  WindowAgg
+               Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+               Pattern: a b+
+               ->  Sort
+                     Sort Key: rpr_integ.id
+                     ->  Seq Scan on rpr_integ
+   ->  Sort
+         Sort Key: j.id
+         ->  Seq Scan on rpr_integ2 j
+(13 rows)
+
+-- Result: matched RPR rows align with dimension rows on id, showing
+-- the join correctly pairs per-row match counts with their labels.
+SELECT r.id, r.cnt, j.label
+FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+ id | cnt | label 
+----+-----+-------
+  1 |   2 | a
+  3 |   2 | c
+  5 |   3 | e
+  8 |   3 | h
+(4 rows)
+
+-- ============================================================
+-- B3. RPR + Set operations
+-- ============================================================
+-- Verify that RPR results combine correctly with non-RPR results
+-- under a UNION ALL.  The plan must show an Append node with two
+-- independent child plans: the RPR branch with Pattern/DEFINE active,
+-- and the non-RPR branch with a plain WindowAgg.  Each child scans
+-- the base relation on its own and contributes its rows to the
+-- unioned output.
+-- Plan: Append with two independent children.  The RPR branch has a
+-- WindowAgg carrying Pattern/Nav Mark Lookback; the non-RPR branch
+-- has a plain WindowAgg with no pattern metadata.
+EXPLAIN (COSTS OFF)
+SELECT id, cnt, 'rpr' AS source FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+                                                QUERY PLAN                                                 
+-----------------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: ('rpr'::text), t.id
+   ->  Append
+         ->  Subquery Scan on t
+               Filter: (t.cnt > 0)
+               ->  WindowAgg
+                     Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+                     Pattern: a b+
+                     ->  Sort
+                           Sort Key: rpr_integ.id
+                           ->  Seq Scan on rpr_integ
+         ->  WindowAgg
+               Window: w1 AS (ORDER BY rpr_integ_1.id)
+               ->  Sort
+                     Sort Key: rpr_integ_1.id
+                     ->  Seq Scan on rpr_integ rpr_integ_1
+(16 rows)
+
+-- Result: rows from both branches are present in the unioned output.
+-- The RPR branch emits only matched rows (cnt > 0), while the
+-- non-RPR branch emits all rows with its own count values.
+SELECT id, cnt, 'rpr' AS source FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+ id | cnt | source 
+----+-----+--------
+  1 |   1 | normal
+  2 |   2 | normal
+  3 |   3 | normal
+  4 |   4 | normal
+  5 |   5 | normal
+  6 |   6 | normal
+  7 |   7 | normal
+  8 |   8 | normal
+  9 |   9 | normal
+ 10 |  10 | normal
+  1 |   2 | rpr
+  3 |   2 | rpr
+  5 |   3 | rpr
+  8 |   3 | rpr
+(14 rows)
+
+-- ============================================================
+-- B4. RPR + Prepared statements
+-- ============================================================
+-- Verify that RPR queries survive the prepared-statement path by
+-- exercising both plancache modes with a parameter that feeds into
+-- RPR's navigation offset (PREV(val, $1)).  The parameter surfaces
+-- the RPR-specific plancache difference:
+--   - custom plan: "Nav Mark Lookback" is resolved to the literal
+--     parameter value at plan time (e.g., "Nav Mark Lookback: 1").
+--   - generic plan: "Nav Mark Lookback" is deferred to execution and
+--     appears as "Nav Mark Lookback: runtime" in the plan.
+-- The result must be identical under both modes.
+-- Register the prepared statement; DEFINE uses PREV(val, $1) so the
+-- parameter reaches RPR's navigation machinery.
+PREPARE rpr_prev(int) AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val, $1))
+ORDER BY id;
+ERROR:  function prev(integer, integer) does not exist
+LINE 7:     DEFINE B AS val > PREV(val, $1))
+                              ^
+DETAIL:  No function of that name accepts the given number of arguments.
+-- Custom plan: Nav Mark Lookback resolved to the literal 1.
+SET plan_cache_mode = force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
+ERROR:  prepared statement "rpr_prev" does not exist
+EXECUTE rpr_prev(1);
+ERROR:  prepared statement "rpr_prev" does not exist
+-- Generic plan: Nav Mark Lookback deferred to execution, shown as
+-- "runtime" in the plan.  Result must match the custom-plan result
+-- exactly.
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
+ERROR:  prepared statement "rpr_prev" does not exist
+EXECUTE rpr_prev(1);
+ERROR:  prepared statement "rpr_prev" does not exist
+RESET plan_cache_mode;
+DEALLOCATE rpr_prev;
+ERROR:  prepared statement "rpr_prev" does not exist
+-- ============================================================
+-- B5. RPR + Partitioned table
+-- ============================================================
+-- Verify that RPR pattern matching works correctly when the source
+-- relation is partitioned.  The planner must gather rows from every
+-- partition into a single ordered stream before RPR can see them,
+-- because pattern matching is sequential across the entire
+-- partition-by group and cannot be performed independently on each
+-- table partition.
+CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id);
+CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6);
+CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11);
+INSERT INTO rpr_part SELECT id, val FROM rpr_integ;
+-- Plan: partition scans are combined with Append (or Merge Append),
+-- sorted into a single ordered stream, and fed into one WindowAgg
+-- that performs RPR pattern matching across the combined stream.
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+                                       QUERY PLAN                                       
+----------------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY rpr_part.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Sort
+         Sort Key: rpr_part.id
+         ->  Append
+               ->  Seq Scan on rpr_part_1
+               ->  Seq Scan on rpr_part_2
+(8 rows)
+
+-- Baseline: the same query against the non-partitioned rpr_integ
+-- produces the per-row reference output.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- Result against the partitioned table must match the baseline
+-- row-for-row.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+DROP TABLE rpr_part;
+-- ============================================================
+-- B6. RPR + LATERAL
+-- ============================================================
+-- RPR inside a LATERAL subquery.  Qualified column references from
+-- the outer query are not yet supported in DEFINE, so this tests
+-- the basic case where LATERAL provides the correlation filter
+-- (WHERE id <= o.id) and DEFINE uses only local columns.  The plan
+-- must show a Nested Loop driving the outer relation into the inner
+-- subquery scan, with the RPR WindowAgg re-executed for each outer
+-- row and the correlation surfacing as a scan-level Filter on
+-- "id <= o.id".
+-- Plan: Nested Loop with the RPR WindowAgg in the inner leg, driven
+-- by the filtered outer rows (o.id IN (5, 10)).
+EXPLAIN (COSTS OFF)
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WHERE id <= o.id
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+                                                QUERY PLAN                                                 
+-----------------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: o.id, r.id
+   ->  Nested Loop
+         ->  Seq Scan on rpr_integ o
+               Filter: (id = ANY ('{5,10}'::integer[]))
+         ->  Subquery Scan on r
+               Filter: (r.cnt > 0)
+               ->  WindowAgg
+                     Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+                     Pattern: a b+
+                     ->  Sort
+                           Sort Key: rpr_integ.id
+                           ->  Seq Scan on rpr_integ
+                                 Filter: (id <= o.id)
+(14 rows)
+
+-- Result: for each of the two outer ids (5 and 10), the LATERAL
+-- subquery produces RPR match counts over the restricted input.
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WHERE id <= o.id
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+ outer_id | id | cnt 
+----------+----+-----
+        5 |  1 |   2
+        5 |  3 |   2
+       10 |  1 |   2
+       10 |  3 |   2
+       10 |  5 |   3
+       10 |  8 |   3
+(6 rows)
+
+-- ============================================================
+-- B7. RPR + Recursive CTE
+-- ============================================================
+-- Verify that an RPR window can appear inside the non-recursive
+-- (base) leg of a recursive CTE.  The plan must show the RPR
+-- WindowAgg sitting under the Recursive Union as the base-leg
+-- child, with the WorkTable Scan feeding the recursive leg above
+-- it.  This confirms that RPR output can seed a recursive CTE
+-- (window functions cannot appear in the recursive leg itself, a
+-- PostgreSQL restriction, so this is the natural place to exercise
+-- "RPR under Recursive Union").
+--
+-- XXX: Whether this case falls under the ISO/IEC 9075-2 4.18.5 /
+-- 6.17.5 prohibition is not something I can judge.  If this case
+-- is not prohibited, the open question is whether a query that
+-- does trigger the prohibition can be constructed at all.
+-- Whether to prohibit this case is left to the community.
+-- Plan: Recursive Union with the RPR WindowAgg on the base leg and
+-- the WorkTable Scan on the recursive leg.
+EXPLAIN (COSTS OFF)
+WITH RECURSIVE seq AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+    UNION ALL
+    SELECT id + 100, val, cnt FROM seq WHERE id < 3
+)
+SELECT id, val, cnt FROM seq ORDER BY id;
+                                              QUERY PLAN                                               
+-------------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: seq.id
+   CTE seq
+     ->  Recursive Union
+           ->  WindowAgg
+                 Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+                 Pattern: a b+
+                 ->  Sort
+                       Sort Key: rpr_integ.id
+                       ->  Seq Scan on rpr_integ
+           ->  WorkTable Scan on seq seq_1
+                 Filter: (id < 3)
+   ->  CTE Scan on seq
+(13 rows)
+
+-- Result: the base leg contributes the RPR match counts; the
+-- recursive leg propagates those counts with shifted ids.
+WITH RECURSIVE seq AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+    UNION ALL
+    SELECT id + 100, val, cnt FROM seq WHERE id < 3
+)
+SELECT id, val, cnt FROM seq ORDER BY id;
+ id  | val | cnt 
+-----+-----+-----
+   1 |  10 |   2
+   2 |  20 |   0
+   3 |  15 |   2
+   4 |  25 |   0
+   5 |   5 |   3
+   6 |  30 |   0
+   7 |  35 |   0
+   8 |  20 |   3
+   9 |  40 |   0
+  10 |  45 |   0
+ 101 |  10 |   2
+ 102 |  20 |   0
+(12 rows)
+
+-- ============================================================
+-- B8. RPR + Incremental sort
+-- ============================================================
+-- Verify that RPR pattern matching works correctly when the input
+-- to WindowAgg arrives via an incremental sort.  The index on (id)
+-- provides presorted input for the first ORDER BY key, so
+-- "ORDER BY id, val" lets the planner use Incremental Sort to order
+-- only on the second key.  The plan must show Incremental Sort
+-- below the RPR WindowAgg, and RPR must produce the same per-row
+-- match counts as it would with a plain Sort.
+CREATE INDEX rpr_integ_id_idx ON rpr_integ (id);
+SET enable_seqscan = off;
+-- Plan: RPR WindowAgg above an Incremental Sort above an Index Scan.
+-- The Incremental Sort declares "Presorted Key: id" and sorts only
+-- on val within each id group.
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id, val
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+                                     QUERY PLAN                                     
+------------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id, val ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Incremental Sort
+         Sort Key: id, val
+         Presorted Key: id
+         ->  Index Scan using rpr_integ_id_idx on rpr_integ
+(7 rows)
+
+-- Result: RPR over the incrementally sorted stream produces match
+-- counts per row.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id, val
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id, val;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+RESET enable_seqscan;
+DROP INDEX rpr_integ_id_idx;
+-- ============================================================
+-- B9. RPR + Volatile function in DEFINE
+-- ============================================================
+-- Records the current behaviour: DEFINE today accepts volatile
+-- functions such as random() and the query runs to completion.
+-- To keep the expected output deterministic the predicate uses
+-- "random() >= 0.0", which is structurally equivalent to TRUE and
+-- therefore does not perturb the match result.  The interesting
+-- property is that volatile invocation does not crash or short-
+-- circuit pattern matching.
+--
+-- XXX: volatile functions in DEFINE are slated to be rejected at
+-- parse time.  Under RPR's NFA engine the same row's DEFINE
+-- predicate may be evaluated multiple times (backtracking,
+-- PREV/NEXT navigation), so a truly volatile result would make
+-- pattern matching non-deterministic.  When the prohibition lands,
+-- this test must be replaced with an error-case test that expects
+-- random() in DEFINE to be rejected.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val) AND random() >= 0.0)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- ============================================================
+-- B10. RPR + Correlated subquery in WHERE
+-- ============================================================
+-- Verify that an RPR window placed inside a correlated scalar
+-- subquery is executed once per outer row.  DEFINE still references
+-- only local columns (qualified refs from the outer query are not
+-- supported in DEFINE); the correlation lives in the subquery's
+-- WHERE clause as "i.id <= o.id".  The plan must show a SubPlan
+-- attached to the outer scan, with the RPR WindowAgg driven by a
+-- per-row scan filter carrying the correlation predicate.
+-- Plan: SubPlan attached to the outer Seq Scan; the inner scan
+-- carries "Filter: (id <= o.id)", confirming the correlation is
+-- evaluated per outer row.
+EXPLAIN (COSTS OFF)
+SELECT o.id, o.val,
+    (SELECT count(*) OVER w
+     FROM rpr_integ i
+     WHERE i.id <= o.id
+     WINDOW w AS (ORDER BY id
+         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+         PATTERN (A B+)
+         DEFINE B AS val > PREV(val))
+     ORDER BY id
+     LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+                                             QUERY PLAN                                              
+-----------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: o.id
+   ->  Seq Scan on rpr_integ o
+         SubPlan expr_1
+           ->  Limit
+                 ->  WindowAgg
+                       Window: w AS (ORDER BY i.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+                       Pattern: a b+
+                       ->  Sort
+                             Sort Key: i.id
+                             ->  Seq Scan on rpr_integ i
+                                   Filter: (id <= o.id)
+(12 rows)
+
+-- Result: each outer row receives the first_cnt from its own
+-- correlated RPR subquery.
+SELECT o.id, o.val,
+    (SELECT count(*) OVER w
+     FROM rpr_integ i
+     WHERE i.id <= o.id
+     WINDOW w AS (ORDER BY id
+         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+         PATTERN (A B+)
+         DEFINE B AS val > PREV(val))
+     ORDER BY id
+     LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+ id | val | first_cnt 
+----+-----+-----------
+  1 |  10 |         0
+  2 |  20 |         2
+  3 |  15 |         2
+  4 |  25 |         2
+  5 |   5 |         2
+  6 |  30 |         2
+  7 |  35 |         2
+  8 |  20 |         2
+  9 |  40 |         2
+ 10 |  45 |         2
+(10 rows)
+
+-- Cleanup
+DROP TABLE rpr_integ;
+DROP TABLE rpr_integ2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 6a2d19d4410..dbfa3e8f2cc 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -107,7 +107,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Row Pattern Recognition tests
 # ----------
-test: rpr rpr_base rpr_explain rpr_nfa
+test: rpr rpr_base rpr_explain rpr_nfa rpr_integration
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
new file mode 100644
index 00000000000..7d70f5e4b98
--- /dev/null
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -0,0 +1,934 @@
+-- ============================================================
+-- RPR Integration Tests
+-- Planner optimization interaction tests for Row Pattern Recognition
+-- ============================================================
+--
+-- Verifies that each planner optimization correctly handles RPR windows.
+-- Even if individual optimizations are tested elsewhere, this file
+-- provides a single checkpoint for all planner/RPR interactions.
+--
+-- A. Planner Optimization Protection Tests
+--    A1. Frame optimization bypass
+--    A2. Run condition pushdown bypass
+--    A3. Window dedup prevention (RPR vs non-RPR)
+--    A4. Window dedup prevention (same PATTERN, different DEFINE)
+--    A5. Unused window removal prevention
+--    A6. Inverse transition bypass
+--    A7. Cost estimation RPR awareness
+--    A8. Subquery flattening prevention
+--    A9. DEFINE expression non-propagation
+--    A10. RPR + LIMIT
+--
+-- B. Integration Scenario Tests
+--    B1. RPR + CTE
+--    B2. RPR + JOIN
+--    B3. RPR + Set operations
+--    B4. RPR + Prepared statements
+--    B5. RPR + Partitioned table
+--    B6. RPR + LATERAL
+--    B7. RPR + Recursive CTE
+--    B8. RPR + Incremental sort
+--    B9. RPR + Volatile function in DEFINE
+--    B10. RPR + Correlated subquery
+--
+
+CREATE TABLE rpr_integ (id INT, val INT);
+INSERT INTO rpr_integ VALUES
+    (1, 10), (2, 20), (3, 15), (4, 25), (5, 5),
+    (6, 30), (7, 35), (8, 20), (9, 40), (10, 45);
+
+-- ============================================================
+-- A1. Frame optimization bypass
+-- ============================================================
+-- optimize_window_clauses() must not apply frame optimization to RPR windows.
+-- Non-RPR case: frame can be optimized (RANGE -> ROWS conversion, etc.).
+-- RPR case: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND
+-- UNBOUNDED FOLLOWING).
+
+-- Non-RPR window with default frame -> frame optimization applied
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id);
+
+-- RPR window -> frame optimization must NOT change the frame
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+
+-- ============================================================
+-- A2. Run condition pushdown bypass
+-- ============================================================
+-- Verify that find_window_run_conditions() does not push a monotonic
+-- filter down as a Run Condition on RPR windows.  RPR match counts are
+-- determined by pattern matching rather than by a monotonic
+-- accumulation over the frame, so a filter such as "cnt > 0" cannot be
+-- used to stop evaluating the window function early.
+
+-- Non-RPR baseline: the filter is expected to appear as a Run Condition.
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+) t WHERE cnt > 0;
+
+-- RPR case: the filter must appear as a Filter above the WindowAgg,
+-- not as a Run Condition.
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0;
+
+-- Verify that the RPR query still returns every row whose match count is
+-- greater than zero, confirming the filter is evaluated above the
+-- WindowAgg rather than cutting off pattern matching prematurely.
+SELECT * FROM (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+ORDER BY id;
+
+-- ============================================================
+-- A3. Window dedup prevention (RPR vs non-RPR)
+-- ============================================================
+-- Verify that PostgreSQL does not merge an RPR window with a non-RPR
+-- window even when both share the same ORDER BY and frame
+-- specification.  RPR pattern matching produces results that are
+-- semantically different from a plain frame-based aggregate, so the
+-- two windows must remain as separate WindowAgg nodes.  Inline window
+-- specs are used throughout this section because only inline windows
+-- are subject to the dedup path; distinct named windows are always
+-- kept separate regardless of equivalence.
+
+-- Non-RPR baseline: two inline windows with identical spec are
+-- deduped by the planner into a single WindowAgg node, confirming
+-- that the dedup path is active for non-RPR windows.
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS cnt,
+    sum(val)  OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS total
+FROM rpr_integ;
+
+-- An inline RPR window and an inline non-RPR window share the same
+-- ORDER BY and frame but must remain as distinct WindowAgg nodes.
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS rpr_cnt,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt
+FROM rpr_integ;
+
+-- Verify that the two windows return independent counts per row,
+-- confirming they were not merged into a single WindowAgg.
+SELECT
+    id, val,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS rpr_cnt,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt
+FROM rpr_integ
+ORDER BY id;
+
+-- ============================================================
+-- A4. Window dedup prevention (same PATTERN, different DEFINE)
+-- ============================================================
+-- Verify that inline-window dedup does not merge two RPR windows
+-- that share the same PATTERN structure but have different DEFINE
+-- conditions.  Even though the ORDER BY, frame, and PATTERN coincide,
+-- the differing DEFINE expressions classify rows differently and
+-- 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.
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS cnt,
+    sum(val)  OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS total
+FROM rpr_integ;
+
+-- Two inline RPR windows with the same PATTERN but opposite DEFINE
+-- conditions must remain as separate WindowAgg nodes.
+EXPLAIN (COSTS OFF)
+SELECT
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS cnt_up,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val < PREV(val)) AS cnt_down
+FROM rpr_integ;
+
+-- Verify that the two windows return different counts per row,
+-- confirming the DEFINE conditions were not collapsed by dedup.
+SELECT
+    id, val,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)) AS cnt_up,
+    count(*) OVER (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val < PREV(val)) AS cnt_down
+FROM rpr_integ
+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.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM (
+    SELECT count(*) OVER w FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+
+SELECT count(*) FROM (
+    SELECT count(*) OVER w FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+
+-- The DEFINE expression references PREV(val), so the window must be
+-- preserved even if the outer query only aggregates over the count.
+-- The plan must still contain a WindowAgg with the PATTERN/DEFINE
+-- intact.
+EXPLAIN (COSTS OFF)
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > PREV(val))
+) t;
+
+-- The DEFINE expression contains no navigation, but the RPR window
+-- must still be preserved because the match structure itself affects
+-- the count.  The plan must retain the WindowAgg.
+EXPLAIN (COSTS OFF)
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS TRUE)
+) t;
+
+SELECT count(*), sum(c) FROM (
+    SELECT count(*) OVER w AS c FROM rpr_integ
+    WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS TRUE)
+) t;
+
+-- XXX: "val" is non-resjunk in the subquery output and is not
+-- referenced by the outer query.  Without a guard,
+-- remove_unused_subquery_outputs() would replace it with NULL in
+-- the subquery output, and that replacement propagates to the
+-- scan's targetlist -- DEFINE would then evaluate with NULL
+-- inputs.  The targetlist has no way to distinguish "exposed to
+-- the outer query" from "referenced only by DEFINE", so the
+-- optimization cannot be applied selectively.  The column guard
+-- in allpaths.c blocks this replacement for any column referenced
+-- by an RPR DEFINE clause, keeping the WindowAgg with DEFINE
+-- active in the plan.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM (
+    SELECT val, count(*) OVER w FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t;
+
+SELECT count(*) FROM (
+    SELECT val, count(*) OVER w FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t;
+
+-- ============================================================
+-- A6. Inverse transition bypass
+-- ============================================================
+-- Verify that RPR windows do not use the moving aggregate (inverse
+-- transition) optimization.  Moving aggregates maintain state by
+-- adding arriving rows and subtracting leaving rows, but an RPR
+-- reduced frame is not a sliding window; the set of rows included in
+-- the frame is determined by pattern matching and cannot be derived
+-- incrementally from the previous frame.
+
+-- sum() would normally be eligible for the moving aggregate
+-- optimization; under RPR it must be computed from scratch over each
+-- reduced frame, and the returned values must match the pattern.
+-- Note: inverse-transition selection is not exposed in the plan, so
+-- there is no direct EXPLAIN assertion for it.  The structural
+-- guarantee is that RPR uses its own navigation mark, distinct from
+-- the moving-aggregate mark, so the inverse-transition path is
+-- never reached on the RPR side.  This test verifies that
+-- separation indirectly: if inverse transition leaked into the RPR
+-- path, state would mix across match boundaries and pattern_sum
+-- would diverge from the expected output, failing the regression.
+SELECT id, val,
+    sum(val) OVER w AS pattern_sum
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- A7. Cost estimation RPR awareness
+-- ============================================================
+-- cost_windowagg() must account for DEFINE expression evaluation cost.
+-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost.
+
+CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$
+DECLARE
+    plan json;
+    cost numeric;
+BEGIN
+    EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan;
+    cost := (plan->0->'Plan'->>'Total Cost')::numeric;
+    RETURN cost;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT get_windowagg_cost(
+    'SELECT count(*) OVER w FROM rpr_integ
+     WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                  PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))')
+    >
+    get_windowagg_cost(
+    'SELECT count(*) OVER w FROM rpr_integ
+     WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)')
+    AS rpr_cost_is_higher;
+
+DROP FUNCTION get_windowagg_cost(text);
+
+-- ============================================================
+-- A8. Subquery flattening prevention
+-- ============================================================
+-- Verify that a subquery containing an RPR window is not flattened
+-- into the outer query.  is_simple_subquery() already blocks pullup
+-- for subqueries with window functions in general; this test confirms
+-- the rule continues to apply to RPR windows, so EXPLAIN must still
+-- show a Subquery Scan above the RPR WindowAgg.
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) sub
+WHERE cnt > 0;
+
+-- ============================================================
+-- A9. DEFINE expression non-propagation
+-- ============================================================
+-- Verify that DEFINE expressions are not propagated into the
+-- targetlist of any upper WindowAgg node.  Only the column references
+-- consumed by DEFINE should be passed up; the full DEFINE expression
+-- is meaningful only inside the RPR WindowAgg that owns it.
+-- 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.
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    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
+FROM rpr_integ
+WINDOW
+    w_rpr AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val)),
+    w_normal AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY rpr_cnt DESC, normal_cnt DESC;
+
+-- ============================================================
+-- A10. RPR + LIMIT
+-- ============================================================
+-- LIMIT must not interfere with RPR pattern matching.  The Limit
+-- node must sit above the WindowAgg so that pattern matching runs
+-- on the full partition first; the result is then a prefix of the
+-- un-LIMITed output.  Pushing Limit below the WindowAgg would
+-- truncate input before matching and silently drop valid matches.
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+LIMIT 5;
+
+-- Reference: un-LIMITed result against which the LIMIT 5 result is
+-- compared.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- LIMIT 5 case; the first five rows must match the reference above.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+LIMIT 5;
+
+-- ============================================================
+-- B1. RPR + CTE
+-- ============================================================
+-- Verify that an RPR window embedded inside a CTE behaves the same as
+-- a direct RPR query:
+--   (1) A single-reference CTE is inlined by the planner and yields
+--       per-row results identical to the direct RPR query.
+--   (2) A multi-reference CTE is materialized (CTE Scan appears in
+--       the plan) so pattern matching runs once, and every reference
+--       observes the same match results.
+
+-- Baseline: direct RPR produces the per-row reference output.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- Single-reference CTE: plan has no "CTE rpr_result" scope, showing
+-- the CTE was inlined into the surrounding query.
+EXPLAIN (COSTS OFF)
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT id, val, cnt FROM rpr_result ORDER BY id;
+
+-- Result must match the baseline row-for-row.
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT id, val, cnt FROM rpr_result ORDER BY id;
+
+-- Multi-reference CTE (self-join): plan has a "CTE rpr_result" scope
+-- and CTE Scan nodes on both sides, showing the CTE was materialized
+-- and pattern matching ran only once.
+EXPLAIN (COSTS OFF)
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+
+-- Result: both references see the same match counts, so the self-join
+-- preserves all matched rows from the baseline.
+WITH rpr_result AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+
+-- ============================================================
+-- B2. RPR + JOIN
+-- ============================================================
+-- Verify that an RPR subquery can be joined with another relation.
+-- Two aspects are checked against a non-RPR baseline:
+--   (1) Flattening: a non-RPR subquery is pulled up by the planner
+--       (no Subquery Scan in the plan); an RPR subquery is kept
+--       un-flattened (Subquery Scan above WindowAgg).
+--   (2) Join correctness: the join aligns each RPR match row with
+--       the dimension-table row on the same key.
+
+CREATE TABLE rpr_integ2 (id INT, label TEXT);
+INSERT INTO rpr_integ2 VALUES
+    (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'),
+    (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j');
+
+-- Baseline: a non-RPR subquery is flattened by the planner.  No
+-- Subquery Scan node appears; the inner SELECT is merged into the
+-- outer join.
+EXPLAIN (COSTS OFF)
+SELECT r.id, r.val, j.label
+FROM (SELECT id, val FROM rpr_integ) r
+JOIN rpr_integ2 j ON r.id = j.id
+ORDER BY r.id;
+
+-- RPR subquery JOIN: the Subquery Scan is preserved above the
+-- WindowAgg, confirming the RPR subquery is not flattened.
+EXPLAIN (COSTS OFF)
+SELECT r.id, r.cnt, j.label
+FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+
+-- Result: matched RPR rows align with dimension rows on id, showing
+-- the join correctly pairs per-row match counts with their labels.
+SELECT r.id, r.cnt, j.label
+FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+
+-- ============================================================
+-- B3. RPR + Set operations
+-- ============================================================
+-- Verify that RPR results combine correctly with non-RPR results
+-- under a UNION ALL.  The plan must show an Append node with two
+-- independent child plans: the RPR branch with Pattern/DEFINE active,
+-- and the non-RPR branch with a plain WindowAgg.  Each child scans
+-- the base relation on its own and contributes its rows to the
+-- unioned output.
+
+-- Plan: Append with two independent children.  The RPR branch has a
+-- WindowAgg carrying Pattern/Nav Mark Lookback; the non-RPR branch
+-- has a plain WindowAgg with no pattern metadata.
+EXPLAIN (COSTS OFF)
+SELECT id, cnt, 'rpr' AS source FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+
+-- Result: rows from both branches are present in the unioned output.
+-- The RPR branch emits only matched rows (cnt > 0), while the
+-- non-RPR branch emits all rows with its own count values.
+SELECT id, cnt, 'rpr' AS source FROM (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+
+-- ============================================================
+-- B4. RPR + Prepared statements
+-- ============================================================
+-- Verify that RPR queries survive the prepared-statement path by
+-- exercising both plancache modes with a parameter that feeds into
+-- RPR's navigation offset (PREV(val, $1)).  The parameter surfaces
+-- the RPR-specific plancache difference:
+--   - custom plan: "Nav Mark Lookback" is resolved to the literal
+--     parameter value at plan time (e.g., "Nav Mark Lookback: 1").
+--   - generic plan: "Nav Mark Lookback" is deferred to execution and
+--     appears as "Nav Mark Lookback: runtime" in the plan.
+-- The result must be identical under both modes.
+
+-- Register the prepared statement; DEFINE uses PREV(val, $1) so the
+-- parameter reaches RPR's navigation machinery.
+PREPARE rpr_prev(int) AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val, $1))
+ORDER BY id;
+
+-- Custom plan: Nav Mark Lookback resolved to the literal 1.
+SET plan_cache_mode = force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
+EXECUTE rpr_prev(1);
+
+-- Generic plan: Nav Mark Lookback deferred to execution, shown as
+-- "runtime" in the plan.  Result must match the custom-plan result
+-- exactly.
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
+EXECUTE rpr_prev(1);
+
+RESET plan_cache_mode;
+DEALLOCATE rpr_prev;
+
+-- ============================================================
+-- B5. RPR + Partitioned table
+-- ============================================================
+-- Verify that RPR pattern matching works correctly when the source
+-- relation is partitioned.  The planner must gather rows from every
+-- partition into a single ordered stream before RPR can see them,
+-- because pattern matching is sequential across the entire
+-- partition-by group and cannot be performed independently on each
+-- table partition.
+
+CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id);
+CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6);
+CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11);
+INSERT INTO rpr_part SELECT id, val FROM rpr_integ;
+
+-- Plan: partition scans are combined with Append (or Merge Append),
+-- sorted into a single ordered stream, and fed into one WindowAgg
+-- that performs RPR pattern matching across the combined stream.
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- Baseline: the same query against the non-partitioned rpr_integ
+-- produces the per-row reference output.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- Result against the partitioned table must match the baseline
+-- row-for-row.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+DROP TABLE rpr_part;
+
+-- ============================================================
+-- B6. RPR + LATERAL
+-- ============================================================
+-- RPR inside a LATERAL subquery.  Qualified column references from
+-- the outer query are not yet supported in DEFINE, so this tests
+-- the basic case where LATERAL provides the correlation filter
+-- (WHERE id <= o.id) and DEFINE uses only local columns.  The plan
+-- must show a Nested Loop driving the outer relation into the inner
+-- subquery scan, with the RPR WindowAgg re-executed for each outer
+-- row and the correlation surfacing as a scan-level Filter on
+-- "id <= o.id".
+
+-- Plan: Nested Loop with the RPR WindowAgg in the inner leg, driven
+-- by the filtered outer rows (o.id IN (5, 10)).
+EXPLAIN (COSTS OFF)
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WHERE id <= o.id
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+
+-- Result: for each of the two outer ids (5 and 10), the LATERAL
+-- subquery produces RPR match counts over the restricted input.
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+    SELECT id, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WHERE id <= o.id
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+
+-- ============================================================
+-- B7. RPR + Recursive CTE
+-- ============================================================
+-- Verify that an RPR window can appear inside the non-recursive
+-- (base) leg of a recursive CTE.  The plan must show the RPR
+-- WindowAgg sitting under the Recursive Union as the base-leg
+-- child, with the WorkTable Scan feeding the recursive leg above
+-- it.  This confirms that RPR output can seed a recursive CTE
+-- (window functions cannot appear in the recursive leg itself, a
+-- PostgreSQL restriction, so this is the natural place to exercise
+-- "RPR under Recursive Union").
+--
+-- XXX: Whether this case falls under the ISO/IEC 9075-2 4.18.5 /
+-- 6.17.5 prohibition is not something I can judge.  If this case
+-- is not prohibited, the open question is whether a query that
+-- does trigger the prohibition can be constructed at all.
+-- Whether to prohibit this case is left to the community.
+
+-- Plan: Recursive Union with the RPR WindowAgg on the base leg and
+-- the WorkTable Scan on the recursive leg.
+EXPLAIN (COSTS OFF)
+WITH RECURSIVE seq AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+    UNION ALL
+    SELECT id + 100, val, cnt FROM seq WHERE id < 3
+)
+SELECT id, val, cnt FROM seq ORDER BY id;
+
+-- Result: the base leg contributes the RPR match counts; the
+-- recursive leg propagates those counts with shifted ids.
+WITH RECURSIVE seq AS (
+    SELECT id, val, count(*) OVER w AS cnt
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val))
+    UNION ALL
+    SELECT id + 100, val, cnt FROM seq WHERE id < 3
+)
+SELECT id, val, cnt FROM seq ORDER BY id;
+
+-- ============================================================
+-- B8. RPR + Incremental sort
+-- ============================================================
+-- Verify that RPR pattern matching works correctly when the input
+-- to WindowAgg arrives via an incremental sort.  The index on (id)
+-- provides presorted input for the first ORDER BY key, so
+-- "ORDER BY id, val" lets the planner use Incremental Sort to order
+-- only on the second key.  The plan must show Incremental Sort
+-- below the RPR WindowAgg, and RPR must produce the same per-row
+-- match counts as it would with a plain Sort.
+
+CREATE INDEX rpr_integ_id_idx ON rpr_integ (id);
+SET enable_seqscan = off;
+
+-- Plan: RPR WindowAgg above an Incremental Sort above an Index Scan.
+-- The Incremental Sort declares "Presorted Key: id" and sorts only
+-- on val within each id group.
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id, val
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val));
+
+-- Result: RPR over the incrementally sorted stream produces match
+-- counts per row.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id, val
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val))
+ORDER BY id, val;
+
+RESET enable_seqscan;
+DROP INDEX rpr_integ_id_idx;
+
+-- ============================================================
+-- B9. RPR + Volatile function in DEFINE
+-- ============================================================
+-- Records the current behaviour: DEFINE today accepts volatile
+-- functions such as random() and the query runs to completion.
+-- To keep the expected output deterministic the predicate uses
+-- "random() >= 0.0", which is structurally equivalent to TRUE and
+-- therefore does not perturb the match result.  The interesting
+-- property is that volatile invocation does not crash or short-
+-- circuit pattern matching.
+--
+-- XXX: volatile functions in DEFINE are slated to be rejected at
+-- parse time.  Under RPR's NFA engine the same row's DEFINE
+-- predicate may be evaluated multiple times (backtracking,
+-- PREV/NEXT navigation), so a truly volatile result would make
+-- pattern matching non-deterministic.  When the prohibition lands,
+-- this test must be replaced with an error-case test that expects
+-- random() in DEFINE to be rejected.
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val) AND random() >= 0.0)
+ORDER BY id;
+
+-- ============================================================
+-- B10. RPR + Correlated subquery in WHERE
+-- ============================================================
+-- Verify that an RPR window placed inside a correlated scalar
+-- subquery is executed once per outer row.  DEFINE still references
+-- only local columns (qualified refs from the outer query are not
+-- supported in DEFINE); the correlation lives in the subquery's
+-- WHERE clause as "i.id <= o.id".  The plan must show a SubPlan
+-- attached to the outer scan, with the RPR WindowAgg driven by a
+-- per-row scan filter carrying the correlation predicate.
+
+-- Plan: SubPlan attached to the outer Seq Scan; the inner scan
+-- carries "Filter: (id <= o.id)", confirming the correlation is
+-- evaluated per outer row.
+EXPLAIN (COSTS OFF)
+SELECT o.id, o.val,
+    (SELECT count(*) OVER w
+     FROM rpr_integ i
+     WHERE i.id <= o.id
+     WINDOW w AS (ORDER BY id
+         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+         PATTERN (A B+)
+         DEFINE B AS val > PREV(val))
+     ORDER BY id
+     LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+
+-- Result: each outer row receives the first_cnt from its own
+-- correlated RPR subquery.
+SELECT o.id, o.val,
+    (SELECT count(*) OVER w
+     FROM rpr_integ i
+     WHERE i.id <= o.id
+     WINDOW w AS (ORDER BY id
+         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+         PATTERN (A B+)
+         DEFINE B AS val > PREV(val))
+     ORDER BY id
+     LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+
+-- Cleanup
+DROP TABLE rpr_integ;
+DROP TABLE rpr_integ2;
-- 
2.50.1 (Apple Git-155)

From ca171084b7c52e27a1f0bb2a17a8747f375e6a2f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 14:09:12 +0900
Subject: [PATCH] Replace reduced frame map with single match result

index 58f9da0b814..7d0f8fd401c 100644
+ * and rpr_match_length.  When rpr_match_valid is true, the entry describes
+ * the match result for the position at rpr_match_start: rpr_match_matched
+ * indicates success or failure, and rpr_match_length gives the number of
+ * rows consumed.  A match with rpr_match_length 0 represents an empty match
+ * (pattern matched but consumed no rows).  When rpr_match_valid is false,
+ * the position has not been evaluated yet (RF_NOT_DETERMINED).
+ * A row's status against the current match result can be obtained by
+ * calling get_reduced_frame_status().

 

 
 			/*
 
 			/*

 
 
 	CHECK_FOR_INTERRUPTS();
 
-#ifdef RPR_DEBUG

 			/*
 
-#ifdef RPR_DEBUG

 

 
 	switch (state)
 	{
 		case RF_FRAME_HEAD:
-			num_reduced_rows = 1;
-			for (i = pos + 1;
 
 		case RF_SKIPPED:
 
 		case RF_UNMATCHED:
+		case RF_EMPTY_MATCH:
 			rtn = -1;
 			break;

-#ifdef RPR_DEBUG

 /*
 
-	/*

-/*

 
 
 /*

 	/*

 

 
 
 /*
 
-	/*
 
 /* ----------------

From 5d9be742c6266d8fbff887a0577c37743d429690 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 21:47:01 +0900
Subject: [PATCH] Add fixed-length group absorption for RPR

index 7d0f8fd401c..aec1057e1b2 100644

+		/*
 
 	/*
 
 				/*
 
-					Assert(count == 1);
-

 
+			/*
 
+		/*

+	/*

+/*

 	/*

 

 

 

 

From 7851d85ef7450c20b55035547ecaf8c977c68207 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 22:00:50 +0900
Subject: [PATCH] Rename rpr_explain test views from sequential numbers to
 descriptive names

 

 

 

 

 

 

 
 
 -- Very long matches
-CREATE VIEW rpr_ev23 AS

 

 
 
 -- Single row
-CREATE VIEW rpr_ev35 AS

 
 -- Multiple alternations
-CREATE VIEW rpr_ev39 AS
 
 -- Optional elements
-CREATE VIEW rpr_ev40 AS
 
 -- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
 
 -- Star quantifier
-CREATE VIEW rpr_ev42 AS

 

 

 -- Exact count {n}
-CREATE VIEW rpr_ev54 AS
 
 -- Range {n,m}
-CREATE VIEW rpr_ev55 AS
 
 -- At least {n,}
-CREATE VIEW rpr_ev56 AS

 

 
 -- Alternation at start
-CREATE VIEW rpr_ev64 AS

 
 -- Alternation at end
-CREATE VIEW rpr_ev67 AS

 
 
 -- Nested groups
-CREATE VIEW rpr_ev72 AS

 

 

 -- Using PREV function
-CREATE VIEW rpr_ev82 AS

 

 

 

 

 

 

 

 

 
 
 -- Very long matches
-CREATE VIEW rpr_ev23 AS

 

 

 -- XML format output
-CREATE VIEW rpr_ev31 AS

 
 -- Empty result set
-CREATE VIEW rpr_ev34 AS
 
 -- Single row
-CREATE VIEW rpr_ev35 AS

 
 -- Nested groups
-CREATE VIEW rpr_ev38 AS
 
 -- Multiple alternations
-CREATE VIEW rpr_ev39 AS
 
 -- Optional elements
-CREATE VIEW rpr_ev40 AS
 
 -- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
 
 -- Star quantifier
-CREATE VIEW rpr_ev42 AS

 

 
 
 -- With INITIAL keyword
-CREATE VIEW rpr_ev49 AS

 -- Plus quantifier
-CREATE VIEW rpr_ev51 AS

 
 -- Exact count {n}
-CREATE VIEW rpr_ev54 AS
 
 -- Range {n,m}
-CREATE VIEW rpr_ev55 AS
 
 -- At least {n,}
-CREATE VIEW rpr_ev56 AS

 
 
 -- Simple alternation
-CREATE VIEW rpr_ev60 AS

 
 
 -- Alternation at start
-CREATE VIEW rpr_ev64 AS

 
 -- Alternation at end
-CREATE VIEW rpr_ev67 AS

 
 -- Simple group
-CREATE VIEW rpr_ev70 AS

 -- Nested groups
-CREATE VIEW rpr_ev72 AS

 

 -- count(*) with pattern
-CREATE VIEW rpr_ev77 AS

 

 -- Using PREV function
-CREATE VIEW rpr_ev82 AS

 

 

 

From 5a5e04eb3becfc5cd185b67243fe15eff5ef6f56 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 08:53:46 +0900
Subject: [PATCH] Fix quote_identifier() for RPR pattern variable name deparse

 

 

 

From b49e64adde991d23799fdfd309e6d996c8d2e5c6 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:14:59 +0900
Subject: [PATCH] Fix execRPR.o ordering in executor Makefile to match
 meson.build

---

From 80726d6151aa00ae6edf73fc49498aac75f9fb28 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:40:53 +0900
Subject: [PATCH] Remove unused force_colno parameter from RPR deparse
 functions

---

 
 
-- 
2.50.1 (Apple Git-155)

From 5e4a1d039508c7872adc2506706a7481b3d3755b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:43:07 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS to RPR context cleanup and finalize
 loops

---

index aec1057e1b2..4c429528b04 100644

 

From e54dd341aef218be3dcce088ee46c4d35f24d482 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:48:55 +0900
Subject: [PATCH] Narrow variable scope in ExecInitWindowAgg DEFINE clause loop

---

-	TargetEntry *te;
-	Expr	   *expr;
-

-			te = lfirst(l);
-			name = te->resname;
-			expr = te->expr;
-

From 747b855b0bb586e0d0b6164065376079dc5473c5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 11:32:56 +0900
Subject: [PATCH] Normalize RPR element flag macros to return bool

---

From 79855c1edcf7c411c63ddeab09ac238f2b7d9986 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 23:52:42 +0900
Subject: [PATCH] Implement 1-slot PREV/NEXT navigation for RPR

LLVM JIT falls back to the interpreter for expressions containing
RPR navigation opcodes, because JIT code caches the outertuple's
tts_values/tts_isnull pointers in the entry block and the
mid-expression slot swap leaves them stale. Only DEFINE clause
expressions with PREV/NEXT are affected; other expressions in
the same query are still JIT-compiled normally.
---
 doc/src/sgml/func/func-window.sgml            |  22 +-
 src/backend/executor/execExpr.c               |  56 ++
 src/backend/executor/execExprInterp.c         | 110 ++++
 src/backend/executor/nodeWindowAgg.c          | 243 ++++----
 src/backend/jit/llvm/llvmjit_expr.c           |  40 ++
 src/backend/jit/llvm/llvmjit_types.c          |   2 +
 src/backend/nodes/nodeFuncs.c                 |  33 ++
 src/backend/parser/parse_func.c               |  30 +-
 src/backend/parser/parse_rpr.c                |  86 +++
 src/backend/utils/adt/ruleutils.c             |  16 +
 src/backend/utils/adt/windowfuncs.c           |  52 +-
 src/include/catalog/pg_proc.dat               |   6 +
 src/include/executor/execExpr.h               |  18 +
 src/include/executor/nodeWindowAgg.h          |   3 +
 src/include/nodes/execnodes.h                 |  10 +-
 src/include/nodes/primnodes.h                 |  31 +
 src/test/regress/expected/rpr.out             | 551 +++++++++++++++++-
 src/test/regress/expected/rpr_base.out        |   2 +-
 src/test/regress/expected/rpr_explain.out     |  80 +++
 src/test/regress/expected/rpr_integration.out |  57 +-
 src/test/regress/sql/rpr.sql                  | 322 +++++++++-
 src/test/regress/sql/rpr_explain.sql          |  56 ++
 src/tools/pgindent/typedefs.list              |   3 +-
 23 files changed, 1655 insertions(+), 174 deletions(-)

@@ -318,12 +323,17 @@
         <indexterm>
          <primary>next</primary>
         </indexterm>

+		case T_RPRNavExpr:
+			{
+				/*
 
+#include "common/int.h"

 
-/*

 

 

 
 
 	/*
 
+	/*

 /*
 
-static bool
 
-	if (node == NULL)
-		return false;
+	/*

-/*

-	/*

 

 
+	/*
 
+			case EEOP_RPR_NAV_SET:

 
 /*
  * transformRPR

 
 	return defineClause;
 }
+
+/*
+ * check_rpr_nav_expr
 
+		case T_RPRNavExpr:
+			{
 
 /*
  * prev
 
 /*
  * next

 

 
 
+/*
+ * RPRNavExpr
+ *
 
+--

 
+-- Using 1-arg PREV (implicit offset 1)
+CREATE VIEW rpr_ev_nav_prev1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+          line           
+-------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v > PREV(v)) );
+(3 rows)
+
+-- Using 1-arg NEXT (implicit offset 1)
+CREATE VIEW rpr_ev_nav_next1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+          line           
+-------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v < NEXT(v)) );
+(3 rows)
+
+-- Using 2-arg PREV (explicit offset)
+CREATE VIEW rpr_ev_nav_prev2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v > PREV(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+                 line                 
+--------------------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v > PREV(v, (2)::bigint)) );
+(3 rows)
+
+-- Using 2-arg NEXT (explicit offset)
+CREATE VIEW rpr_ev_nav_next2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        A AS TRUE,
+        B AS v < NEXT(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+                 line                 
+--------------------------------------
+   PATTERN (a b+) 
+   DEFINE
+   b AS (v < NEXT(v, (2)::bigint)) );
+(3 rows)
+
 -- Using NULL comparisons
 CREATE VIEW rpr_ev_def_null AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 0ee612b74fb..f9f70a814ca 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1026,27 +1026,66 @@ WINDOW w AS (ORDER BY id
     PATTERN (A B+)
     DEFINE B AS val > PREV(val, $1))
 ORDER BY id;
-ERROR:  function prev(integer, integer) does not exist
-LINE 7:     DEFINE B AS val > PREV(val, $1))
-                              ^
-DETAIL:  No function of that name accepts the given number of arguments.
 -- Custom plan: Nav Mark Lookback resolved to the literal 1.
 SET plan_cache_mode = force_custom_plan;
 EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
-ERROR:  prepared statement "rpr_prev" does not exist
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(6 rows)
+
 EXECUTE rpr_prev(1);
-ERROR:  prepared statement "rpr_prev" does not exist
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
 -- Generic plan: Nav Mark Lookback deferred to execution, shown as
 -- "runtime" in the plan.  Result must match the custom-plan result
 -- exactly.
 SET plan_cache_mode = force_generic_plan;
 EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
-ERROR:  prepared statement "rpr_prev" does not exist
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b+
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(6 rows)
+
 EXECUTE rpr_prev(1);
-ERROR:  prepared statement "rpr_prev" does not exist
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
 RESET plan_cache_mode;
 DEALLOCATE rpr_prev;
-ERROR:  prepared statement "rpr_prev" does not exist
 -- ============================================================
 -- B5. RPR + Partitioned table
 -- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 95794d409e1..504476a2b02 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -440,6 +440,326 @@ WINDOW w AS (
   B AS val > PREV(val) * 0.99
 );
 
+--

From efb99428cfdb41363d49d4b7ca199f9212ba5a6e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 10:54:30 +0900
Subject: [PATCH] Add JIT compilation support for RPR PREV/NEXT navigation

---

 
 			case EEOP_RPR_NAV_SET:

 

From 7d9f1f094dbd685a03d982f9d62a86fb392c877f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 11:49:32 +0900
Subject: [PATCH] Add tuplestore trim optimization for RPR PREV navigation

 
 
+		/*

+/*
 
+/*

 
 	/* RPR navigation */
 
+	/*
 
+/*

From fea456180111bfdb10d4dc580fc4062fe0ae6b15 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 16:06:06 +0900
Subject: [PATCH] Update RPR code comments to reflect 1-slot navigation model

---

index 4c429528b04..5428d0e8fc4 100644

From 79f32ee6991e3cbf5bea0e51099e9613c13f5ec0 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 09:48:54 +0900
Subject: [PATCH] Enable JIT compilation for PREV/NEXT navigation tests in RPR

---

 RESET jit_above_cost;
+RESET jit;
 
 --
 -- IGNORE NULLS
-- 
2.50.1 (Apple Git-155)

From 081f70847766b2aa312b667e5b7c8e2a41088378 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 09:53:14 +0900
Subject: [PATCH] Add 2-arg PREV/NEXT test for row pattern navigation with host
 variable

---

From a4f260651422e7bc364789d493784d5e7f9e7c52 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 10:46:38 +0900
Subject: [PATCH] Add Nav Mark Lookback to EXPLAIN and fix
 compute_nav_max_offset()

Add regression tests covering constant offsets, NEXT-only, multiple
PREV offsets, and host variable offsets under both custom and generic
plans.
---
 src/backend/commands/explain.c                |   9 +
 src/backend/optimizer/plan/createplan.c       |   2 +-
 src/test/regress/expected/rpr_base.out        | 231 ++++++----
 src/test/regress/expected/rpr_explain.out     | 397 ++++++++++++++----
 src/test/regress/expected/rpr_integration.out |  70 ++-
 src/test/regress/sql/rpr_explain.sql          |  71 ++++
 6 files changed, 590 insertions(+), 190 deletions(-)

 
 
 	return maxOffset;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 -- Very long matches

 

 
 
 -- Single row

 
 
 -- Multiple alternations
 
 -- Optional elements
 
 -- Bounded quantifiers
 
 -- Star quantifier

 

 

 

 
 -- Exact count {n}
 
 -- Range {n,m}
 
 -- At least {n,}

 

 

 -- Alternation at start

 
 -- Alternation at end

 

 -- Nested groups

 

 

 
 
 -- Using PREV function

 

 

+RESET plan_cache_mode;
+DEALLOCATE rpr_nav_offset_prep;
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index f9f70a814ca..9dbf0f43c8f 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -67,10 +67,11 @@ WINDOW w AS (ORDER BY id
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_integ
-(6 rows)
+(7 rows)
 
 -- ============================================================
 -- A2. Run condition pushdown bypass
@@ -117,10 +118,11 @@ SELECT * FROM (
    ->  WindowAgg
          Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: rpr_integ.id
                ->  Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
 
 -- Verify that the RPR query still returns every row whose match count is
 -- greater than zero, confirming the filter is evaluated above the
@@ -190,10 +192,11 @@ FROM rpr_integ;
    ->  WindowAgg
          Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: id
                ->  Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
 
 -- Verify that the two windows return independent counts per row,
 -- confirming they were not merged into a single WindowAgg.
@@ -250,10 +253,11 @@ FROM rpr_integ;
  WindowAgg
    Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_integ
-(6 rows)
+(7 rows)
 
 -- Two inline RPR windows with the same PATTERN but opposite DEFINE
 -- conditions must remain as separate WindowAgg nodes.
@@ -273,13 +277,15 @@ FROM rpr_integ;
  WindowAgg
    Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  WindowAgg
          Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: id
                ->  Seq Scan on rpr_integ
-(9 rows)
+(11 rows)
 
 -- Verify that the two windows return different counts per row,
 -- confirming the DEFINE conditions were not collapsed by dedup.
@@ -336,8 +342,9 @@ SELECT count(*) FROM (
    ->  WindowAgg
          Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a+"
+         Nav Mark Lookback: 1
          ->  Seq Scan on rpr_integ
-(5 rows)
+(6 rows)
 
 SELECT count(*) FROM (
     SELECT count(*) OVER w FROM rpr_integ
@@ -369,8 +376,9 @@ SELECT count(*), sum(c) FROM (
    ->  WindowAgg
          Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a+"
+         Nav Mark Lookback: 1
          ->  Seq Scan on rpr_integ
-(5 rows)
+(6 rows)
 
 SELECT count(*), sum(c) FROM (
     SELECT count(*) OVER w AS c FROM rpr_integ
@@ -401,8 +409,9 @@ SELECT count(*), sum(c) FROM (
    ->  WindowAgg
          Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a+"
+         Nav Mark Lookback: 0
          ->  Seq Scan on rpr_integ
-(5 rows)
+(6 rows)
 
 SELECT count(*), sum(c) FROM (
     SELECT count(*) OVER w AS c FROM rpr_integ
@@ -441,10 +450,11 @@ SELECT count(*) FROM (
    ->  WindowAgg
          Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: rpr_integ.id
                ->  Seq Scan on rpr_integ
-(7 rows)
+(8 rows)
 
 SELECT count(*) FROM (
     SELECT val, count(*) OVER w FROM rpr_integ
@@ -555,10 +565,11 @@ WHERE cnt > 0;
    ->  WindowAgg
          Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: rpr_integ.id
                ->  Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
 
@@ -595,12 +606,13 @@ WINDOW
 
 -- Executing the same query shows the client result is limited to
 -- the two projected columns; "id" and "val" that appeared in the
@@ -654,10 +666,11 @@ LIMIT 5;
    ->  WindowAgg
          Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
          Pattern: a b+
+         Nav Mark Lookback: 1
          ->  Sort
                Sort Key: id
                ->  Seq Scan on rpr_integ
-(7 rows)
+(8 rows)
 
 -- Reference: un-LIMITed result against which the LIMIT 5 result is
 -- compared.
@@ -748,10 +761,11 @@ SELECT id, val, cnt FROM rpr_result ORDER BY id;
  WindowAgg
    Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Sort
          Sort Key: rpr_integ.id
          ->  Seq Scan on rpr_integ
-(6 rows)
+(7 rows)
 
 -- Result must match the baseline row-for-row.
 WITH rpr_result AS (
@@ -802,6 +816,7 @@ ORDER BY r1.id;
      ->  WindowAgg
            Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
            Pattern: a b+
+           Nav Mark Lookback: 1
            ->  Sort
                  Sort Key: rpr_integ.id
                  ->  Seq Scan on rpr_integ
@@ -813,7 +828,7 @@ ORDER BY r1.id;
          Sort Key: r1.id, r1.cnt
          ->  CTE Scan on rpr_result r1
                Filter: (cnt > 0)
-(17 rows)
+(18 rows)
 
 -- Result: both references see the same match counts, so the self-join
 -- preserves all matched rows from the baseline.
@@ -896,13 +911,14 @@ ORDER BY r.id;
          ->  WindowAgg
                Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
                Pattern: a b+
+               Nav Mark Lookback: 1
                ->  Sort
                      Sort Key: rpr_integ.id
                      ->  Seq Scan on rpr_integ
    ->  Sort
          Sort Key: j.id
          ->  Seq Scan on rpr_integ2 j
-(13 rows)
+(14 rows)
 
 -- Result: matched RPR rows align with dimension rows on id, showing
 -- the join correctly pairs per-row match counts with their labels.
@@ -961,6 +977,7 @@ ORDER BY source, id;
                ->  WindowAgg
                      Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
                      Pattern: a b+
+                     Nav Mark Lookback: 1
                      ->  Sort
                            Sort Key: rpr_integ.id
                            ->  Seq Scan on rpr_integ
@@ -969,7 +986,7 @@ ORDER BY source, id;
                ->  Sort
                      Sort Key: rpr_integ_1.id
                      ->  Seq Scan on rpr_integ rpr_integ_1
-(16 rows)
+(17 rows)
 
 -- Result: rows from both branches are present in the unioned output.
 -- The RPR branch emits only matched rows (cnt > 0), while the
@@ -1034,10 +1051,11 @@ EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_integ
-(6 rows)
+(7 rows)
 
 EXECUTE rpr_prev(1);
  id | val | cnt 
@@ -1064,10 +1082,11 @@ EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: runtime
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_integ
-(6 rows)
+(7 rows)
 
 EXECUTE rpr_prev(1);
  id | val | cnt 
@@ -1115,12 +1134,13 @@ ORDER BY id;
  WindowAgg
    Window: w AS (ORDER BY rpr_part.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Sort
          Sort Key: rpr_part.id
          ->  Append
                ->  Seq Scan on rpr_part_1
                ->  Seq Scan on rpr_part_2
-(8 rows)
+(9 rows)
 
 -- Baseline: the same query against the non-partitioned rpr_integ
 -- produces the per-row reference output.
@@ -1208,11 +1228,12 @@ ORDER BY o.id, r.id;
                ->  WindowAgg
                      Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
                      Pattern: a b+
+                     Nav Mark Lookback: 1
                      ->  Sort
                            Sort Key: rpr_integ.id
                            ->  Seq Scan on rpr_integ
                                  Filter: (id <= o.id)
-(14 rows)
+(15 rows)
 
 -- Result: for each of the two outer ids (5 and 10), the LATERAL
 -- subquery produces RPR match counts over the restricted input.
@@ -1279,13 +1300,14 @@ SELECT id, val, cnt FROM seq ORDER BY id;
            ->  WindowAgg
                  Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
                  Pattern: a b+
+                 Nav Mark Lookback: 1
                  ->  Sort
                        Sort Key: rpr_integ.id
                        ->  Seq Scan on rpr_integ
            ->  WorkTable Scan on seq seq_1
                  Filter: (id < 3)
    ->  CTE Scan on seq
-(13 rows)
+(14 rows)
 
 -- Result: the base leg contributes the RPR match counts; the
 -- recursive leg propagates those counts with shifted ids.
@@ -1343,11 +1365,12 @@ WINDOW w AS (ORDER BY id, val
  WindowAgg
    Window: w AS (ORDER BY id, val ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b+
+   Nav Mark Lookback: 1
    ->  Incremental Sort
          Sort Key: id, val
          Presorted Key: id
          ->  Index Scan using rpr_integ_id_idx on rpr_integ
-(7 rows)
+(8 rows)
 
 -- Result: RPR over the incrementally sorted stream produces match
 -- counts per row.
@@ -1449,11 +1472,12 @@ ORDER BY o.id;
                  ->  WindowAgg
                        Window: w AS (ORDER BY i.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
                        Pattern: a b+
+                       Nav Mark Lookback: 1
                        ->  Sort
                              Sort Key: i.id
                              ->  Seq Scan on rpr_integ i
                                    Filter: (id <= o.id)
-(12 rows)
+(13 rows)
 
 -- Result: each outer row receives the first_cnt from its own
 -- correlated RPR subquery.

From 61571e745b76c864158c7c574136e13c1913c516 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 5 Apr 2026 00:56:20 +0900
Subject: [PATCH] Implement FIRST/LAST and compound navigation for RPR

 
+      <row>
 
+   <para>

 

 /*
 
 	/*

 		if (offset < 0)
 			ereport(ERROR,
 
 	/*
 
+	/*

 /*
index 5428d0e8fc4..01df2a11e0a 100644
 
+/*

+		/*

 
 /*
 
 	/*
 
 		/*

 
 	/*

 /*

 

 /*
  * eval_nav_max_offset

 

-		/*

 
 
-	list_free(offsets);
 
 /*
 
 		/*

+#include "common/int.h"
 
 /*

 
-				if (c->constisnull)

 
 
 /*

 
-	return maxOffset;
+	return result;
 }
 
 /*

 	/*

 

 

 
 
 static bool

 
-	if (result.has_nav)
-		ereport(ERROR,

 

 	/* RPR navigation */

+/*
 
 	/*

 
-/*

+--
 
+RESET jit_above_cost;
+RESET jit;

 DROP TABLE rpr_null;

 
 RESET plan_cache_mode;
 
+--

 

 
 DROP TABLE rpr_null;

 

 

From c333424313fb8d94ff0aefffb189890db776fefe Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 13:31:44 +0900
Subject: [PATCH] Guard against int64 overflow in RPR bounded frame end
 computation

---

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 01df2a11e0a..94f1b2941a2 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -22,6 +22,7 @@
  */
 #include "postgres.h"
 
+#include "common/int.h"

From f3d7cb40298ee33fa37b5bd75c17ad2cb7e20ffe Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 11:15:12 +0900
Subject: [PATCH] Fix RPR error message style: hint format, terminology,
 capitalization

 

 

 

From 809e8b5c9f79e40ee114bef963e2a902800608ed Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 10:09:41 +0900
Subject: [PATCH] Fix comment typos, grammar, and inaccuracies in RPR code

---

index 94f1b2941a2..baee45ce54e 100644
 
 		/*
 
 /*
  * rpr_is_defined

From 9a81f1981cd930fa1e25288a07f350fbace4c9a5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 10:50:36 +0900
Subject: [PATCH] Fix RPR documentation: synopsis, grammar, and terminology

 
    <para>
 
    <para>
 
    <para>
 
   <para>

@@ -328,12 +329,13 @@
        </para>
        <para>

 <synopsis>
 
    <para>

From c33aca418319816bde883d1ad6b07f7effbdddea Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 13:59:29 +0900
Subject: [PATCH] Fix nav_slot pass-by-ref dangling pointer in RPR navigation

 

 

From 76b60fb538906346144be7430b858ddd471ec3ab Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 20:49:27 +0900
Subject: [PATCH] Add inline comments for complex RPR algorithms and design
 notes

index baee45ce54e..7ba7b6fb672 100644
 
 				/*

 
 		/*
 
 	/*

From a074cdb45ffaa6a46a0b60a581a2eb527d855cad Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 20:52:13 +0900
Subject: [PATCH] Remove unused include and fix header ordering in RPR files

---

-#include "common/int.h"

---

From: Tatsuo Ishii <[email-masked]>
Date: Mon, 27 Apr 2026 13:52:43 +0900 (JST)

[Chao Li <[email-masked]> is added to Cc: I accidentally
forgot to add him. Sorry, Chao.]

Hi Henson,

I looked through 0001-0031 patches.

> with the review adjustments folded in.

Thank you for taking care of my request.

>     Recognition)" expansion and the later occurrence is the bare

Confirmed. 0006 Looks good to me.

>           disposition is left to the community.

Ok, let's discuss on this later on.

Ok.

Comment to A1:

:
:

The comment says frame optimization applied. But I don't see any frame
option changes (see how the first query changes the frame options).

I thought a frame optimization is something like this.
EXPLAIN(COSTS OFF)
SELECT row_number() OVER W FROM generate_series(1,5) s(i)
WINDOW w AS (
       ORDER BY i
       ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
);
                      QUERY PLAN                      
------------------------------------------------------
 WindowAgg
   Window: w AS (ORDER BY i ROWS UNBOUNDED PRECEDING)
   ->  Sort
         Sort Key: i
         ->  Function Scan on generate_series s
(5 rows)

EXPLAIN(COSTS OFF)
SELECT row_number() OVER W FROM generate_series(1,5) s(i)
WINDOW w AS (
       ORDER BY i
       ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
       PATTERN (A+)
       DEFINE A AS i > 2
);
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 WindowAgg
   Window: w AS (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
   Pattern: a+"
   Nav Mark Lookback: 0
   ->  Sort
         Sort Key: i
         ->  Function Scan on generate_series s
(7 rows)

>       * README.rpr split deferred per the above.

Thanks for the new patch. 0008 looks good to me.

>     "Nav Mark Lookback" lines, respectively).

Comment to 0009:

Why do not use bms here? Maybe winstate->nfaVisitedElems could become
quite big?

0010-0016 look good to me.

Comment to 0017:
+
+/*
+ * check_rpr_nav_expr

"Checks for" --> "Check for" (no "s").

+		ereport(ERROR,

 errmsg("PREV and NEXT cannot be nested") ->
 errmsg("cannot nest PREV and NEXT") (avoid passive voice in error messages)

+	ereport(ERROR,

errmsg("next() can only be used in a DEFINE clause") ->
errmsg("cannot use next() other than in a DEFINE clause")

The same reason above. There are some places where similar changing
necessary.

You can use Max macro here.

0018-0023 look good to me.

Comment to 0024:

"Returns the column value at the row" is not appropriate because
first() accepts any expression as the first argument. e.g. first(col1
+ col2).  Instead what about "Returns value evaluated at the row"?
Same thing can be said to other row pattern navigation operations.
I think they inherits my mistakes in my earlier patch. Sorry for this.

Non ASCII letter(≠) should not be used.

0025-0031 look good to me.

> benefits from this flexibility.

Agreed. The flexibility does not bring benefits to users.

> would you prefer to keep the current behaviour in place?

Let's prohibit volatile functions in DEFINE. Although this needs extra
checking, it would make proceeding codes and tests simpler.  If
others, especially Vik and Jacob, think differently, please join the
discussion.

> review progresses, and I am happy to adjust either way based on

I too prefer to have it after the 31-patch set.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Mon, 27 Apr 2026 17:03:26 +0900

Hi Tatsuo,

Thank you very much for the careful review covering 0001-0031 in a
single pass.

> +WINDOW w AS (ORDER BY id);

Thank you for pointing this out.  I will analyse A1 and reflect
the findings in the next revision.

> Why do not use bms here? Maybe winstate->nfaVisitedElems could become

Thank you for the suggestion.  I was not aware of Bitmapset when
this was written; I will switch nfaVisitedElems to Bitmapset.

On sizing, nfa->nelems is structurally bounded by RPR_ELEMIDX_MAX
(INT16_MAX), since RPRNFAState.elemIdx is int16 and scanRPRPattern()
rejects anything past that with "pattern too complex".  So the
bitmap is at most 4 KB per WindowAgg even in the worst case.

> 0010-0016 look good to me.

Confirmed.

> "Checks for" --> "Check for" (no "s").

Thank you, will fix.

> nav->location)));

Thank you for the guidance.  I will fix the two messages called out
here and also look over the surrounding RPR errmsg() strings for
similar passive-voice phrasings, and rewrite those in the active
voice consistent with the PostgreSQL message style guide.

> You can use Max macro here.

Thank you, will switch both sites to Max().

Confirmed.

> Comment to 0024:
>
> +        <function>first</function> ( <parameter>value</parameter>
> <type>anyelement</type> [, <parameter>offset</parameter>
> <type>bigint</type> ] )

Thank you, and please do not apologise -- I should have caught this
when I carried the wording across.  I will reword the synopses for
first(), last(), prev(), next() (and any other entry that inherited
the same phrasing) along the line you suggested.

> Non ASCII letter(≠) should not be used.

Thank you, will fix and sweep rpr*.out for any other non-ASCII
characters (the → on the same line is also non-ASCII).

> 0025-0031 look good to me.

Confirmed.

> others, especially Vik and Jacob, think differently, please join the

Thank you for the decision.  Since this is a policy change that may
still attract further community discussion, I would like to handle
it as a lower-priority, self-contained follow-up on top of 0031, so
it can be picked up, held, or revised independently.  Tentative
plan: reject volatile functions in DEFINE during parse-analysis via
contain_volatile_functions(), fold with the existing offset
runtime-constant check where the two share traversal, and -- if the
prohibition lands -- convert the B9 test into an error-case test
and drop the XXX note.

> I too prefer to have it after the 31-patch set.

Thank you, confirmed.  Both items (volatile-DEFINE prohibition and
README.rpr split) will land as follow-up patches on top of 0031,
after the current series is committed.

One quick question: for the 0007 / 0009 / 0017 / 0024 review fixes
summarised below, would additional follow-up patches on top of 0031
be preferable to refreshing the already-reviewed series?

For ease of tracking, here is a one-line summary per patch of the
changes I plan to fold into the next revision:

  0007: analyse A1 and, if the diagnosis holds, revise it so the
        frame optimization bypass is observable in EXPLAIN
        (adjusting the comment and expected output accordingly).
  0009: switch WindowAggState.nfaVisitedElems from a bitmapword[]
        to Bitmapset, replacing the manual WORDNUM/BITNUM sites
        with bms_add_member() / bms_is_member() / bms_free().
  0017: fix the "Checks for" -> "Check for" comment, rewrite the
        called-out passive-voice errmsg() strings (and the similar
        ones nearby) in the active voice, and use Max() at the two
        offset-update sites.
  0024: reword the navigation synopses (first/last/prev/next) to
        "Returns the value of <value> evaluated at the row ...",
        and replace the non-ASCII ≠ / → in rpr.out with != / ->
        (sweeping the rest of rpr*.out for other non-ASCII).

In addition, I plan to follow the current series with two new
patches on top of 0031 (final numbering deferred until they are
posted):

  new: README.rpr split -- extract the design notes currently
       inlined in execRPR.c into src/backend/executor/README.rpr,
       as previously discussed.
  new: Prohibit volatile functions in DEFINE -- separate,
       self-contained patch as described above; intended as a
       lower-priority follow-up that can be picked up, held, or
       revised independently of the main series depending on how
       the community discussion settles.

Separately, one item is deferred for later discussion rather than
folded into a patch:

  deferred: B7 Recursive CTE XXX in 0007 -- whether this case
            falls under ISO/IEC 9075-2 4.18.5 / 6.17.5 prohibition
            ("Ok, let's discuss on this later on.").  Noted, happy
            to pick this up whenever convenient.

Once you let me know your preference on the sequencing question
above, I will send the corresponding patches as soon as they are
ready, and post the two follow-ups separately so that they can be
reviewed and committed on top of the main series.

Thank you again for the thorough review.

Regards,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Mon, 27 Apr 2026 17:42:20 +0900 (JST)

Hi Henson,

> the findings in the next revision.

Thanks in advance.

> bitmap is at most 4 KB per WindowAgg even in the worst case.

Hmm. From the comment from bitmapset.c:
 * A bitmap set can represent any set of nonnegative integers, although
 * it is mainly intended for sets where the maximum value is not large,
 * say at most a few hundred.

Maybe 4 KB is tool large? If so, there's no need to rush to change the
implementation. You can leave it as it is for now.

> the same phrasing) along the line you suggested.

Thank you.

> prohibition lands -- convert the B9 test into an error-case test

Your tentative plan looks good to me.

> be preferable to refreshing the already-reviewed series?

Yes, that's fine. I will apply the delta patch for 0007 / 0009 / 0017
/ 0024 on top of 0031.

Confirmed.

Ok.

Ok.

> Thank you again for the thorough review.

Thank you for your effort!
--
Tatsuo Ishii
SRA OSS K.K.
English: http://www.sraoss.co.jp/index_en/
Japanese:http://www.sraoss.co.jp

---

From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 02:11:00 +0900

Hi Tatsuo,

Attached are 40 incremental patches on top of v46, with all
review-driven follow-ups folded in.  The 0001-0031 portion is
unchanged from the previous send (filenames preserved).  The new
patches 0032-0040 are the delta + follow-up patches we agreed to
deliver on top of 0031, plus two cosmetic patches that surfaced
during the sweep.

As you suggested, I have not refreshed 0001-0031; the new work
is layered on top so that the existing review state on those
patches stays valid.

Numbering and subjects of 0001-0031 are unchanged.  The new
patches are:

  - 0032 Fix A1 frame optimization bypass test in
    rpr_integration

    Per your A1 review.  Switches the baseline to row_number()
    with an explicit ROWS frame so that the bypass guard is
    actually exercised.  Block comment reworded in A2/A3 style.

  - 0033 Fix imperative voice in check_rpr_nav_expr header
    comment

    Per your typo note: "Checks for" -> "Check for".

  - 0034 Fix passive voice in RPR error messages

    Per your style note.  Sweep of RPR-touched code paths
    (parse_rpr.c, parse_func.c, windowfuncs.c) for the
    "is not permitted when ... is used" / "can only be used in"
    patterns, rewritten to "cannot use ... with" /
    "cannot use ... outside".

  - 0035 Use Max()/Min() macros for RPR extremum accumulation

    Per your Max() macro suggestion.  Sweep of all hand-rolled
    "if (x > y) y = x;" sites in createplan.c, nodeWindowAgg.c,
    and execRPR.c.  No double-evaluation hazards on the call
    sites.

  - 0036 Reword RPR navigation function synopses

    Per your sgml note.  "Returns the column value at the row"
    -> "Returns value evaluated at the row" (the first argument
    can be an arbitrary expression).

  - 0037 Replace non-ASCII characters in RPR code and tests

    Per your non-ASCII note.  Sweep of RPR-touched files;
    mostly comments and rpr*.out.

  - 0038 Move RPR design notes from execRPR.c to README.rpr

    Per the README.rpr split we discussed.  Extracts the
    design block at the head of execRPR.c into
    src/backend/executor/README.rpr (plain text, matching the
    executor/README convention).  No code changes.

  - 0039 Apply pgindent canonicalization to RPR comment blocks

    Cosmetic.  pgindent sweep across RPR-touched files; labeled
    blocks wrapped in /*---------- ... *---------- to preserve
    hanging indents.  No code changes.

  - 0040 Reword RPR navigation function descriptions in
    pg_proc.dat

    Cosmetic.  Aligns the eight RPR navigation descr fields
    with the surrounding "fetch the ... row value" pattern used
    by lag / lead / first_value / last_value / nth_value.

On the items deferred to post-v47:

  - DEFINE volatile prohibition: prepared as a self-contained
    follow-up so it can be reviewed independently of v47.

  - 0009 nfaVisitedElems representation: per your decision,
    kept as-is.  Three points behind that:

      (1) Size concern.  visited is 1 bit per element, a flat
          NFA element is 16 bytes -- a fixed 128:1 ratio
          (~0.78%).  The bitmap is dwarfed by the NFA it
          tracks (worst case ~4 KiB next to ~512 KiB), so it
          is not a memory hot spot.

      (2) Bitmapset misfit.  Its invariants ("no trailing zero
          word", "empty set = NULL") map the per-advance reset
          onto pfree + palloc-on-next-add, which defeats the
          in-place / fixed-capacity reuse pattern this code
          relies on.

      (3) Future variant.  A high-water mark approach (track
          the min/max touched word index, memset only that
          span on reset) preserves the in-place pattern and
          shrinks reset cost from O(numElements/64) to
          O(span_words).  Held as a future optimization
          candidate.

  - 0007 / B7 (Recursive CTE XXX): tracked as a discussion item
    awaiting community input on the standard interpretation.

Regards,
Henson
From f79d4358bc0033bc4fe8b4f8c9e32904d3df6a93 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 24 Mar 2026 19:04:19 +0900
Subject: [PATCH 01/40] Remove unused regex/regex.h include from
 nodeWindowAgg.c

---

From 31e07dcbd5391b7ff9ef8293fcb090cf8f845c71 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:25:40 +0900
Subject: [PATCH 02/40] Add CHECK_FOR_INTERRUPTS() to nfa_add_state_unique()
 for state explosion patterns

---

From 0f15fdabc01fc1503f2a13253df65844ece4c86d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 11:03:39 +0900
Subject: [PATCH 03/40] Add CHECK_FOR_INTERRUPTS() to nfa_try_absorb_context()
 loop

---

From 6601dda3d297ee8928bbe1c035102d683c78251f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:20:05 +0900
Subject: [PATCH 04/40] Fix in-place modification of defineClause TargetEntry
 in setrefs.c

---

 	/*

From 31e7dacb8b0fa6ead63ff92c19aa5dfb0cde76a1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 25 Mar 2026 00:37:50 +0900
Subject: [PATCH 05/40] Fix mark handling for last_value() under RPR

 
 out_of_frame:

From b0f0184ef082dbd0a4744d08f6b0b7d5366c2733 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 11:55:02 +0900
Subject: [PATCH 06/40] Fix DEFINE expression handling in RPR window planning

 
+		/*

 		/*
 
-		/*

-		/* -------------------
 
-		/*

From ee4eee935abcc5d0e089b19e88a81e8ecac52607 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 14:09:53 +0900
Subject: [PATCH 07/40] Add RPR planner integration tests

 
 # ----------

From ca171084b7c52e27a1f0bb2a17a8747f375e6a2f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 14:09:12 +0900
Subject: [PATCH 08/40] Replace reduced frame map with single match result

 

 

 			/*
 
 			/*

 
 
 	CHECK_FOR_INTERRUPTS();
 
-#ifdef RPR_DEBUG

 			/*
 
-#ifdef RPR_DEBUG

 

 
 	switch (state)
 	{
 		case RF_FRAME_HEAD:
-			num_reduced_rows = 1;
-			for (i = pos + 1;
 
 		case RF_SKIPPED:
 
 		case RF_UNMATCHED:
+		case RF_EMPTY_MATCH:
 			rtn = -1;
 			break;

-#ifdef RPR_DEBUG

 /*
 
-	/*

-/*

 
 
 /*

 	/*

 

 
 
 /*
 
-	/*
 
 /* ----------------

From 5d9be742c6266d8fbff887a0577c37743d429690 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 21:47:01 +0900
Subject: [PATCH 09/40] Add fixed-length group absorption for RPR

 
 
+		/*
 
 	/*
 
 				/*
 
-					Assert(count == 1);
-

 
+			/*
 
+		/*

+	/*

+/*

 	/*

 

 

 

 

From 7851d85ef7450c20b55035547ecaf8c977c68207 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 22:00:50 +0900
Subject: [PATCH 10/40] Rename rpr_explain test views from sequential numbers
 to descriptive names

 

 

 

 

 

 

 
 
 -- Very long matches
-CREATE VIEW rpr_ev23 AS

 

 
 
 -- Single row
-CREATE VIEW rpr_ev35 AS

 
 -- Multiple alternations
-CREATE VIEW rpr_ev39 AS
 
 -- Optional elements
-CREATE VIEW rpr_ev40 AS
 
 -- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
 
 -- Star quantifier
-CREATE VIEW rpr_ev42 AS

 

 

 -- Exact count {n}
-CREATE VIEW rpr_ev54 AS
 
 -- Range {n,m}
-CREATE VIEW rpr_ev55 AS
 
 -- At least {n,}
-CREATE VIEW rpr_ev56 AS

 

 
 -- Alternation at start
-CREATE VIEW rpr_ev64 AS

 
 -- Alternation at end
-CREATE VIEW rpr_ev67 AS

 
 
 -- Nested groups
-CREATE VIEW rpr_ev72 AS

 

 

 -- Using PREV function
-CREATE VIEW rpr_ev82 AS

 

 

 

 

 

 

 

 

 
 
 -- Very long matches
-CREATE VIEW rpr_ev23 AS

 

 

 -- XML format output
-CREATE VIEW rpr_ev31 AS

 
 -- Empty result set
-CREATE VIEW rpr_ev34 AS
 
 -- Single row
-CREATE VIEW rpr_ev35 AS

 
 -- Nested groups
-CREATE VIEW rpr_ev38 AS
 
 -- Multiple alternations
-CREATE VIEW rpr_ev39 AS
 
 -- Optional elements
-CREATE VIEW rpr_ev40 AS
 
 -- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
 
 -- Star quantifier
-CREATE VIEW rpr_ev42 AS

 

 
 
 -- With INITIAL keyword
-CREATE VIEW rpr_ev49 AS

 -- Plus quantifier
-CREATE VIEW rpr_ev51 AS

 
 -- Exact count {n}
-CREATE VIEW rpr_ev54 AS
 
 -- Range {n,m}
-CREATE VIEW rpr_ev55 AS
 
 -- At least {n,}
-CREATE VIEW rpr_ev56 AS

 
 
 -- Simple alternation
-CREATE VIEW rpr_ev60 AS

 
 
 -- Alternation at start
-CREATE VIEW rpr_ev64 AS

 
 -- Alternation at end
-CREATE VIEW rpr_ev67 AS

 
 -- Simple group
-CREATE VIEW rpr_ev70 AS

 -- Nested groups
-CREATE VIEW rpr_ev72 AS

 

 -- count(*) with pattern
-CREATE VIEW rpr_ev77 AS

 

 -- Using PREV function
-CREATE VIEW rpr_ev82 AS

 

 

 

From 5a5e04eb3becfc5cd185b67243fe15eff5ef6f56 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 08:53:46 +0900
Subject: [PATCH 11/40] Fix quote_identifier() for RPR pattern variable name
 deparse

 

 

 

From b49e64adde991d23799fdfd309e6d996c8d2e5c6 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:14:59 +0900
Subject: [PATCH 12/40] Fix execRPR.o ordering in executor Makefile to match
 meson.build

---

From 80726d6151aa00ae6edf73fc49498aac75f9fb28 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:40:53 +0900
Subject: [PATCH 13/40] Remove unused force_colno parameter from RPR deparse
 functions

---

 
 
-- 
2.50.1 (Apple Git-155)

From 5e4a1d039508c7872adc2506706a7481b3d3755b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:43:07 +0900
Subject: [PATCH 14/40] Add CHECK_FOR_INTERRUPTS to RPR context cleanup and
 finalize loops

---

 

From e54dd341aef218be3dcce088ee46c4d35f24d482 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 09:48:55 +0900
Subject: [PATCH 15/40] Narrow variable scope in ExecInitWindowAgg DEFINE
 clause loop

---

-	TargetEntry *te;
-	Expr	   *expr;
-

-			te = lfirst(l);
-			name = te->resname;
-			expr = te->expr;
-

From 747b855b0bb586e0d0b6164065376079dc5473c5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 11:32:56 +0900
Subject: [PATCH 16/40] Normalize RPR element flag macros to return bool

---

From 79855c1edcf7c411c63ddeab09ac238f2b7d9986 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 20 Mar 2026 23:52:42 +0900
Subject: [PATCH 17/40] Implement 1-slot PREV/NEXT navigation for RPR

 
@@ -318,12 +323,17 @@
         <indexterm>
          <primary>next</primary>
         </indexterm>

+		case T_RPRNavExpr:
+			{
+				/*
 
+#include "common/int.h"

 
-/*

 

 

 
 
 	/*
 
+	/*

 /*
 
-static bool
 
-	if (node == NULL)
-		return false;
+	/*

-/*

-	/*

 

 
+	/*
 
+			case EEOP_RPR_NAV_SET:

 
 /*
  * transformRPR

 
 	return defineClause;
 }
+
+/*
+ * check_rpr_nav_expr
 
+		case T_RPRNavExpr:
+			{
 
 /*
  * prev
 
 /*
  * next

 

 
 
+/*
+ * RPRNavExpr
+ *
 
+--

 
 
+--

From efb99428cfdb41363d49d4b7ca199f9212ba5a6e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 10:54:30 +0900
Subject: [PATCH 18/40] Add JIT compilation support for RPR PREV/NEXT
 navigation

---

 
 			case EEOP_RPR_NAV_SET:

 

From 7d9f1f094dbd685a03d982f9d62a86fb392c877f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 4 Apr 2026 11:49:32 +0900
Subject: [PATCH 19/40] Add tuplestore trim optimization for RPR PREV
 navigation

 
 
+		/*

+/*
 
+/*

 
 	/* RPR navigation */
 
+	/*
 
+/*

From fea456180111bfdb10d4dc580fc4062fe0ae6b15 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 2 Apr 2026 16:06:06 +0900
Subject: [PATCH 20/40] Update RPR code comments to reflect 1-slot navigation
 model

---

From 79f32ee6991e3cbf5bea0e51099e9613c13f5ec0 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 09:48:54 +0900
Subject: [PATCH 21/40] Enable JIT compilation for PREV/NEXT navigation tests
 in RPR

---

 RESET jit_above_cost;
+RESET jit;
 
 --
 -- IGNORE NULLS
-- 
2.50.1 (Apple Git-155)

From 081f70847766b2aa312b667e5b7c8e2a41088378 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 09:53:14 +0900
Subject: [PATCH 22/40] Add 2-arg PREV/NEXT test for row pattern navigation
 with host variable

---

From a4f260651422e7bc364789d493784d5e7f9e7c52 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 6 Apr 2026 10:46:38 +0900
Subject: [PATCH 23/40] Add Nav Mark Lookback to EXPLAIN and fix
 compute_nav_max_offset()

 

 	return maxOffset;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 -- Very long matches

 

 
 
 -- Single row

 
 
 -- Multiple alternations
 
 -- Optional elements
 
 -- Bounded quantifiers
 
 -- Star quantifier

 

 

 

 
 -- Exact count {n}
 
 -- Range {n,m}
 
 -- At least {n,}

 

 

 -- Alternation at start

 
 -- Alternation at end

 

 -- Nested groups

 

 

 
 
 -- Using PREV function

 

 

+RESET plan_cache_mode;

 

 
 SELECT count(*) FROM (

 
 SELECT count(*) FROM (

 

 

 EXECUTE rpr_prev(1);
  id | val | cnt 
 
 EXECUTE rpr_prev(1);
  id | val | cnt 

 

 

From 61571e745b76c864158c7c574136e13c1913c516 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 5 Apr 2026 00:56:20 +0900
Subject: [PATCH 24/40] Implement FIRST/LAST and compound navigation for RPR

 
+      <row>
 
+   <para>

 

 /*
 
 	/*

 		if (offset < 0)
 			ereport(ERROR,
 
 	/*
 
+	/*

 /*
 
+/*

+		/*

 
 /*
 
 	/*
 
 		/*

 
 	/*

 /*

 

 /*
  * eval_nav_max_offset

 

-		/*

 
 
-	list_free(offsets);
 
 /*
 
 		/*

+#include "common/int.h"
 
 /*

 
-				if (c->constisnull)

 
 
 /*

 
-	return maxOffset;
+	return result;
 }
 
 /*

 	/*

 

 

 
 
 static bool

 
-	if (result.has_nav)
-		ereport(ERROR,

 

 	/* RPR navigation */

+/*
 
 	/*

 
-/*

+--
 
+RESET jit_above_cost;
+RESET jit;

 DROP TABLE rpr_null;

 
 RESET plan_cache_mode;
 
+--

 

 
 DROP TABLE rpr_null;

 

 

From c333424313fb8d94ff0aefffb189890db776fefe Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 13:31:44 +0900
Subject: [PATCH 25/40] Guard against int64 overflow in RPR bounded frame end
 computation

---

+#include "common/int.h"

From f3d7cb40298ee33fa37b5bd75c17ad2cb7e20ffe Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 11:15:12 +0900
Subject: [PATCH 26/40] Fix RPR error message style: hint format, terminology,
 capitalization

 

 

 

From 809e8b5c9f79e40ee114bef963e2a902800608ed Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 10:09:41 +0900
Subject: [PATCH 27/40] Fix comment typos, grammar, and inaccuracies in RPR
 code

---

 		/*
 
 /*
  * rpr_is_defined

From 9a81f1981cd930fa1e25288a07f350fbace4c9a5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 10:50:36 +0900
Subject: [PATCH 28/40] Fix RPR documentation: synopsis, grammar, and
 terminology

 
    <para>
 
    <para>
 
    <para>
 
   <para>

@@ -328,12 +329,13 @@
        </para>
        <para>

 <synopsis>
 
    <para>

From c33aca418319816bde883d1ad6b07f7effbdddea Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 13:59:29 +0900
Subject: [PATCH 29/40] Fix nav_slot pass-by-ref dangling pointer in RPR
 navigation

 

 

From 76b60fb538906346144be7430b858ddd471ec3ab Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 20:49:27 +0900
Subject: [PATCH 30/40] Add inline comments for complex RPR algorithms and
 design notes

 
 				/*

 
 		/*
 
 	/*

From a074cdb45ffaa6a46a0b60a581a2eb527d855cad Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 7 Apr 2026 20:52:13 +0900
Subject: [PATCH 31/40] Remove unused include and fix header ordering in RPR
 files

---

-#include "common/int.h"

From 27109165b8c6764089bf106c750c63b7e45dc7a1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 00:10:55 +0900
Subject: [PATCH 32/40] Fix A1 frame optimization bypass test in
 rpr_integration

The non-RPR baseline used count(*) with a default frame.  count's
prosupport does not handle SupportRequestOptimizeWindowClause, so
optimize_window_clauses() was never invoked on the baseline; the
absence of an explicit frame in EXPLAIN was a ruleutils
default-frame omission, not the result of any rewrite.  The plan
diff therefore did not actually exercise the RPR guard.

Use row_number() instead, which has prosupport handling that
request, and give both queries the same explicit input frame
(ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING).  The non-RPR
plan now shows the rewritten frame (ROWS UNBOUNDED PRECEDING)
while the RPR plan keeps it intact, making the guard's effect
observable in EXPLAIN.

Reword the block comment to describe the actual planner action,
and align the inline labels with the surrounding A2/A3 sections.
---
 src/test/regress/expected/rpr_integration.out | 30 +++++++++++--------
 src/test/regress/sql/rpr_integration.sql      | 26 +++++++++-------
 2 files changed, 34 insertions(+), 22 deletions(-)

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 9dbf0f43c8f..0cc79b75601 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -38,26 +38,32 @@ INSERT INTO rpr_integ VALUES
 -- ============================================================
 -- A1. Frame optimization bypass
 -- ============================================================
--- optimize_window_clauses() must not apply frame optimization to RPR windows.
--- Non-RPR case: frame can be optimized (RANGE -> ROWS conversion, etc.).
--- RPR case: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND
--- UNBOUNDED FOLLOWING).
--- Non-RPR window with default frame -> frame optimization applied
+-- Verify that optimize_window_clauses() does not apply frame
+-- optimization to RPR windows.  Both queries below use the same input
+-- frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) with
+-- row_number(), whose prosupport handles
+-- SupportRequestOptimizeWindowClause and triggers frame rewriting.
+-- In the non-RPR baseline the planner rewrites the frame to ROWS
+-- UNBOUNDED PRECEDING, while in the RPR case the guard in
+-- optimize_window_clauses() blocks the rewrite and the frame is
+-- preserved as specified.
+-- Non-RPR baseline: the planner rewrites the frame to ROWS UNBOUNDED PRECEDING.
 EXPLAIN (COSTS OFF)
-SELECT count(*) OVER w FROM rpr_integ
-WINDOW w AS (ORDER BY id);
-            QUERY PLAN             
------------------------------------
+SELECT row_number() OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+                      QUERY PLAN                       
+-------------------------------------------------------
  WindowAgg
-   Window: w AS (ORDER BY id)
+   Window: w AS (ORDER BY id ROWS UNBOUNDED PRECEDING)
    ->  Sort
          Sort Key: id
          ->  Seq Scan on rpr_integ
 (5 rows)
 
--- RPR window -> frame optimization must NOT change the frame
+-- RPR case: the frame is preserved as specified.
 EXPLAIN (COSTS OFF)
-SELECT count(*) OVER w FROM rpr_integ
+SELECT row_number() OVER w FROM rpr_integ
 WINDOW w AS (ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     PATTERN (A B+)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 7d70f5e4b98..6d47728e911 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -40,19 +40,25 @@ INSERT INTO rpr_integ VALUES
 -- ============================================================
 -- A1. Frame optimization bypass
 -- ============================================================
--- optimize_window_clauses() must not apply frame optimization to RPR windows.
--- Non-RPR case: frame can be optimized (RANGE -> ROWS conversion, etc.).
--- RPR case: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND
--- UNBOUNDED FOLLOWING).
-
--- Non-RPR window with default frame -> frame optimization applied
+-- Verify that optimize_window_clauses() does not apply frame
+-- optimization to RPR windows.  Both queries below use the same input
+-- frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) with
+-- row_number(), whose prosupport handles
+-- SupportRequestOptimizeWindowClause and triggers frame rewriting.
+-- In the non-RPR baseline the planner rewrites the frame to ROWS
+-- UNBOUNDED PRECEDING, while in the RPR case the guard in
+-- optimize_window_clauses() blocks the rewrite and the frame is
+-- preserved as specified.
+
+-- Non-RPR baseline: the planner rewrites the frame to ROWS UNBOUNDED PRECEDING.
 EXPLAIN (COSTS OFF)
-SELECT count(*) OVER w FROM rpr_integ
-WINDOW w AS (ORDER BY id);
+SELECT row_number() OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
 
--- RPR window -> frame optimization must NOT change the frame
+-- RPR case: the frame is preserved as specified.
 EXPLAIN (COSTS OFF)
-SELECT count(*) OVER w FROM rpr_integ
+SELECT row_number() OVER w FROM rpr_integ
 WINDOW w AS (ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     PATTERN (A B+)
-- 
2.50.1 (Apple Git-155)

From 015261bd7e853fcf26188d8374e8d9d0259967ba Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 00:19:20 +0900
Subject: [PATCH 33/40] Fix imperative voice in check_rpr_nav_expr header
 comment

The header comment of check_rpr_nav_expr() opens with the
imperative "Validate a single RPRNavExpr node ..." but the
following sentence reverts to the third-person "Checks for
illegal nesting ...".  Drop the trailing s so that both
sentences are imperative and consistent.

Comment-only change; no functional effect.
---
 src/backend/parser/parse_rpr.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 8864b20e6cf..87ccf55a5ee 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -428,7 +428,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 /*
  * check_rpr_nav_expr
  *		Validate a single RPRNavExpr node by walking its arg and offset_arg
- *		subtrees in a single pass each.  Checks for illegal nesting, missing
+ *		subtrees in a single pass each.  Check for illegal nesting, missing
  *		column references, and non-constant offset expressions.
  *
  * Nesting rules (SQL standard 5.6.4):
-- 
2.50.1 (Apple Git-155)

From 10dc83735b042833db79ae7c131b4739c37e7580 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 00:30:19 +0900
Subject: [PATCH 34/40] Fix passive voice in RPR error messages

Several RPR error messages used passive constructions like "X is not
permitted when row pattern recognition is used" or
"<funcname>() can only be used in a DEFINE clause".  PostgreSQL's
message style guide prefers active voice for prohibitions and
constraints; rephrase the affected messages so the action is the
subject.

  - "FRAME option {GROUPS,RANGE} is not permitted when row pattern
    recognition is used" -> "cannot use FRAME option {GROUPS,RANGE}
    with row pattern recognition"
  - "EXCLUDE options are not permitted when row pattern recognition
    is used" -> "cannot use EXCLUDE options with row pattern
    recognition"
  - "FRAME must start at CURRENT ROW when row pattern recognition is
    used" -> "... when using row pattern recognition" (constraint
    form retained, only the trailing passive participle replaced)
  - "row pattern navigation cannot be nested more than two levels
    deep" -> "cannot nest row pattern navigation more than two
    levels deep"
  - "%s can only be used in a DEFINE clause" (parse_func.c) and
    eight matching defensive stubs in windowfuncs.c
    ("PREV()/NEXT()/FIRST()/LAST() can only be used in a DEFINE
    clause") -> "cannot use ... outside a DEFINE clause"

State-describing messages such as "DEFINE variable %s is not used
in PATTERN" and copula constraints like "must be a direct argument
of the outer navigation" / "offset must not be negative" are left
as is; they are not passive in the prohibition sense and follow
existing PG idioms.

Update regression tests (rpr_base.sql/.out and rpr.out) to match
the new wording.
---
 src/backend/parser/parse_func.c        |  2 +-
 src/backend/parser/parse_rpr.c         | 10 ++---
 src/backend/utils/adt/windowfuncs.c    | 16 ++++----
 src/test/regress/expected/rpr.out      | 14 +++----
 src/test/regress/expected/rpr_base.out | 56 +++++++++++++-------------
 src/test/regress/sql/rpr_base.sql      | 24 +++++------
 6 files changed, 61 insertions(+), 61 deletions(-)

diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index aa45a98713f..07bf11965b8 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -790,7 +790,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 		/* Not a column projection — report error */
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("%s can only be used in a DEFINE clause",
+				 errmsg("cannot use %s outside a DEFINE clause",
 						NameListToString(funcname)),
 				 parser_errposition(pstate, location)));
 	}
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 87ccf55a5ee..f56b7db5bc8 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -76,7 +76,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	if (wc->frameOptions & FRAMEOPTION_GROUPS)
 		ereport(ERROR,
 				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("FRAME option GROUPS is not permitted when row pattern recognition is used"),
+				 errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
 				 errhint("Use ROWS instead."),
 				 parser_errposition(pstate,
 									windef->frameLocation >= 0 ?
@@ -84,7 +84,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	if (wc->frameOptions & FRAMEOPTION_RANGE)
 		ereport(ERROR,
 				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("FRAME option RANGE is not permitted when row pattern recognition is used"),
+				 errmsg("cannot use FRAME option RANGE with row pattern recognition"),
 				 errhint("Use ROWS instead."),
 				 parser_errposition(pstate,
 									windef->frameLocation >= 0 ?
@@ -111,7 +111,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 		ereport(ERROR,
 				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("FRAME must start at CURRENT ROW when row pattern recognition is used"),
+				 errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
 				 errdetail("Current frame starts with %s.", startBound),
 				 errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
 				 parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location)));
@@ -137,7 +137,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 		ereport(ERROR,
 				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("EXCLUDE options are not permitted when row pattern recognition is used"),
+				 errmsg("cannot use EXCLUDE options with row pattern recognition"),
 				 errdetail("Frame definition includes %s.", excludeType),
 				 errhint("Remove the EXCLUDE clause from the window definition."),
 				 parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location)));
@@ -494,7 +494,7 @@ check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
 			if (result.nav_count > 1)
 				ereport(ERROR,
 						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("row pattern navigation cannot be nested more than two levels deep"),
+						 errmsg("cannot nest row pattern navigation more than two levels deep"),
 						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
 						 parser_errposition(pstate, nav->location)));
 
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index fb966cae43c..46e7a03a666 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -740,7 +740,7 @@ window_prev(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("PREV() can only be used in a DEFINE clause")));
+			 errmsg("cannot use PREV() outside a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -754,7 +754,7 @@ window_next(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("NEXT() can only be used in a DEFINE clause")));
+			 errmsg("cannot use NEXT() outside a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -768,7 +768,7 @@ window_prev_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("PREV() can only be used in a DEFINE clause")));
+			 errmsg("cannot use PREV() outside a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -782,7 +782,7 @@ window_next_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("NEXT() can only be used in a DEFINE clause")));
+			 errmsg("cannot use NEXT() outside a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -796,7 +796,7 @@ window_first(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("FIRST() can only be used in a DEFINE clause")));
+			 errmsg("cannot use FIRST() outside a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -810,7 +810,7 @@ window_last(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("LAST() can only be used in a DEFINE clause")));
+			 errmsg("cannot use LAST() outside a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -824,7 +824,7 @@ window_first_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("FIRST() can only be used in a DEFINE clause")));
+			 errmsg("cannot use FIRST() outside a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -838,6 +838,6 @@ window_last_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("LAST() can only be used in a DEFINE clause")));
+			 errmsg("cannot use LAST() outside a DEFINE clause")));
 	PG_RETURN_NULL();			/* not reached */
 }
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 32aa8bc3722..3520c5c5d76 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1023,12 +1023,12 @@ WINDOW w AS (
 --
 -- PREV outside DEFINE clause
 SELECT prev(price) FROM stock;
-ERROR:  prev can only be used in a DEFINE clause
+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:  next can only be used in a DEFINE clause
+ERROR:  cannot use next outside a DEFINE clause
 LINE 1: SELECT next(price) FROM stock;
                ^
 -- Nested PREV
@@ -1431,11 +1431,11 @@ WINDOW w AS (
 
 -- 2-arg PREV/NEXT outside DEFINE clause
 SELECT prev(price, 2) FROM stock;
-ERROR:  prev can only be used in a DEFINE clause
+ERROR:  cannot use prev outside a DEFINE clause
 LINE 1: SELECT prev(price, 2) FROM stock;
                ^
 SELECT next(price, 2) FROM stock;
-ERROR:  next can only be used in a DEFINE clause
+ERROR:  cannot use next outside a DEFINE clause
 LINE 1: SELECT next(price, 2) FROM stock;
                ^
 -- 2-arg PREV/NEXT: negative offset
@@ -1967,15 +1967,15 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
 ERROR:  row pattern navigation offset must not be negative
 -- FIRST/LAST outside DEFINE clause (error cases)
 SELECT first(val) FROM rpr_nav;
-ERROR:  first can only be used in a DEFINE clause
+ERROR:  cannot use first outside a DEFINE clause
 LINE 1: SELECT first(val) FROM rpr_nav;
                ^
 SELECT last(val) FROM rpr_nav;
-ERROR:  last can only be used in a DEFINE clause
+ERROR:  cannot use last outside a DEFINE clause
 LINE 1: SELECT last(val) FROM rpr_nav;
                ^
 SELECT first(val, 1) FROM rpr_nav;
-ERROR:  first can only be used in a DEFINE clause
+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
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 912bd7b7c77..3637a5c8002 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -402,7 +402,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME must start at CURRENT ROW when row pattern recognition is used
+ERROR:  FRAME must start at CURRENT ROW when using row pattern recognition
 LINE 5:     ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
@@ -419,12 +419,12 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  EXCLUDE options are not permitted when row pattern recognition is used
+ERROR:  cannot use EXCLUDE options with row pattern recognition
 LINE 6:     EXCLUDE CURRENT ROW
             ^
 DETAIL:  Frame definition includes EXCLUDE CURRENT ROW.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- EXCLUDE GROUP not permitted
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -435,12 +435,12 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  EXCLUDE options are not permitted when row pattern recognition is used
+ERROR:  cannot use EXCLUDE options with row pattern recognition
 LINE 6:     EXCLUDE GROUP
             ^
 DETAIL:  Frame definition includes EXCLUDE GROUP.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- EXCLUDE TIES not permitted
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -451,12 +451,12 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  EXCLUDE options are not permitted when row pattern recognition is used
+ERROR:  cannot use EXCLUDE options with row pattern recognition
 LINE 6:     EXCLUDE TIES
             ^
 DETAIL:  Frame definition includes EXCLUDE TIES.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- RANGE frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -466,11 +466,11 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
+ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 5:     RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 -- GROUPS frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -480,11 +480,11 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME option GROUPS is not permitted when row pattern recognition is used
+ERROR:  cannot use FRAME option GROUPS with row pattern recognition
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -494,7 +494,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME must start at CURRENT ROW when row pattern recognition is used
+ERROR:  FRAME must start at CURRENT ROW when using row pattern recognition
 LINE 5:     ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with offset PRECEDING.
@@ -509,7 +509,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME must start at CURRENT ROW when row pattern recognition is used
+ERROR:  FRAME must start at CURRENT ROW when using row pattern recognition
 LINE 5:     ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with offset FOLLOWING.
@@ -637,11 +637,11 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
-ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
+ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 5:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -653,11 +653,11 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
-ERROR:  FRAME option GROUPS is not permitted when row pattern recognition is used
+ERROR:  cannot use FRAME option GROUPS with row pattern recognition
 LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 DROP TABLE rpr_frame;
 -- ============================================================
 -- PARTITION BY + FRAME Tests
@@ -702,11 +702,11 @@ WINDOW w AS (
     DEFINE A AS val >= 10, B AS val >= 20
 )
 ORDER BY id;
-ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
+ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 6:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 DROP TABLE rpr_partition;
 -- ============================================================
 -- PATTERN Syntax Tests
@@ -1710,10 +1710,10 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
-ERROR:  prev can only be used in a DEFINE clause
+ERROR:  cannot use prev outside a DEFINE clause
 LINE 1: SELECT PREV(id), id, val, COUNT(*) OVER w as cnt
                ^
--- Expected: ERROR: prev can only be used in a DEFINE clause
+-- Expected: ERROR: cannot use prev outside a DEFINE clause
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1726,10 +1726,10 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
-ERROR:  next can only be used in a DEFINE clause
+ERROR:  cannot use next outside a DEFINE clause
 LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
                ^
--- Expected: ERROR: next can only be used in a DEFINE clause
+-- Expected: ERROR: cannot use next outside a DEFINE clause
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1795,16 +1795,16 @@ ORDER BY id;
 
 -- FIRST function cannot be used other than in DEFINE
 SELECT FIRST(id), id, val FROM rpr_nav;
-ERROR:  first can only be used in a DEFINE clause
+ERROR:  cannot use first outside a DEFINE clause
 LINE 1: SELECT FIRST(id), id, val FROM rpr_nav;
                ^
--- Expected: ERROR: first can only be used in a DEFINE clause
+-- Expected: ERROR: cannot use first outside a DEFINE clause
 -- LAST function cannot be used other than in DEFINE
 SELECT LAST(id), id, val FROM rpr_nav;
-ERROR:  last can only be used in a DEFINE clause
+ERROR:  cannot use last outside a DEFINE clause
 LINE 1: SELECT LAST(id), id, val FROM rpr_nav;
                ^
--- Expected: ERROR: last can only be used in a DEFINE clause
+-- Expected: ERROR: cannot use last outside a DEFINE clause
 DROP TABLE rpr_nav;
 -- ============================================================
 -- SKIP TO / INITIAL Tests
@@ -3272,7 +3272,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(FIRST(PREV(v))) > 0
 );
-ERROR:  row pattern navigation cannot be nested more than two levels deep
+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.
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index f9d5aa89d7a..90b1ae96a0e 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -351,7 +351,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- EXCLUDE GROUP not permitted
 SELECT COUNT(*) OVER w
@@ -363,7 +363,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- EXCLUDE TIES not permitted
 SELECT COUNT(*) OVER w
@@ -375,7 +375,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- RANGE frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
@@ -386,7 +386,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
 -- GROUPS frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
@@ -397,7 +397,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
@@ -504,7 +504,7 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -517,7 +517,7 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 
 DROP TABLE rpr_frame;
 
@@ -557,7 +557,7 @@ WINDOW w AS (
     DEFINE A AS val >= 10, B AS val >= 20
 )
 ORDER BY id;
--- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition

@@ -1262,7 +1262,7 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
--- Expected: ERROR: prev can only be used in a DEFINE clause
+-- Expected: ERROR: cannot use prev outside a DEFINE clause
 
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
@@ -1276,7 +1276,7 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
--- Expected: ERROR: next can only be used in a DEFINE clause
+-- Expected: ERROR: cannot use next outside a DEFINE clause
 
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1319,11 +1319,11 @@ ORDER BY id;
 
 -- FIRST function cannot be used other than in DEFINE
 SELECT FIRST(id), id, val FROM rpr_nav;
--- Expected: ERROR: first can only be used in a DEFINE clause
+-- Expected: ERROR: cannot use first outside a DEFINE clause
 
 -- LAST function cannot be used other than in DEFINE
 SELECT LAST(id), id, val FROM rpr_nav;
--- Expected: ERROR: last can only be used in a DEFINE clause
+-- Expected: ERROR: cannot use last outside a DEFINE clause
 
 DROP TABLE rpr_nav;
 
-- 
2.50.1 (Apple Git-155)

From c4d1fab1046b2f1d5c60ac71c0e139f0bc5eabce Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 00:46:05 +0900
Subject: [PATCH 35/40] Use Max()/Min() macros for RPR extremum accumulation

The RPR planner and executor accumulated extremum values (max
backward reach, min forward offset, NFA state/context high-water
marks, length statistics) with hand-written
"if (X > current) current = X" idioms.  Replace these with the
existing Max()/Min() macros from c.h, matching the convention used
elsewhere in the codebase.

Six Max() sites and five Min() sites are converted across
createplan.c (nav_offset_walker), nodeWindowAgg.c
(eval_nav_max_offset_walker, eval_nav_first_offset_walker,
ExecWindowAgg navmark update), and execRPR.c (NFA state/context
allocators and length stats updater).  Behavior is unchanged; all
operands are simple struct members or local variables, so the
macro's double-evaluation caveat does not apply.
---
 src/backend/executor/execRPR.c          | 14 ++++++--------
 src/backend/executor/nodeWindowAgg.c    |  9 +++------
 src/backend/optimizer/plan/createplan.c | 16 ++++------------
 3 files changed, 13 insertions(+), 26 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 7ba7b6fb672..84f09093b4b 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1789,8 +1789,8 @@ nfa_state_alloc(WindowAggState *winstate)
 	/* Update statistics */
 	winstate->nfaStatesActive++;
 	winstate->nfaStatesTotalCreated++;
-	if (winstate->nfaStatesActive > winstate->nfaStatesMax)
-		winstate->nfaStatesMax = winstate->nfaStatesActive;
+	winstate->nfaStatesMax = Max(winstate->nfaStatesMax,
+								 winstate->nfaStatesActive);
 
 	return state;
 }
@@ -2001,8 +2001,8 @@ nfa_context_alloc(WindowAggState *winstate)
 	/* Update statistics */
 	winstate->nfaContextsActive++;
 	winstate->nfaContextsTotalCreated++;
-	if (winstate->nfaContextsActive > winstate->nfaContextsMax)
-		winstate->nfaContextsMax = winstate->nfaContextsActive;
+	winstate->nfaContextsMax = Max(winstate->nfaContextsMax,
+								   winstate->nfaContextsActive);
 
 	return ctx;
 }
@@ -2046,10 +2046,8 @@ nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen)
 	}
 	else
 	{
-		if (newLen < stats->min)
-			stats->min = newLen;
-		if (newLen > stats->max)
-			stats->max = newLen;
+		stats->min = Min(stats->min, newLen);
+		stats->max = Max(stats->max, newLen);
 	}
 	stats->total += newLen;
 }
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 02f17e5472c..89e66c73735 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2536,8 +2536,7 @@ ExecWindowAgg(PlanState *pstate)
 						+ winstate->navFirstOffset;
 				else
 					firstreach = 0;
-				if (firstreach < navmarkpos)
-					navmarkpos = firstreach;
+				navmarkpos = Min(navmarkpos, firstreach);
 			}
 
 			if (navmarkpos > winstate->nav_winobj->markpos)
@@ -4007,8 +4006,7 @@ eval_nav_max_offset_walker(Node *node, void *ctx)
 				reach = (inner > outer) ? inner - outer : 0;
 		}
 
-		if (reach > context->maxOffset)
-			context->maxOffset = reach;
+		context->maxOffset = Max(context->maxOffset, reach);
 
 		return false;			/* don't walk into children */
 	}
@@ -4118,8 +4116,7 @@ eval_nav_first_offset_walker(Node *node, void *ctx)
 			}
 		}
 
-		if (combined < context->minOffset)
-			context->minOffset = combined;
+		context->minOffset = Min(context->minOffset, combined);
 
 		return false;
 	}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 50668f3b7ab..d7adf17a242 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2551,10 +2551,7 @@ nav_offset_walker(Node *node, void *ctx)
 				int64		offset;
 
 				if (extract_const_offset(nav->offset_arg, 1, &offset))
-				{
-					if (offset > context->maxOffset)
-						context->maxOffset = offset;
-				}
+					context->maxOffset = Max(context->maxOffset, offset);
 				else
 					context->maxNeedsEval = true;
 			}
@@ -2573,10 +2570,7 @@ nav_offset_walker(Node *node, void *ctx)
 				int64		offset;
 
 				if (extract_const_offset(nav->offset_arg, 0, &offset))
-				{
-					if (offset < context->firstOffset)
-						context->firstOffset = offset;
-				}
+					context->firstOffset = Min(context->firstOffset, offset);
 				else
 					context->firstNeedsEval = true;
 			}
@@ -2610,8 +2604,7 @@ nav_offset_walker(Node *node, void *ctx)
 					else
 						combined = (inner > outer) ? inner - outer : 0;
 
-					if (combined > context->maxOffset)
-						context->maxOffset = combined;
+					context->maxOffset = Max(context->maxOffset, combined);
 				}
 				else
 					context->maxNeedsEval = true;
@@ -2659,8 +2652,7 @@ nav_offset_walker(Node *node, void *ctx)
 							combined = INT64_MAX;
 					}
 
-					if (combined < context->firstOffset)
-						context->firstOffset = combined;
+					context->firstOffset = Min(context->firstOffset, combined);
 				}
 				else
 					context->firstNeedsEval = true;
-- 
2.50.1 (Apple Git-155)

From 9ec53a2c53e1aa80c40e7997981021b0c1060297 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 00:51:47 +0900
Subject: [PATCH 36/40] Reword RPR navigation function synopses

The first-line synopses of prev/next/first/last in func-window.sgml
read "Returns the column value at the row ...".  This is too narrow:
the first argument is an arbitrary expression (e.g. first(col1 +
col2)), not just a column reference.

Reword each of the four entries to "Returns <value> evaluated at the
row that is <offset> rows ...", matching the wording already used by
lag, lead, first_value, and last_value in the same table.  This both
addresses the narrowness and aligns the navigation functions with
the established window-function documentation idiom.

Documentation-only change.
---
 doc/src/sgml/func/func-window.sgml | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index d109a2d22bc..d1da105ad0f 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -308,8 +308,9 @@
         <returnvalue>anyelement</returnvalue>
        </para>
        <para>
-        Returns the column value at the row <parameter>offset</parameter>
-        rows before the current row within the partition;
+        Returns <parameter>value</parameter> evaluated at the row that is
+        <parameter>offset</parameter> rows before the current row within
+        the partition;
         returns NULL if the target row is outside the partition.
         <parameter>offset</parameter> defaults to 1 if omitted.
         <parameter>offset</parameter> must be a non-negative integer;
@@ -328,8 +329,9 @@
         <returnvalue>anyelement</returnvalue>
        </para>
        <para>
-        Returns the column value at the row <parameter>offset</parameter>
-        rows after the current row within the partition;
+        Returns <parameter>value</parameter> evaluated at the row that is
+        <parameter>offset</parameter> rows after the current row within
+        the partition;
         returns NULL if the target row is outside the partition.
         <parameter>offset</parameter> defaults to 1 if omitted.
         <parameter>offset</parameter> must be a non-negative integer;
@@ -348,8 +350,8 @@
         <returnvalue>anyelement</returnvalue>
        </para>
        <para>
-        Returns the column value at the row <parameter>offset</parameter>
-        rows after the match start row;
+        Returns <parameter>value</parameter> evaluated at the row that is
+        <parameter>offset</parameter> rows after the match start row;
         returns NULL if the target row is beyond the current row.
         <parameter>offset</parameter> defaults to 0 if omitted, referring to the
         match start row itself.
@@ -368,8 +370,9 @@
         <returnvalue>anyelement</returnvalue>
        </para>
        <para>
-        Returns the column value at the row <parameter>offset</parameter>
-        rows before the current row within the match;
+        Returns <parameter>value</parameter> evaluated at the row that is
+        <parameter>offset</parameter> rows before the current row within
+        the match;
         returns NULL if the target row is before the match start row.
         <parameter>offset</parameter> defaults to 0 if omitted, referring to the
         current row itself.
-- 
2.50.1 (Apple Git-155)

From 1c5d2a431ca85a5e74b4448aacb8c8cc1da0dd0c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 00:59:10 +0900
Subject: [PATCH 37/40] Replace non-ASCII characters in RPR code and tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The RPR patches accumulated a few Unicode characters (arrows, the
not-equal sign, plus-minus, em dash, section sign) in regression
test comments and a handful of source-code comments.  The PostgreSQL
project conventionally keeps source and regression test files in
plain ASCII.  Replace these with their ASCII equivalents:

  ->  for U+2192 RIGHTWARDS ARROW
  !=  for U+2260 NOT EQUAL TO
  +/- for U+00B1 PLUS-MINUS SIGN
  --  for U+2014 EM DASH
  SQL standard <n>  for "§<n>" references (matching the existing
                    "SQL standard 5.6.4" idiom in parse_rpr.c)

Affected: createplan.c, execRPR.c, parse_func.c (comments), and the
rpr / rpr_base / rpr_nfa regression test files (sql + expected).
Two short comment line breaks in createplan.c are reflowed by
pgindent as a mechanical consequence of the slightly longer ASCII
replacements.

Documentation/comment-only change; no functional effect.
---
 src/backend/executor/execRPR.c          |  2 +-
 src/backend/optimizer/plan/createplan.c |  8 ++---
 src/backend/parser/parse_func.c         |  2 +-
 src/test/regress/expected/rpr.out       | 30 ++++++++--------
 src/test/regress/expected/rpr_base.out  |  6 ++--
 src/test/regress/expected/rpr_nfa.out   | 48 ++++++++++++-------------
 src/test/regress/sql/rpr.sql            | 30 ++++++++--------
 src/test/regress/sql/rpr_base.sql       |  6 ++--
 src/test/regress/sql/rpr_nfa.sql        | 48 ++++++++++++-------------
 9 files changed, 90 insertions(+), 90 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 84f09093b4b..15e439daaae 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -631,7 +631,7 @@
  * with a compound kind (RPR_NAV_PREV_FIRST, etc.).  The executor
  * computes the target position in two steps: first the inner reference
  * point (match_start + N or currentpos - N) with match-range validation,
- * then the outer adjustment (± M) with partition-range validation.
+ * then the outer adjustment (+/- M) with partition-range validation.
  * If either step is out of range, the result is NULL.
  *
  * nav_slot caches the last fetched position (nav_slot_pos) to avoid
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index d7adf17a242..a1643e40be4 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2578,8 +2578,8 @@ nav_offset_walker(Node *node, void *ctx)
 
 		/*
 		 * Compound PREV_LAST / NEXT_LAST: base = currentpos. PREV_LAST(v, N,
-		 * M): target = currentpos - N - M → lookback = N + M NEXT_LAST(v,
-		 * N, M): target = currentpos - N + M → lookback = max(N - M, 0)
+		 * M): target = currentpos - N - M -> lookback = N + M NEXT_LAST(v, N,
+		 * M): target = currentpos - N + M -> lookback = max(N - M, 0)
 		 */
 		if (nav->kind == RPR_NAV_PREV_LAST ||
 			nav->kind == RPR_NAV_NEXT_LAST)
@@ -2614,8 +2614,8 @@ nav_offset_walker(Node *node, void *ctx)
 		/*
 		 * Compound PREV_FIRST / NEXT_FIRST: base = match_start. PREV_FIRST(v,
 		 * N, M): target = match_start + N - M NEXT_FIRST(v, N, M): target =
-		 * match_start + N + M The combined offset (N±M) from match_start can
-		 * be negative, meaning rows before match_start are needed.
+		 * match_start + N + M The combined offset (N+/-M) from match_start
+		 * can be negative, meaning rows before match_start are needed.
 		 */
 		if (nav->kind == RPR_NAV_PREV_FIRST ||
 			nav->kind == RPR_NAV_NEXT_FIRST)
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 07bf11965b8..07eccd6de32 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -787,7 +787,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 				return projection;
 		}
 
-		/* Not a column projection — report error */
+		/* Not a column projection -- report error */
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("cannot use %s outside a DEFINE clause",
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 3520c5c5d76..85384f6b096 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1723,9 +1723,9 @@ WINDOW w AS (
 CREATE TEMP TABLE rpr_nav (id int, val int);
 INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10);
 -- FIRST(val) = constant: B matches when match_start has val=10
--- match_start=1(10): A=id1, B=id2, FIRST(val)=10 → match {1,2}
--- match_start=3(30): A=id3, B=id4, FIRST(val)=30≠10 → no match
--- match_start=4(10): A=id4, B=id5, FIRST(val)=10 → match {4,5}
+-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 -> match {1,2}
+-- match_start=3(30): A=id3, B=id4, FIRST(val)=30!=10 -> no match
+-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 -> match {4,5}
 SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -1767,8 +1767,8 @@ FROM rpr_nav WINDOW w AS (
 -- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where
 -- first and last rows have the same val.
 -- match_start=1(10): reluctant tries B early:
---   id2(20≠10), id3(30≠10), id4(10=10) → match {1,2,3,4}
--- match_start=5(50): id6(10≠50) → no match
+--   id2(20!=10), id3(30!=10), id4(10=10) -> match {1,2,3,4}
+-- match_start=5(50): id6(10!=50) -> no match
 SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -1790,7 +1790,7 @@ FROM rpr_nav WINDOW w AS (
 -- Greedy A+ with FIRST(val) = LAST(val): find longest match where
 -- first and last rows have the same val.
 -- match_start=1(10): greedy A eats all, B tries last:
---   id6(10=10) → match {1,2,3,4,5,6}
+--   id6(10=10) -> match {1,2,3,4,5,6}
 SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -1851,8 +1851,8 @@ FROM rpr_nav WINDOW w AS (
 (6 rows)
 
 -- FIRST(val, 1): match_start + 1 row (second row of match)
--- match_start=1(10): FIRST(val,1)=20, B needs val=20 → id2(20) match, id3(30) no
--- match_start=3(30): FIRST(val,1)=10, B needs val=10 → id4(10) match
+-- match_start=1(10): FIRST(val,1)=20, B needs val=20 -> id2(20) match, id3(30) no
+-- match_start=3(30): FIRST(val,1)=10, B needs val=10 -> id4(10) match
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -1871,7 +1871,7 @@ FROM rpr_nav WINDOW w AS (
   6 |  10 |    |   0
 (6 rows)
 
--- FIRST(val, 99): offset beyond match range → NULL, no match
+-- FIRST(val, 99): offset beyond match range -> NULL, no match
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -1930,7 +1930,7 @@ FROM rpr_nav WINDOW w AS (
   6 |  10 |    |   0
 (6 rows)
 
--- LAST(val, 99): offset before match_start → NULL
+-- LAST(val, 99): offset before match_start -> NULL
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -1991,8 +1991,8 @@ DROP TABLE rpr_names;
 -- Compound navigation: PREV(FIRST(val), M)
 -- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10)
 -- PREV(FIRST(val), 1): target = match_start + 0 - 1 = match_start - 1
--- At match_start=1: target=0 → out of range → NULL
--- At match_start=3: target=2(val=20) → 20 > 0 → true
+-- At match_start=1: target=0 -> out of range -> NULL
+-- At match_start=3: target=2(val=20) -> 20 > 0 -> true
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -2012,7 +2012,7 @@ FROM rpr_nav WINDOW w AS (
 (6 rows)
 
 -- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2
--- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 → true
+-- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 -> true
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -2160,7 +2160,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
 );
 ERROR:  row pattern navigation offset must not be negative
 -- Compound: default offsets on both sides
--- PREV(FIRST(val)): inner=0 (match_start), outer=1 → target = match_start - 1
+-- PREV(FIRST(val)): inner=0 (match_start), outer=1 -> target = match_start - 1
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -2179,7 +2179,7 @@ FROM rpr_nav WINDOW w AS (
   6 |  10 |    |   0
 (6 rows)
 
--- NEXT(LAST(val)): inner=0 (currentpos), outer=1 → target = currentpos + 1
+-- NEXT(LAST(val)): inner=0 (currentpos), outer=1 -> target = currentpos + 1
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 3637a5c8002..a63211ff364 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3065,7 +3065,7 @@ LINE 6:     DEFINE A AS val > 0
             ^
 -- Expected: Syntax error
 -- Qualified column references (NOT SUPPORTED)
--- Pattern variable qualified name: not supported (valid per §4.16, not yet implemented)
+-- Pattern variable qualified name: not supported (valid per SQL standard 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3104,7 +3104,7 @@ ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
 -- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
--- FROM-clause range variable qualified name: not allowed (prohibited by §6.5)
+-- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -5821,7 +5821,7 @@ CREATE TABLE rpr_sort (id INT, category VARCHAR(10), val INT);
 INSERT INTO rpr_sort VALUES
     (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
--- RPR with GROUP BY (aggregate in DEFINE → ERROR before GROUP BY interaction)
+-- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction)
 -- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 250f7f131b1..a19b26c3b94 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1151,7 +1151,7 @@ WINDOW w AS (
 
 -- Optional reluctant group: (A B)?? C
 -- nfa_advance_begin: reluctant tries skip first, but skip path needs C
--- at row 1 which is A → skip fails. Enter path succeeds: A(1) B(2) C(3).
+-- at row 1 which is A -> skip fails. Enter path succeeds: A(1) B(2) C(3).
 WITH test_optional_reluctant AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2961,8 +2961,8 @@ WINDOW w AS (
   9 | {_}   |             |          
 (9 rows)
 
--- Nested END→END max reached
--- Inner group (A B){2} reaches max=2 → exits to outer END
+-- Nested END->END max reached
+-- Inner group (A B){2} reaches max=2 -> exits to outer END
 WITH test_end_nested_max AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3002,8 +3002,8 @@ WINDOW w AS (
   9 | {_}   |             |          
 (9 rows)
 
--- Nested END→END between min/max
--- Inner group (A B){1,3} exits between min/max → outer END count++
+-- Nested END->END between min/max
+-- Inner group (A B){1,3} exits between min/max -> outer END count++
 WITH test_end_nested_mid AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3884,9 +3884,9 @@ WINDOW w AS (
 
 -- Coverage failure: older can't cover newer's states
 -- Pattern A+ | B+ with SKIP PAST LAST ROW.
--- Row 1: only A → Ctx1 takes A branch only (B fails).
--- Row 2: A and B → Ctx2 takes both branches.
--- Absorption: Ctx1 has A but no B → can't cover Ctx2's B state → fails.
+-- Row 1: only A -> Ctx1 takes A branch only (B fails).
+-- Row 2: A and B -> Ctx2 takes both branches.
+-- Absorption: Ctx1 has A but no B -> can't cover Ctx2's B state -> fails.
 WITH test_coverage_fail AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A', '_']),
@@ -3920,8 +3920,8 @@ WINDOW w AS (
 
 -- Absorb skips completed context (older->states==NULL)
 -- Pattern A+ | B+ with SKIP PAST LAST ROW.
--- Row 1: A only → Ctx1 takes A branch. Row 2: B only → Ctx1 A fails (completed).
--- Ctx2 takes B branch. Absorption: Ctx1 states==NULL → skip.
+-- Row 1: A only -> Ctx1 takes A branch. Row 2: B only -> Ctx1 A fails (completed).
+-- Ctx2 takes B branch. Absorption: Ctx1 states==NULL -> skip.
 WITH test_older_completed AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3953,9 +3953,9 @@ WINDOW w AS (
 
 -- Absorb skips non-absorbable context (!hasAbsorbableState)
 -- Pattern A+ | B C with SKIP PAST LAST ROW (only A+ branch absorbable).
--- Row 1: B only → Ctx1 takes B branch (non-absorbable), advances to C.
--- Row 2: C,A → Ctx1 C matches (hasAbsorbableState=false). Ctx2 takes A (absorbable).
--- Absorption: Ctx1 !hasAbsorbableState → skip.
+-- Row 1: B only -> Ctx1 takes B branch (non-absorbable), advances to C.
+-- Row 2: C,A -> Ctx1 C matches (hasAbsorbableState=false). Ctx2 takes A (absorbable).
+-- Absorption: Ctx1 !hasAbsorbableState -> skip.
 WITH test_older_non_absorbable AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B', '_']),
@@ -4397,7 +4397,7 @@ WINDOW w AS (
 -- (A?){0,3}: min=0, nullable inner.
 -- A never matches. A? matches empty, min=0 satisfied immediately.
 -- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration → no match (same as {2,3})
+-- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4427,7 +4427,7 @@ WINDOW w AS (
 -- (A?){1,3}: min=1, nullable inner.
 -- A never matches. Need 1 empty iteration to satisfy min=1.
 -- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration → no match (same as {2,3})
+-- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4458,7 +4458,7 @@ WINDOW w AS (
 -- A never matches. Need 2 empty iterations to satisfy min=2.
 -- Per standard: STR06=(STRE STRE) is valid for min=2.
 -- Expected: empty match for every row
--- XXX: visited bitmap blocks second empty iteration → match failure
+-- XXX: visited bitmap blocks second empty iteration -> match failure
 WITH test_728_min2 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4486,10 +4486,10 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){2,3} mixed: some rows match A, some don't
--- Rows 1-2: A matches, greedy takes 2 → min satisfied
+-- Rows 1-2: A matches, greedy takes 2 -> min satisfied
 -- Row 3: A doesn't match, needs 2 empty iterations for min=2
 -- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
--- Row 4: A matches 1 real iter + 1 ff empty exit → match 4-4
+-- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -4524,8 +4524,8 @@ WINDOW w AS (
 -- fires as a parallel exit path (EXIT ONLY, no greedy/reluctant loop).
 -- Data: alternating A, B rows (6 rows)
 -- Greedy: each row gets the longest match from its starting position.
--- Row 1: 3 iters (A@1,B@2)(A@3,B@4)(A@5,B@6) → 1-6
--- Row 5: 1 real iter + 1 ff empty exit → 5-6
+-- Row 1: 3 iters (A@1,B@2)(A@3,B@4)(A@5,B@6) -> 1-6
+-- Row 5: 1 real iter + 1 ff empty exit -> 5-6
 WITH test_728_multi_body AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -4590,9 +4590,9 @@ WINDOW w AS (
 (3 rows)
 
 -- (A? B?){2,3}: mixed real and empty iterations
--- Row 1: iter1 real (A@1,B@2), iter2 at row 3 empty → ff exit, match 1-2
--- Row 3: C doesn't match A or B → NULL
--- Row 4: iter1 real (A@4,B@5), iter2 at end empty → ff exit, match 4-5
+-- Row 1: iter1 real (A@1,B@2), iter2 at row 3 empty -> ff exit, match 1-2
+-- Row 3: C doesn't match A or B -> NULL
+-- Row 4: iter1 real (A@4,B@5), iter2 at end empty -> ff exit, match 4-5
 WITH test_728_multi_mixed AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -4629,7 +4629,7 @@ WINDOW w AS (
 -- ------------------------------------------------------------
 -- Standard's worked example: A? B+ with specific data
 -- Preferment order: (A)(BBB), (A)(BB), (A)(B), ()(BBB), ()(BB), ()(B)
--- Row 1: A condition (price>100) is false → A fails
+-- Row 1: A condition (price>100) is false -> A fails
 -- Backtrack: empty A?, then B+ from row 1
 -- Expected: rows 1-3 match as B (A? takes empty match)
 WITH test_73_example AS (
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 724d460b2da..5563e062cde 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -820,9 +820,9 @@ CREATE TEMP TABLE rpr_nav (id int, val int);
 INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10);
 
 -- FIRST(val) = constant: B matches when match_start has val=10
--- match_start=1(10): A=id1, B=id2, FIRST(val)=10 → match {1,2}
--- match_start=3(30): A=id3, B=id4, FIRST(val)=30≠10 → no match
--- match_start=4(10): A=id4, B=id5, FIRST(val)=10 → match {4,5}
+-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 -> match {1,2}
+-- match_start=3(30): A=id3, B=id4, FIRST(val)=30!=10 -> no match
+-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 -> match {4,5}
 SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -846,8 +846,8 @@ FROM rpr_nav WINDOW w AS (
 -- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where
 -- first and last rows have the same val.
 -- match_start=1(10): reluctant tries B early:
---   id2(20≠10), id3(30≠10), id4(10=10) → match {1,2,3,4}
--- match_start=5(50): id6(10≠50) → no match
+--   id2(20!=10), id3(30!=10), id4(10=10) -> match {1,2,3,4}
+-- match_start=5(50): id6(10!=50) -> no match
 SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -860,7 +860,7 @@ FROM rpr_nav WINDOW w AS (
 -- Greedy A+ with FIRST(val) = LAST(val): find longest match where
 -- first and last rows have the same val.
 -- match_start=1(10): greedy A eats all, B tries last:
---   id6(10=10) → match {1,2,3,4,5,6}
+--   id6(10=10) -> match {1,2,3,4,5,6}
 SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -894,8 +894,8 @@ FROM rpr_nav WINDOW w AS (
 );
 
 -- FIRST(val, 1): match_start + 1 row (second row of match)
--- match_start=1(10): FIRST(val,1)=20, B needs val=20 → id2(20) match, id3(30) no
--- match_start=3(30): FIRST(val,1)=10, B needs val=10 → id4(10) match
+-- match_start=1(10): FIRST(val,1)=20, B needs val=20 -> id2(20) match, id3(30) no
+-- match_start=3(30): FIRST(val,1)=10, B needs val=10 -> id4(10) match
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -905,7 +905,7 @@ FROM rpr_nav WINDOW w AS (
     DEFINE A AS TRUE, B AS val = FIRST(val, 1)
 );
 
--- FIRST(val, 99): offset beyond match range → NULL, no match
+-- FIRST(val, 99): offset beyond match range -> NULL, no match
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -937,7 +937,7 @@ FROM rpr_nav WINDOW w AS (
     DEFINE A AS TRUE, B AS LAST(val, 1) < 30
 );
 
--- LAST(val, 99): offset before match_start → NULL
+-- LAST(val, 99): offset before match_start -> NULL
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -977,8 +977,8 @@ DROP TABLE rpr_names;
 -- Compound navigation: PREV(FIRST(val), M)
 -- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10)
 -- PREV(FIRST(val), 1): target = match_start + 0 - 1 = match_start - 1
--- At match_start=1: target=0 → out of range → NULL
--- At match_start=3: target=2(val=20) → 20 > 0 → true
+-- At match_start=1: target=0 -> out of range -> NULL
+-- At match_start=3: target=2(val=20) -> 20 > 0 -> true
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -989,7 +989,7 @@ FROM rpr_nav WINDOW w AS (
 );
 
 -- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2
--- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 → true
+-- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 -> true
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -1074,7 +1074,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
 );
 
 -- Compound: default offsets on both sides
--- PREV(FIRST(val)): inner=0 (match_start), outer=1 → target = match_start - 1
+-- PREV(FIRST(val)): inner=0 (match_start), outer=1 -> target = match_start - 1
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
@@ -1084,7 +1084,7 @@ FROM rpr_nav WINDOW w AS (
     DEFINE A AS TRUE, B AS PREV(FIRST(val)) IS NOT NULL
 );
 
--- NEXT(LAST(val)): inner=0 (currentpos), outer=1 → target = currentpos + 1
+-- NEXT(LAST(val)): inner=0 (currentpos), outer=1 -> target = currentpos + 1
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
 FROM rpr_nav WINDOW w AS (
     ORDER BY id
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 90b1ae96a0e..86ed06fec68 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2083,7 +2083,7 @@ WINDOW w AS (

--- Pattern variable qualified name: not supported (valid per §4.16, not yet implemented)
+-- Pattern variable qualified name: not supported (valid per SQL standard 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2116,7 +2116,7 @@ WINDOW w AS (
 );
 -- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
 
--- FROM-clause range variable qualified name: not allowed (prohibited by §6.5)
+-- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3611,7 +3611,7 @@ INSERT INTO rpr_sort VALUES
     (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
 
--- RPR with GROUP BY (aggregate in DEFINE → ERROR before GROUP BY interaction)
+-- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction)
 -- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index aaa7b44f789..1d27e0dc09e 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -806,7 +806,7 @@ WINDOW w AS (
 
 -- Optional reluctant group: (A B)?? C
 -- nfa_advance_begin: reluctant tries skip first, but skip path needs C
--- at row 1 which is A → skip fails. Enter path succeeds: A(1) B(2) C(3).
+-- at row 1 which is A -> skip fails. Enter path succeeds: A(1) B(2) C(3).
 WITH test_optional_reluctant AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2102,8 +2102,8 @@ WINDOW w AS (
         D AS 'D' = ANY(flags)
 );
 
--- Nested END→END max reached
--- Inner group (A B){2} reaches max=2 → exits to outer END
+-- Nested END->END max reached
+-- Inner group (A B){2} reaches max=2 -> exits to outer END
 WITH test_end_nested_max AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2131,8 +2131,8 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
--- Nested END→END between min/max
--- Inner group (A B){1,3} exits between min/max → outer END count++
+-- Nested END->END between min/max
+-- Inner group (A B){1,3} exits between min/max -> outer END count++
 WITH test_end_nested_mid AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2820,9 +2820,9 @@ WINDOW w AS (
 
 -- Coverage failure: older can't cover newer's states
 -- Pattern A+ | B+ with SKIP PAST LAST ROW.
--- Row 1: only A → Ctx1 takes A branch only (B fails).
--- Row 2: A and B → Ctx2 takes both branches.
--- Absorption: Ctx1 has A but no B → can't cover Ctx2's B state → fails.
+-- Row 1: only A -> Ctx1 takes A branch only (B fails).
+-- Row 2: A and B -> Ctx2 takes both branches.
+-- Absorption: Ctx1 has A but no B -> can't cover Ctx2's B state -> fails.
 WITH test_coverage_fail AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A', '_']),
@@ -2848,8 +2848,8 @@ WINDOW w AS (
 
 -- Absorb skips completed context (older->states==NULL)
 -- Pattern A+ | B+ with SKIP PAST LAST ROW.
--- Row 1: A only → Ctx1 takes A branch. Row 2: B only → Ctx1 A fails (completed).
--- Ctx2 takes B branch. Absorption: Ctx1 states==NULL → skip.
+-- Row 1: A only -> Ctx1 takes A branch. Row 2: B only -> Ctx1 A fails (completed).
+-- Ctx2 takes B branch. Absorption: Ctx1 states==NULL -> skip.
 WITH test_older_completed AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2874,9 +2874,9 @@ WINDOW w AS (
 
 -- Absorb skips non-absorbable context (!hasAbsorbableState)
 -- Pattern A+ | B C with SKIP PAST LAST ROW (only A+ branch absorbable).
--- Row 1: B only → Ctx1 takes B branch (non-absorbable), advances to C.
--- Row 2: C,A → Ctx1 C matches (hasAbsorbableState=false). Ctx2 takes A (absorbable).
--- Absorption: Ctx1 !hasAbsorbableState → skip.
+-- Row 1: B only -> Ctx1 takes B branch (non-absorbable), advances to C.
+-- Row 2: C,A -> Ctx1 C matches (hasAbsorbableState=false). Ctx2 takes A (absorbable).
+-- Absorption: Ctx1 !hasAbsorbableState -> skip.
 WITH test_older_non_absorbable AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B', '_']),
@@ -3236,7 +3236,7 @@ WINDOW w AS (
 -- (A?){0,3}: min=0, nullable inner.
 -- A never matches. A? matches empty, min=0 satisfied immediately.
 -- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration → no match (same as {2,3})
+-- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3260,7 +3260,7 @@ WINDOW w AS (
 -- (A?){1,3}: min=1, nullable inner.
 -- A never matches. Need 1 empty iteration to satisfy min=1.
 -- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration → no match (same as {2,3})
+-- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3285,7 +3285,7 @@ WINDOW w AS (
 -- A never matches. Need 2 empty iterations to satisfy min=2.
 -- Per standard: STR06=(STRE STRE) is valid for min=2.
 -- Expected: empty match for every row
--- XXX: visited bitmap blocks second empty iteration → match failure
+-- XXX: visited bitmap blocks second empty iteration -> match failure
 WITH test_728_min2 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3307,10 +3307,10 @@ WINDOW w AS (
 );
 
 -- (A?){2,3} mixed: some rows match A, some don't
--- Rows 1-2: A matches, greedy takes 2 → min satisfied
+-- Rows 1-2: A matches, greedy takes 2 -> min satisfied
 -- Row 3: A doesn't match, needs 2 empty iterations for min=2
 -- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
--- Row 4: A matches 1 real iter + 1 ff empty exit → match 4-4
+-- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3338,8 +3338,8 @@ WINDOW w AS (
 -- fires as a parallel exit path (EXIT ONLY, no greedy/reluctant loop).
 -- Data: alternating A, B rows (6 rows)
 -- Greedy: each row gets the longest match from its starting position.
--- Row 1: 3 iters (A@1,B@2)(A@3,B@4)(A@5,B@6) → 1-6
--- Row 5: 1 real iter + 1 ff empty exit → 5-6
+-- Row 1: 3 iters (A@1,B@2)(A@3,B@4)(A@5,B@6) -> 1-6
+-- Row 5: 1 real iter + 1 ff empty exit -> 5-6
 WITH test_728_multi_body AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3389,9 +3389,9 @@ WINDOW w AS (
 );
 
 -- (A? B?){2,3}: mixed real and empty iterations
--- Row 1: iter1 real (A@1,B@2), iter2 at row 3 empty → ff exit, match 1-2
--- Row 3: C doesn't match A or B → NULL
--- Row 4: iter1 real (A@4,B@5), iter2 at end empty → ff exit, match 4-5
+-- Row 1: iter1 real (A@1,B@2), iter2 at row 3 empty -> ff exit, match 1-2
+-- Row 3: C doesn't match A or B -> NULL
+-- Row 4: iter1 real (A@4,B@5), iter2 at end empty -> ff exit, match 4-5
 WITH test_728_multi_mixed AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3421,7 +3421,7 @@ WINDOW w AS (
 
 -- Standard's worked example: A? B+ with specific data
 -- Preferment order: (A)(BBB), (A)(BB), (A)(B), ()(BBB), ()(BB), ()(B)
--- Row 1: A condition (price>100) is false → A fails
+-- Row 1: A condition (price>100) is false -> A fails
 -- Backtrack: empty A?, then B+ from row 1
 -- Expected: rows 1-3 match as B (A? takes empty match)
 WITH test_73_example AS (
-- 
2.50.1 (Apple Git-155)

From f086f06ab0fda30d98dd0435c4decad30dc400f4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 01:05:04 +0900
Subject: [PATCH 38/40] Move RPR design notes from execRPR.c to README.rpr

The "Flat-Array Stream NFA Guide" (Chapters I-XII plus Appendix A-C)
was carried as a single 1580-line C comment block at the top of
execRPR.c, accounting for nearly half the file's length.  Move it
verbatim to a new src/backend/executor/README.rpr, following the
same plain-tex convention as the existing executor/README, and
leave a four-line pointer comment in execRPR.c.

All chapter cross-references in the document are intra-document
("Chapter IV", "Chapter VIII", "Chapter XII", etc.) and no other
source or comment refers to them, so extracting the block whole
introduces no dangling references.

Code-organization change; no functional or test impact.
---
 src/backend/executor/README.rpr | 1578 ++++++++++++++++++++++++++++++
 src/backend/executor/execRPR.c  | 1580 +------------------------------
 2 files changed, 1580 insertions(+), 1578 deletions(-)
 create mode 100644 src/backend/executor/README.rpr

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
new file mode 100644
index 00000000000..52bcd77390c
--- /dev/null
+++ b/src/backend/executor/README.rpr
@@ -0,0 +1,1578 @@
+============================================================================
+  PostgreSQL Row Pattern Recognition: Flat-Array Stream NFA Guide
+============================================================================
+
+  Target audience: Developers with a basic understanding of the PostgreSQL
+                   executor and planner architecture
+
+  Scope: The entire process from PATTERN/DEFINE clause parsing to NFA
+         runtime execution
+
+  Related code:
+    - src/backend/parser/parse_rpr.c          (parser phase)
+    - src/backend/optimizer/plan/rpr.c        (optimizer phase)
+    - src/backend/executor/nodeWindowAgg.c    (executor phase, window agg)
+    - src/backend/executor/execRPR.c          (executor phase, NFA engine)
+    - src/include/executor/execRPR.h          (NFA public API)
+    - src/include/nodes/plannodes.h           (plan node definitions)
+    - src/include/nodes/execnodes.h           (execution state definitions)
+    - src/include/optimizer/rpr.h             (types and constants)
+    - src/backend/optimizer/plan/createplan.c (nav offset computation)
+
+============================================================================
+
+What is a Flat-Array Stream NFA?
+
+  The NFA in this implementation is not a traditional state-transition graph
+  but a flat array of fixed-size 16-byte elements. At runtime, it processes
+  the row stream in a forward-only manner, expanding epsilon transitions
+  eagerly without backtracking.
+
+  - Flat-Array: Pattern compiled into a flat array,
+                not a graph (Chapter IV)
+  - Stream:     Rows consumed sequentially in one direction,
+                never revisited (Chapter XII)
+  - NFA:        Nondeterministic execution where multiple states
+                coexist within a single context (Chapter VI)
+
+Chapter I  Row Pattern Recognition Overview
+============================================================================
+
+Row Pattern Recognition (hereafter RPR) is a feature introduced in SQL:2016
+that matches regex-based patterns against ordered row sets.
+
+The SQL standard defines two forms:
+
+  Feature R010: MATCH_RECOGNIZE (FROM clause)
+    - Dedicated table operator
+    - Provides dedicated functions such as MATCH_NUMBER(), CLASSIFIER()
+    - Supports ONE ROW PER MATCH / ALL ROWS PER MATCH
+
+  Feature R020: RPR in a window (WINDOW clause)
+    - Integrated into the existing window function framework
+    - Supports ALL ROWS PER MATCH only
+    - No MATCH_NUMBER()
+
+This implementation targets Feature R020.
+
+The basic syntax is as follows:
+
+  SELECT ...
+  OVER (
+    PARTITION BY ...
+    ORDER BY ...
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    [INITIAL | SEEK]   -- SEEK is defined in the standard but not implemented
+    AFTER MATCH SKIP TO NEXT ROW | SKIP PAST LAST ROW
+    PATTERN ( <regex> )
+    DEFINE <variable> AS <condition>, ...
+  )
+
+The PATTERN clause is a regular expression over row pattern variables.
+The DEFINE clause specifies boolean conditions that determine whether each
+variable evaluates to true for the current row.
+
+Example:
+
+  PATTERN (A+ B)
+  DEFINE A AS price > PREV(price),
+         B AS price < PREV(price)
+
+This pattern matches "a span where prices rise consecutively then drop."
+
+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                 |
+  +------------------------------------------------------------+
+
+Each phase uses independent data structures, and the interfaces between
+phases are well-defined:
+
+  Parser -> Planner:    WindowClause.rpPattern (RPRPatternNode tree)
+                        WindowClause.defineClause (TargetEntry list)
+
+  Planner -> Executor:  WindowAgg.rpPattern (RPRPattern struct)
+                        WindowAgg.defineClause (TargetEntry list)
+
+Chapter III  Parsing Phase
+============================================================================
+
+III-1. Entry Point
+
+  transformWindowDefinitions() (parse_clause.c)
+    +-- transformRPR() (parse_rpr.c)
+
+transformRPR() is invoked when RPCommonSyntax is present and performs the
+following:
+
+  (1) Frame option validation
+      - Only ROWS is allowed (RANGE, GROUPS are not)
+      - The start boundary must be CURRENT ROW
+      - EXCLUDE option is not allowed
+
+  (2) Transcription to WindowClause
+      - Copies rpPattern, rpSkipTo, initial fields
+
+  (3) DEFINE clause transformation (transformDefineClause)
+
+III-2. PATTERN AST
+
+The parser transforms the PATTERN clause into an RPRPatternNode tree.
+Each node has one of the following four types:
+
+  RPR_PATTERN_VAR    Variable reference. Name stored in varName field.
+  RPR_PATTERN_SEQ    Concatenation. Children node list in children.
+  RPR_PATTERN_ALT    Alternation. Branch node list in children.
+  RPR_PATTERN_GROUP  Group (parentheses). Body node list in children.
+
+All nodes have min/max fields to express quantifiers:
+
+  A       -> VAR(A, min=1, max=1)
+  A+      -> VAR(A, min=1, max=INF)
+  A*      -> VAR(A, min=0, max=INF)
+  A?      -> VAR(A, min=0, max=1)
+  A{3,5}  -> VAR(A, min=3, max=5)
+
+If the reluctant field is true, the quantifier is reluctant (non-greedy).
+(RPRPatternNode.reluctant is bool; reluctant_location is the separate
+ParseLoc field holding the '?' token position, or -1 if absent.)
+
+Example: PATTERN ((A+ B) | C*)
+
+  ALT
+  +-- SEQ
+  |   +-- VAR(A, 1, INF)
+  |   +-- VAR(B, 1, 1)
+  +-- VAR(C, 0, INF)
+
+III-3. DEFINE Clause Transformation
+
+transformDefineClause() processes each DEFINE variable as follows:
+
+  (1) Checks for duplicate variable names
+  (2) Transforms the expression via transformExpr()
+  (3) Extracts Var nodes via pull_var_clause() and ensures each is
+      present in the query targetlist, so the planner propagates the
+      referenced columns through the plan tree
+  (4) Wraps in a TargetEntry with the variable name set in resname
+
+After all variables are processed:
+  (5) Coerces each expression to Boolean type (coerce_to_boolean)
+
+Variables that are used in PATTERN but not defined in DEFINE are implicitly
+evaluated as TRUE (matching all rows).
+
+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()
+
+  Phase 1: AST optimization (optimizeRPRPattern)
+  Phase 2: Statistics collection (scanRPRPattern)
+  Phase 3: Memory allocation (allocateRPRPattern)
+  Phase 4: NFA element fill (fillRPRPattern)
+  Phase 5: Finalization (finalizeRPRPattern)
+  Phase 6: Absorbability analysis (computeAbsorbability)
+
+IV-3. Phase 1: AST Optimization
+
+After copying the parser-generated AST, the following optimizations are
+applied:
+
+  (a) SEQ flattening: Unwrap nested SEQ nodes
+      SEQ(A, SEQ(B, C)) -> SEQ(A, B, C)
+
+  (b) Consecutive variable merging: Merge consecutive occurrences of the
+      same variable into a single quantifier
+      A A -> A{2}
+      A{2,3} A{1,2} -> A{3,5}
+
+  (c) Consecutive group merging: Merge repeated identical groups
+      (A B)+ (A B)+ -> (A B){2,INF}
+
+  (d) Consecutive ALT merging: Merge repeated identical ALT nodes
+      (A | B) (A | B) (A | B) -> (A | B){3}
+
+  (e) Prefix/suffix absorption: Absorb identical sequences before/after
+      a group
+      A B (A B)+ -> (A B){2,INF}
+
+  (f) ALT flattening and deduplication
+      (A | (B | C)) -> (A | B | C)
+      (A | B | A) -> (A | B)
+
+  (g) Quantifier multiplication: Collapse nested quantifiers when safe
+      (A+)+ -> A+
+      (A{2,3}){5} -> A{10,15}
+
+  (h) Single-child unwrap
+      SEQ(A) -> A,  (A){1,1} -> A
+
+IV-4. Phase 4: NFA Element Array Generation
+
+Transforms the optimized AST into a flat array of RPRPatternElement.
+This is the core data structure used for NFA simulation at runtime.
+
+RPRPatternElement struct (16 bytes):
+
+  Field      Size     Description
+  ---------------------------------------------------------
+  varId      1B      Variable ID (0-251) or control code (252-255)
+  depth      1B      Group nesting depth
+  flags      1B      Bit flags (see below)
+  reserved   1B      Padding
+  min        4B      Quantifier lower bound
+  max        4B      Quantifier upper bound
+  next       2B      Next element index (sequential flow)
+  jump       2B      Branch target index (for ALT/GROUP)
+
+Control codes:
+
+  RPR_VARID_BEGIN (252)  Group start marker
+  RPR_VARID_END   (253)  Group end marker
+  RPR_VARID_ALT   (254)  Alternation start marker
+  RPR_VARID_FIN   (255)  Pattern completion marker
+
+Element flags (1 byte, bitmask):
+
+  0x01  RPR_ELEM_RELUCTANT          (VAR, BEGIN, END)
+        Non-greedy quantifier.  Prefers shorter match: try exit-loop
+        first, then repeat.  Set on VAR for simple (A+?),
+        on BEGIN+END for group ((...)+?).
+
+  0x02  RPR_ELEM_EMPTY_LOOP         (END)
+        Group body can produce empty match (all children nullable).
+        Creates a fast-forward exit clone alongside the normal
+        loop-back so cycle detection doesn't kill legitimate
+        matches. (IV-4b)
+
+  0x04  RPR_ELEM_ABSORBABLE_BRANCH  (VAR, BEGIN, END, ALT)
+        Element lies within an absorbable region.  Used at runtime
+        to track whether the current NFA state is in an absorbable
+        context.
+
+  0x08  RPR_ELEM_ABSORBABLE         (VAR, END)
+        Absorption judgment 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
+
+  Accessor macros:
+    RPRElemIsReluctant(e)        (e)->flags & 0x01
+    RPRElemCanEmptyLoop(e)       (e)->flags & 0x02
+    RPRElemIsAbsorbableBranch(e) (e)->flags & 0x04
+    RPRElemIsAbsorbable(e)       (e)->flags & 0x08
+
+Example: PATTERN (A+ B | C)
+
+  AST: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
+
+  Compilation result:
+
+  idx  varId  depth  min  max  next  jump  Description
+  ------------------------------------------------------------
+   0   ALT    0      1    1    1     -1    Alternation start
+   1   A(0)   1      1    INF  2     3     Branch 1: A+
+   2   B(1)   1      1    1    4     -1    Branch 1: B -> FIN
+   3   C(2)   1      1    1    4     -1    Branch 2: C -> FIN
+   4   FIN    0      1    1    -1    -1    Pattern completion
+
+  - idx 0: ALT marker. next(=1) is the start of the first branch
+  - idx 1: Variable A. next(=2) is B, jump(=3) is the start of the second
+           branch
+  - idx 2: Variable B. next(=4) is FIN
+  - idx 3: Variable C. next(=4) is FIN
+  - idx 4: FIN marker. Match completion signal
+
+Roles of next and jump:
+
+  - next: The next element to move to "after consuming" the current element.
+          For VAR, the next position after a successful match.
+          For BEGIN/END, the next position inside/outside the group.
+
+  - jump: The element to "skip to."
+          In ALT, a jump from one branch to the next branch.
+          In BEGIN, a skip path to END+1 (for groups with min=0).
+          In END, a loop-back to the start of the group body.
+
+Example: PATTERN ((A B)+)
+
+  idx  varId    depth  min  max  next  jump  Description
+  --------------------------------------------------------------
+   0   BEGIN    0      1    INF  1     4     Group start
+   1   A(0)     1      1    1    2     -1    A
+   2   B(1)     1      1    1    3     -1    B
+   3   END      0      1    INF  4     1     Group end
+   4   FIN      0      1    1    -1    -1    Pattern completion
+
+  - idx 0: BEGIN. next(=1) enters the group body.
+           jump(=4) skips to after END = FIN (used when min=0).
+  - idx 3: END. next(=4) exits the group.
+           jump(=1) loops back to the start of the group body.
+
+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:
+
+  Greedy (default):  try loop-back first, then exit  (prefer longer match)
+  Reluctant:         try exit first, then loop-back   (prefer shorter match)
+
+The flag is set on all elements that carry the quantifier:
+
+  Simple VAR (A+?):     RPR_ELEM_RELUCTANT on the VAR element
+  Group ((...)+?):      RPR_ELEM_RELUCTANT on BEGIN and END elements
+
+At runtime (nfa_advance), the flag controls DFS exploration order:
+
+  VAR with quantifier:
+    Greedy:    primary path = next (continue), clone = jump (skip)
+    Reluctant: primary path = jump (skip), clone = next (continue)
+
+  END element:
+    Greedy:    primary path = jump (loop-back), clone = next (exit)
+    Reluctant: primary path = next (exit), clone = jump (loop-back)
+
+  BEGIN with min=0:
+    Greedy:    primary path = next (enter group), clone = jump (skip)
+    Reluctant: primary path = jump (skip), clone = next (enter group)
+
+The absorption optimization requires greedy quantifiers. Reluctant
+quantifiers are excluded from absorbability analysis (see IV-5).
+
+IV-4b. Empty Loop Flag (RPR_ELEM_EMPTY_LOOP)
+
+The empty-loop flag is set during Phase 4 (fillRPRPatternGroup) on the END
+element when the group body is nullable -- i.e., every path through the
+body can match zero rows (all children are nullable).
+
+Example patterns that trigger this flag:
+
+  (A?)*    A is nullable (min=0), so group body is nullable -> END gets flag
+  (A? B?)+ Both children nullable -> body nullable -> END gets flag
+  (A | B*) B* is nullable, making the ALT nullable -> END gets flag
+
+The flag works in conjunction with the empty match cycle detection
+(elemIdx visited bitmap). Without this flag, cycle detection alone would
+cause legitimate matches to fail.
+
+Problem example: (A*){2,3}
+  - Iteration 1: A* consumes all available rows -> count=1, END reached
+  - Loop-back for iteration 2: A* matches zero rows -> END reached again
+  - Cycle detection sees the same elemIdx on the same row -> state killed
+  - count never reaches min(2) -> match fails (incorrect)
+
+With the RPR_ELEM_EMPTY_LOOP flag, nfa_advance_end creates two paths:
+the normal loop-back (which cycle detection will eventually kill) and
+a fast-forward exit clone that bypasses the loop entirely.
+(See IX-4(c) for detailed runtime behavior.)
+
+IV-5. Absorbability Analysis (RPR_ELEM_ABSORBABLE)
+
+Context absorption is an optimization technique that reduces O(n^2) to O(n).
+(Runtime behavior is described in Chapter VIII.)
+
+This phase determines whether the pattern has a structure suitable for the
+absorption optimization and sets flags on the relevant elements:
+
+  RPR_ELEM_ABSORBABLE         Absorption comparison point
+  RPR_ELEM_ABSORBABLE_BRANCH  Element within an absorbable region
+
+Eligibility conditions:
+
+  (1) SKIP PAST LAST ROW (not NEXT ROW)
+  (2) Frame end is UNBOUNDED FOLLOWING
+
+Structural conditions (isUnboundedStart + computeAbsorbabilityRecursive):
+
+  Case 1: Simple VAR+ (e.g., A+)
+          -> ABSORBABLE | ABSORBABLE_BRANCH set on the VAR
+  Case 2: GROUP+ with fixed-length children (min == max, recursively)
+          e.g., (A B)+, (A B{2})+, ((A (B C){2}){2})+
+          -> ABSORBABLE_BRANCH on all elements within the group,
+             ABSORBABLE | ABSORBABLE_BRANCH on END
+
+          Why this is safe: when every child has min == max, the group
+          is semantically equivalent to unrolling its body into {1,1}
+          elements.  E.g., (A B{2})+ behaves like (A B B)+.  Each
+          iteration consumes a fixed number of rows, so an earlier
+          context's count always dominates a later one's (monotonicity).
+
+  Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
+          -> Recurses from BEGIN into the body, applying Case 1.
+            ABSORBABLE | ABSORBABLE_BRANCH set on A.
+            B and END get no flags -> absorption stops once past A.
+
+Absorbability is determined per-element, not per-pattern.
+Absorption comparison is performed only when a state resides at an
+element with the RPR_ELEM_ABSORBABLE flag. Once a state leaves the
+flagged region, absorption is permanently disabled for that state.
+
+Through this mechanism, the runtime guarantees monotonicity:
+"a context that started earlier always subsumes a context that
+started later."
+
+Chapter V  NFA Runtime Data Structures
+============================================================================
+
+V-1. RPRNFAState -- NFA State
+
+A single NFA state represents "how far the pattern has progressed."
+
+  Field         Description
+  -----------------------------------------------------------
+  elemIdx       Index of the current pattern element
+  counts[]      Repetition count per group depth
+  isAbsorbable  Whether the state is in an absorbable region
+  next          Next state in the linked list
+
+The size of the counts array is rpPattern->maxDepth (= maximum group
+nesting depth + 1), allocated as a flexible array member at the end of
+the struct.
+
+Example: In PATTERN ((A B)+ C), a state waiting for B in the 3rd iteration
+
+  Element array: [0:BEGIN(d0) 1:A(d1) 2:B(d1) 3:END(d0) 4:C(d0) 5:FIN]
+
+  elemIdx = 2 (B, depth 1)
+  counts[0] = 2 (depth 0: depth of END. Group completed 2 iterations)
+  counts[1] = 1 (depth 1: depth of B. A matched in current iteration)
+
+  Counts are indexed by depth, not by elemIdx.
+  counts[0] is incremented when passing through END(depth 0),
+  and the group repetition count is preserved even when
+  the state is at B(depth 1).
+
+Definition of two states being "equal":
+
+  Two states are equal if they have the same elemIdx and the same counts
+  up to the depth of that element.
+  nfa_states_equal() compares counts[0..elem->depth] using memcmp.
+  Only counts at or below the depth of the current element are meaningful.
+
+V-2. RPRNFAContext -- Matching Context
+
+A single context represents "a matching attempt started from a specific
+start row."
+
+  Field                 Description
+  ---------------------------------------------------------------------
+  states                Linked list of active NFA states
+  matchStartRow         Row number where matching started
+  matchEndRow           Row number where matching completed
+                        (-1 if incomplete)
+  lastProcessedRow      Last row processed
+  matchedState          State that reached FIN (for greedy fallback)
+  hasAbsorbableState    Whether this context can absorb other contexts
+  allStatesAbsorbable   Whether this context can be absorbed
+  next, prev            Doubly-linked list
+
+Since the NFA is nondeterministic, multiple states can coexist
+simultaneously within a single context.
+
+Example: In PATTERN (A | B) C, if the first row matches both A and B,
+two states coexist within the context:
+
+  State 1: elemIdx=3 (waiting for C, via branch A)
+  State 2: elemIdx=3 (waiting for C, via branch B)
+
+In this case, since the (elemIdx, counts) of the two states are equal,
+nfa_add_state_unique() retains only State 1 (branch A), which was
+added first.
+Because DFS processes the first branch of ALT first, the state via A
+is registered first, and the state via B is discarded as a duplicate.
+This is the preferment guarantee.
+
+V-3. RPR Fields of WindowAggState
+
+  nfaContext / nfaContextTail   Doubly-linked list of active contexts
+  nfaContextFree                Reuse pool for contexts
+  nfaStateFree                  Reuse pool for states
+  nfaVarMatched                 Per-row cache: varMatched[varId]
+  nfaVisitedElems               Bitmap for cycle detection
+  nfaStateSize                  Precomputed size of RPRNFAState
+
+Memory management:
+
+  States and contexts are managed through their own free lists.
+  Instead of palloc, they are obtained from the reuse pool, and
+  returned to the pool upon deallocation.
+  This reduces the overhead of frequent allocation/deallocation.
+
+Chapter VI  NFA Execution: 3-Phase Model
+============================================================================
+
+VI-1. Entry Point and Overall Flow
+
+When the window function processes each row, row_is_in_reduced_frame()
+is called. This function determines whether the current row belongs to
+a matched frame, and if necessary, calls update_reduced_frame() to
+drive the NFA.
+
+Flow of update_reduced_frame():
+
+  (1) Find or create a context for the target row
+  (2) Enter the row processing loop
+  (3) After the loop ends, record the match result
+
+Pseudocode of the row processing loop:
+
+  targetCtx = ExecRPRGetHeadContext(pos)
+  if targetCtx == NULL:
+      targetCtx = ExecRPRStartContext(pos)
+
+  for currentPos = startPos; targetCtx->states != NULL; currentPos++:
+      if not nfa_evaluate_row(currentPos):  -- row does not exist
+          ExecRPRFinalizeAllContexts()      -- finalize all contexts
+          ExecRPRCleanupDeadContexts()      -- clean up after finalization
+          break
+
+      ExecRPRProcessRow(currentPos)         -- 3-phase processing
+      ExecRPRStartContext(currentPos + 1)   -- pre-create next start point
+      ExecRPRCleanupDeadContexts()          -- remove dead contexts
+
+Key point: Processing a single row may require processing multiple rows
+ahead. Due to the nature of window functions, determining the frame for
+row N requires looking at rows beyond N.
+
+VI-2. Context Creation: ExecRPRStartContext()
+
+Creates a new context and performs the initial advance.
+
+  (1) Allocate context via nfa_context_alloc()
+  (2) Set matchStartRow = pos
+  (3) Create initial state: elemIdx=0 (first pattern element),
+      counts=all zero
+  (4) Call nfa_advance(initialAdvance=true)
+
+The initial advance expands epsilon transitions at the beginning of
+the pattern. For example, the initial advance for PATTERN ((A | B) C):
+
+  Start: elemIdx=0 (ALT)
+    -> Expand ALT branches
+      -> elemIdx=1 (A) -- VAR, so add state; stop here
+      -> elemIdx=2 (B) -- VAR, so add state; stop here
+
+  Result: Two states in the context {waiting for A, waiting for B}
+
+During the initial advance, reaching FIN is not recorded as a match.
+This is to prevent empty matches.
+
+VI-3. Row Evaluation: nfa_evaluate_row()
+
+Evaluates all variable conditions in the DEFINE clause at once for
+the current row.
+
+  for each defineClause[i]:
+      result = ExecEvalExpr(defineClause[i])
+      varMatched[i] = (not null and true)
+
+To support row navigation operators (PREV, NEXT, FIRST, LAST),
+a 1-slot model is used: only ecxt_outertuple is set to the current
+row.  Navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
+emitted during DEFINE expression compilation:
+
+  NAV_SET:     save ecxt_outertuple, swap in target row via nav_slot
+  (evaluate):  argument expression reads from swapped slot
+  NAV_RESTORE: restore original ecxt_outertuple
+
+Compound navigation (PREV(FIRST()), NEXT(FIRST()), PREV(LAST()),
+NEXT(LAST())) is flattened by the parser into a single RPRNavExpr
+with a compound kind (RPR_NAV_PREV_FIRST, etc.).  The executor
+computes the target position in two steps: first the inner reference
+point (match_start + N or currentpos - N) with match-range validation,
+then the outer adjustment (+/- M) with partition-range validation.
+If either step is out of range, the result is NULL.
+
+nav_slot caches the last fetched position (nav_slot_pos) to avoid
+redundant tuplestore lookups when multiple navigation calls target
+the same row.
+
+The varMatched array is referenced later in Phase 1 (Match).
+
+VI-4. Per-Context Re-evaluation (match_start-dependent variables)
+
+DEFINE variables that depend on match_start (those containing FIRST,
+LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST)
+are identified at plan time via defineMatchStartDependent.  The shared
+evaluation in nfa_evaluate_row() uses the head context's matchStartRow
+for FIRST/LAST base position.
+
+When processing a context whose matchStartRow differs from the shared
+value, nfa_reevaluate_dependent_vars() temporarily sets nav_match_start
+to that context's matchStartRow and re-evaluates only the dependent
+variables.  The original nav_match_start and currentpos are saved and
+restored after re-evaluation.
+
+Summary of evaluation strategy by navigation content:
+
+  Navigation content               evaluation
+  -------------------------------------------------------
+  No navigation                    shared (once per row)
+  PREV/NEXT only                   shared (once per row)
+  LAST (no offset)                 shared (once per row)
+  LAST (with offset)               per-context
+  FIRST (any)                      per-context
+  Compound (inner FIRST)           per-context
+  Compound (inner LAST, no off.)   shared (once per row)
+  Compound (inner LAST, w/off.)    per-context
+
+VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
+
+Navigation functions require access to past rows via the tuplestore.
+To allow tuplestore_trim() to free rows that are no longer reachable,
+the planner computes two offsets (see compute_nav_offsets):
+
+  navMaxOffset (Nav Mark Lookback):
+    Maximum backward reach from currentpos.  Contributed by PREV,
+    LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
+    Mark position: currentpos - navMaxOffset.
+
+  navFirstOffset (Nav Mark Lookahead):
+    Minimum forward offset from match_start.  Contributed by FIRST
+    and compound PREV_FIRST/NEXT_FIRST.  Can be negative when
+    compound PREV_FIRST looks before match_start.
+    Mark position: oldest_context->matchStartRow + navFirstOffset.
+
+The actual mark is set to: min(lookback_mark, lookahead_mark).
+This ensures all rows reachable by any navigation function are retained.
+
+When offsets contain non-constant expressions (Param), the planner sets
+navMaxOffsetKind/navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL and the
+executor evaluates them at init time.  On overflow, the kind is set to
+RPR_NAV_OFFSET_RETAIN_ALL, disabling trim for that dimension.
+
+VI-6. ExecRPRProcessRow(): 3-Phase Processing
+
+NFA processing for a single row is divided into three phases:
+
+  +--------------------------------------------+
+  |  Phase 1: MATCH (convergence)              |
+  |  Compare the current row against each VAR  |
+  |  state. Remove states that fail to match.  |
+  |                                            |
+  |  Phase 2: ABSORB (absorption)              |
+  |  Merge duplicate contexts to prevent       |
+  |  state explosion.                          |
+  |                                            |
+  |  Phase 3: ADVANCE (expansion)              |
+  |  Expand epsilon transitions to prepare     |
+  |  for the next row.                         |
+  +--------------------------------------------+
+
+This ordering is important:
+
+  - Match executes first to "consume the current row."
+  - Absorb executes immediately after Match, when states have been updated.
+  - Advance executes last to prepare "states waiting for the next row."
+
+Chapter VII  Phase 1: Match
+============================================================================
+
+nfa_match() iterates through each state in the context:
+
+  (1) Check whether the state's elemIdx is a VAR element
+  (2) Compare against the current row using nfa_eval_var_match()
+  (3) Match success: increment repetition count, retain state
+  (4) Match failure: remove state
+
+Match determination (nfa_eval_var_match):
+
+  If varId is within the range of defineVariableList:
+      Use the value of varMatched[varId]
+
+  If varId exceeds the range (variable not defined in DEFINE):
+      Unconditionally true (matches all rows)
+
+Immediate advance for simple VARs:
+
+  For a VAR with min=1, max=1 where the next element is END,
+  the Match phase processes through END immediately.
+  This is necessary for accurate state comparison in Phase 2 (Absorb).
+
+  Example: In PATTERN ((A B)+), when A matches, it immediately advances
+  to B, and when B matches, it immediately advances through END to
+  complete the group count. This enables absorption comparison with
+  other contexts.
+
+Chapter VIII  Phase 2: Absorb (Context Absorption)
+============================================================================
+
+VIII-1. Problem
+
+In the current implementation, a new context is started for each row
+processed.
+Applying PATTERN (A+) to 10 rows produces 10 contexts,
+each of which tracks state independently.
+
+If there are N rows, the total number of states becomes O(N^2):
+
+  Context 1 (started at row 1): can match A up to N times
+  Context 2 (started at row 2): can match A up to N-1 times
+  ...
+  Context N (started at row N): can match A 1 time
+
+VIII-2. Solution: Context Absorption
+
+Key observation: a context started earlier contains
+all matches of a later-started context (monotonicity principle).
+
+If Context 1 started at row 1 and matched A 5 times,
+the state where Context 2 (started at row 2) matched A 4 times
+is already contained within Context 1.
+
+Therefore Context 2 can be "absorbed" into Context 1.
+
+VIII-3. Absorption Conditions
+
+Planner-time prerequisites (all must hold for absorption to be enabled):
+
+  (a) SKIP PAST LAST ROW.  SKIP TO NEXT ROW creates overlapping
+      contexts that cannot be safely absorbed.
+  (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
+      FOLLOWING).  Limited frames apply differently to each context,
+      breaking the monotonicity principle.
+  (c) No match_start-dependent navigation in DEFINE.
+
+      Mechanism: each context has a different matchStartRow, so FIRST
+      resolves to a different row for each context at the same
+      currentpos.  An earlier context's DEFINE result no longer
+      subsumes a later one's, making count-dominance comparison
+      invalid.  Rather than comparing matchStartRow at runtime
+      (which would complicate the absorb path), any match_start
+      dependency disables absorption entirely.
+
+      Navigation content              match_start dep.  absorption
+      ------------------------------------------------------------
+      No navigation                   none              safe
+      PREV/NEXT only                  none              safe
+      LAST (no offset)                none              safe
+      LAST (with offset)              boundary check    unsafe
+      FIRST (any)                     direct            unsafe
+      Compound (inner FIRST)          direct            unsafe
+      Compound (inner LAST, no off.)  none              safe
+      Compound (inner LAST, w/off.)   boundary chk      unsafe
+
+Runtime conditions (evaluated per context pair):
+
+  (1) The pattern is marked as isAbsorbable (see IV-5)
+  (2) allStatesAbsorbable of the target context is true
+  (3) An earlier context "covers" all states of the target
+
+Cover condition (nfa_states_covered):
+
+  A state with the same elemIdx exists in the earlier context,
+  and the count at that depth is greater than or equal -- then it is covered.
+
+VIII-4. Dual-Flag Design
+
+Two boolean flags make the absorption decision efficient:
+
+  hasAbsorbableState (monotonic: only true->false transition possible)
+    "Does this context have the ability to absorb other contexts?"
+    true if at least one absorbable state exists.
+    Transitions to false when states are removed leaving no absorbable
+    states.
+    Once false, it never becomes true again.
+
+  allStatesAbsorbable (dynamic: can fluctuate)
+    "Can this context be absorbed?"
+    true if all states are in an absorbable region.
+    Becomes false when a non-absorbable state is added; reverts to true
+    when it is removed.
+
+VIII-5. Absorption Order
+
+nfa_absorb_contexts() traverses from tail (newest) to head (oldest).
+
+  for ctx = tail to head:
+      if ctx.allStatesAbsorbable:
+          for older = ctx.prev to head:
+              if older.hasAbsorbableState:
+                  if nfa_states_covered(older, ctx):
+                      free(ctx)  -- absorbed
+                      break
+
+Since inspection starts from the newest context, the most recently started
+(= having the shortest match) context is absorbed first.
+
+Chapter IX  Phase 3: Advance (Epsilon Transition Expansion)
+============================================================================
+
+IX-1. Overview
+
+nfa_advance() expands epsilon transitions from each state after Match,
+generating "new states waiting for the next row."
+
+An epsilon transition is a transition that moves without consuming a row:
+
+  - ALT: branch to each alternative
+  - BEGIN: enter group (or skip if min=0)
+  - END: loop-back within group (or exit when condition is met)
+  - FIN: record match completion
+  - VAR loop/exit: repeat/exit according to the quantifier
+
+Expansion stops upon reaching a VAR element, and the state is added.
+This is because VAR is the element that "will consume the next row."
+
+IX-2. Processing Order: DFS and Preferment
+
+advance processes states in lexicographic order,
+performing Depth-First Search (DFS) on each state.
+
+This DFS order is what guarantees the SQL standard's "preferment":
+
+  The branch that appears first in the PATTERN text takes precedence.
+
+Example: PATTERN (A | B) C
+
+  The first branch A of the ALT takes precedence over the second branch B.
+  When both A and B can match, the match via A is selected.
+
+nfa_add_state_unique() prevents duplicate addition of the same state,
+so the state added first (= from the preferred branch) is retained.
+
+IX-3. Routing Function: nfa_route_to_elem()
+
+All inter-element transitions in the advance phase go through
+nfa_route_to_elem().
+This function branches its behavior based on the type of the next element:
+
+  If the next element is VAR:
+    (1) Add the state to the context (nfa_add_state_unique)
+    (2) If the VAR has min=0, also add a skip path (recurse via next)
+    -> Expansion stops here (VAR is the element that "will consume the next
+       row")
+
+  If the next element is non-VAR (ALT, BEGIN, END, FIN):
+    -> Recursively call nfa_advance_state() to continue expansion
+
+With this structure, advance recursively follows epsilon transitions
+until reaching a VAR, consistently stopping only at VAR elements.
+
+IX-4. Per-Element advance Behavior
+
+(a) ALT (nfa_advance_alt)
+
+  Upon encountering an ALT element, all branches are expanded in order.
+  The first element of each branch is connected via a jump pointer.
+
+  idx=0 (ALT) -> branch 1 start (next) -> branch 2 start (jump) -> ...
+
+  nfa_advance_state() is recursively called for each branch.
+
+(b) BEGIN (nfa_advance_begin)
+
+  Handles group entry.
+  jump points to the element after END (= first element outside the group).
+
+  Greedy (default):
+    (1) Enter the group body (move via next, reset the count at that depth)
+    (2) If min=0, also add a group skip path (move via jump)
+
+  Reluctant:
+    Order reversed -- skip path first, group entry second.
+    If the skip path reaches FIN, the group entry path is not generated
+    (shortest match preferred).
+
+(c) END (nfa_advance_end)
+
+  Handles group termination. This is the core of the repetition logic.
+
+  Let count be the count at the current depth:
+
+  count < min:
+    Loop-back (move via jump, repeat the group body)
+
+    If the RPR_ELEM_EMPTY_LOOP flag is set:
+      In addition to loop-back, also add a fast-forward exit path.
+      This is because the body may produce an empty match, causing count
+      to never reach min. fast-forward resets counts[depth] to 0
+      and exits via next (treating the remaining required iterations
+      as empty matches).
+
+  min <= count < max:
+    Greedy: loop-back first, exit second
+    Reluctant: exit first, loop-back second
+               If the exit path reaches FIN, loop-back is omitted.
+
+  count >= max:
+    Unconditional exit (move via next)
+
+  On exit: reset counts[depth] = 0, and if the next element is an outer END,
+  increment the count at the outer depth.
+
+(d) VAR (nfa_advance_var)
+
+  Handles repeat/exit for a VAR element with a quantifier.
+
+  Let count be the count at the current depth:
+
+  count < min:
+    Unconditional loop (stay at the same elemIdx, wait for the next row)
+
+  min <= count < max:
+    Greedy: loop first, exit (next) second
+    Reluctant: exit first, loop second
+               If the exit path reaches FIN, loop is omitted.
+
+  count >= max:
+    Unconditional exit (move via next)
+
+  On exit: reset counts[depth] = 0.
+
+(e) FIN
+
+  Match success. The current state is moved to matchedState for recording,
+  and matchEndRow is set to the current row.
+
+  Upon reaching FIN, all remaining unprocessed states are removed
+  (early termination). By DFS order, the path that reached FIN first
+  has the highest preferment, so the rest are inferior paths.
+  This is the core mechanism that guarantees preferment.
+
+  In SKIP PAST LAST ROW mode, upon reaching FIN, subsequent contexts
+  that started within the match range are immediately pruned.
+
+IX-5. State Deduplication: nfa_add_state_unique()
+
+When adding a new state to a context, it is compared against existing
+states;
+if an identical state already exists, it is not added.
+
+Comparison criteria: elemIdx + counts[0..elem->depth] (see V-1)
+
+This deduplication is the core mechanism that suppresses NFA state
+explosion.
+Because DFS order causes preferred-branch states to be added first,
+identical states from lower-priority branches are automatically discarded.
+
+IX-6. Cycle Detection: nfaVisitedElems
+
+When a group body can produce an empty match,
+looping back from END may cause an infinite loop.
+
+Example: PATTERN ((A?)*)
+
+  A? has min=0, so it can pass through without matching.
+  If the outer group repeats: BEGIN -> A? skip -> END -> BEGIN -> ...
+
+To prevent this:
+
+  (1) At compile time: set the RPR_ELEM_EMPTY_LOOP flag on the END
+      of groups whose body is nullable.
+      The runtime effect of this flag is described in IX-4(c):
+      when count < min, a fast-forward exit path is added,
+      resolving the deadlock where count cannot increase due to empty
+      matches.
+
+  (2) At runtime: initialize the nfaVisitedElems bitmap immediately before
+      DFS expansion of each state within advance (once per state).
+      During DFS, epsilon elements (END, ALT, BEGIN) are marked in the
+      bitmap at nfa_advance_state entry.  VAR elements are marked later
+      when added to the state list (nfa_add_state_unique), so that
+      legitimate loop-back to the same VAR in a new group iteration
+      (e.g., END -> ALT -> same VAR) is not blocked.
+      If a previously visited elemIdx is revisited, that path is terminated.
+
+  Note: the bitmap tracks only elemIdx and does not consider counts.
+  Therefore, legitimate revisits to the same elemIdx but with different
+  counts may also be blocked.  This only occurs when the group body is
+  nullable (all paths can match empty), causing END -> loop-back ->
+  skip -> END within a single DFS.  In such cases the END element has
+  the RPR_ELEM_EMPTY_LOOP flag, so the fast-forward exit (IX-4(c))
+  provides an alternative path that bypasses the cycle.
+
+Chapter X  Match Result Processing
+============================================================================
+
+X-1. Match Result
+
+RPR tracks the current match result as a single entry in WindowAggState
+with four fields: rpr_match_valid, rpr_match_matched, rpr_match_start,
+and rpr_match_length.  When rpr_match_valid is true, the entry describes
+the match result for the position at rpr_match_start: rpr_match_matched
+indicates success or failure, and rpr_match_length gives the number of
+rows consumed.  A match with rpr_match_length 0 represents an empty match
+(pattern matched but consumed no rows).  When rpr_match_valid is false,
+the position has not been evaluated yet (RF_NOT_DETERMINED).
+
+A row's status against the current match result can be obtained by
+calling get_reduced_frame_status().
+
+X-2. AFTER MATCH SKIP
+
+Determines the starting point for the next match attempt after a successful
+match:
+
+  SKIP TO NEXT ROW:
+    New match attempt begins from the row after the match start row.
+    Overlapping matches are possible.
+
+  SKIP PAST LAST ROW:
+    New match attempt begins from the row after the match end row.
+    Only non-overlapping matches are possible.
+
+X-3. INITIAL vs SEEK
+
+  Standard definition (section 6.12):
+  INITIAL: "is used to look for a match whose first row is R."
+  SEEK:    "is used to permit a search for the first match anywhere
+           from R through the end of the full window frame."
+  In either case, if there is no match, the reduced window frame is empty.
+  The default is INITIAL.
+
+  Current implementation:
+  SEEK is not supported (the parser raises an error).
+  Only INITIAL is supported, searching only for matches starting at each
+  row position pos.
+
+X-4. Bounded Frame Handling
+
+  When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
+  FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
+  frameOffset indicating the upper bound.  Before the match phase,
+  any context whose match has exceeded the frame boundary
+  (currentPos >= matchStartRow + frameOffset + 1) is finalized early
+  by forcing a mismatch.  This prevents matches from extending beyond
+  the window frame.  The sum is clamped to PG_INT64_MAX on overflow.
+
+  Note that bounded frames also disable context absorption at the
+  planner level (see VIII-3(b)), since the frame boundary breaks the
+  monotonicity assumption required for correct absorption.
+
+Chapter XI  Worked Example: Full Execution Trace
+============================================================================
+
+XI-1. Query
+
+  SELECT company, tdate, price,
+         first_value(price) OVER w AS start_price,
+         last_value(price) OVER w AS end_price
+  FROM stock
+  WINDOW w AS (
+    PARTITION BY company
+    ORDER BY tdate
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS price > PREV(price),
+           B AS price < PREV(price)
+  );
+
+XI-2. Data
+
+  Row#    tdate       price
+  --------------------------
+  0       2024-01-01  100
+  1       2024-01-02  110
+  2       2024-01-03  120
+  3       2024-01-04  115
+  4       2024-01-05  130
+
+XI-3. Compilation Result
+
+  PATTERN (A+ B) -> unchanged after optimization
+
+  idx  varId  depth  min  max  next  jump
+  -----------------------------------------
+   0   A(0)   0      1    INF  1     -1     A+
+   1   B(1)   0      1    1    2     -1     B
+   2   FIN    0      1    1    -1    -1
+
+  DEFINE: A -> "price > PREV(price)", B -> "price < PREV(price)"
+  isAbsorbable = true (A+ is a simple unbounded VAR)
+
+XI-4. Execution Trace
+
+--- Row 0 (price=100) ---
+
+  update_reduced_frame(0) called.
+
+  Context C0 created (matchStartRow=0).
+  Initial advance: elemIdx=0(A) -> VAR, so state is added.
+  C0.states = [{elemIdx=0, counts=[0]}]
+
+  nfa_evaluate_row(0):
+    A: price(100) > PREV(price) -> no PREV -> false
+    B: price(100) < PREV(price) -> no PREV -> false
+    varMatched = [false, false]
+
+  ExecRPRProcessRow(0):
+    Phase 1 (Match): A(0) state vs varMatched[0]=false -> state removed
+    C0.states = [] (empty)
+
+    Phase 2 (Absorb): skipped (no states)
+    Phase 3 (Advance): skipped (no states)
+
+  C0.states is empty, so the loop terminates.
+  matchEndRow < matchStartRow -> unmatched.
+
+--- Row 1 (price=110) ---
+
+  update_reduced_frame(1) called.
+
+  Context C1 created (matchStartRow=1).
+  Initial advance: C1.states = [{elemIdx=0, counts=[0]}]
+
+  nfa_evaluate_row(1):
+    A: 110 > PREV(100) -> true
+    B: 110 < PREV(100) -> false
+    varMatched = [true, false]
+
+  ExecRPRProcessRow(1):
+    Phase 1 (Match): A(0) match succeeds -> counts[0]++ -> counts=[1]
+    C1.states = [{elemIdx=0, counts=[1]}]
+
+    Phase 3 (Advance):
+      State {elemIdx=0, counts=[1]}: A+ (min=1, count=1, max=INF)
+        count >= min, so:
+        Greedy -> loop first: keep {elemIdx=0, counts=[1]}
+                  exit: reset counts[0]=0, next(=1) -> {elemIdx=1,
+                        counts=[0]}
+    C1.states = [{elemIdx=0, counts=[1]}, {elemIdx=1, counts=[0]}]
+
+--- Row 2 (price=120) ---
+
+  Context C2 created (matchStartRow=2).
+  Initial advance: C2.states = [{elemIdx=0, counts=[0]}]
+
+  nfa_evaluate_row(2):
+    A: 120 > PREV(110) -> true
+    B: 120 < PREV(110) -> false
+    varMatched = [true, false]
+
+  C1 ExecRPRProcessRow(2):
+    Phase 1 (Match):
+      {elemIdx=0, counts=[1]}: A matches -> counts=[2]
+      {elemIdx=1, counts=[0]}: B does not match -> removed
+    C1.states = [{elemIdx=0, counts=[2]}]
+
+  C2 ExecRPRProcessRow(2):
+    Phase 1 (Match):
+      {elemIdx=0, counts=[0]}: A matches -> counts=[1]
+    C2.states = [{elemIdx=0, counts=[1]}]
+
+    Phase 2 (Absorb):
+      Does C1 (started earlier) cover C2?
+        C1: {elemIdx=0, counts=[2]}, C2: {elemIdx=0, counts=[1]}
+        Same elemIdx, C1.counts >= C2.counts -> covered
+      C2 absorbed. -> removed.
+
+    Phase 3 (Advance):
+      {elemIdx=0, counts=[2]}: Greedy -> loop + exit
+        Loop: {elemIdx=0, counts=[2]}
+        Exit: reset counts[0]=0, next(=1) -> {elemIdx=1, counts=[0]}
+    C1.states = [{elemIdx=0, counts=[2]}, {elemIdx=1, counts=[0]}]
+
+  Context C3 created (matchStartRow=3).
+
+--- Row 3 (price=115) ---
+
+  nfa_evaluate_row(3):
+    A: 115 > PREV(120) -> false
+    B: 115 < PREV(120) -> true
+    varMatched = [false, true]
+
+  ExecRPRProcessRow(3):
+    Phase 1 (Match):
+      {elemIdx=0, counts=[2]}: A does not match -> removed
+      {elemIdx=1, counts=[0]}: B matches -> counts=[1]
+    C1.states = [{elemIdx=1, counts=[1]}]
+
+    Phase 3 (Advance):
+      {elemIdx=1, counts=[1]}: B (min=1, max=1)
+        count(1) >= max(1) -> unconditional exit
+        Reset counts[0]=0, next = 2 (FIN)
+      FIN reached -> matchEndRow = 3, matchedState recorded.
+      Early termination: no remaining states, so completed immediately.
+    C1.states = [] (empty after reaching FIN)
+
+  C1.states is empty and matchEndRow=3 >= matchStartRow=1 -> match succeeds.
+
+  rpr_match_start = 1, rpr_match_length = 3
+
+--- Row 4 (price=130) ---
+
+  update_reduced_frame(4) called.
+  C3 was already created but matchStartRow=3, so it is not applicable.
+  New context C4 created (matchStartRow=4).
+
+  nfa_evaluate_row(4):
+    A: 130 > PREV(115) -> true
+    B: 130 < PREV(115) -> false
+
+  ... No subsequent rows, so ExecRPRFinalizeAllContexts() is called.
+  Match incomplete -> unmatched.
+
+XI-5. Final Result
+
+  Row 0: unmatched     -> frame = the row itself
+  Row 1: match head    -> frame = rows 1 through 3
+  Row 2: inside match  -> skipped
+  Row 3: inside match  -> skipped
+  Row 4: unmatched     -> frame = the row itself
+
+Chapter XII  Summary of Key Design Decisions
+============================================================================
+
+XII-1. Flat Array vs Tree-Based NFA
+
+  Choice: Flat array (RPRPatternElement[])
+
+  Rationale:
+  - Cache-friendly: 16-byte fixed size, contiguous memory
+  - Index-based references: 2-byte indices instead of pointers
+  - Easy to serialize: can use memcpy when passing to plan nodes
+
+XII-2. Forward-only Execution vs Backtracking
+
+  Choice: Forward-only (state set tracking)
+
+  Rationale:
+  - Backtracking takes exponential time in the worst case
+  - NFA simulation guarantees polynomial time
+  - DFS order naturally guarantees preferment.
+    Greedy/reluctant per quantifier requires only reversing the DFS order
+  - Window functions receive sorted rows sequentially.
+    Forward-only fits directly into this pipeline,
+    whereas backtracking requires re-fetching previous rows
+  - DEFINE conditions are SQL expressions (PREV, RUNNING aggregates, etc.)
+    with high re-evaluation cost. Forward-only requires only one evaluation
+    per row
+
+XII-3. Per-Context Management
+
+  Choice: Independent context per start row
+
+  Rationale:
+  - Supports overlapping matches under SKIP TO NEXT ROW
+  - Determines the frame for each row independently
+  - Absorption optimization can eliminate redundant contexts in O(n)
+
+XII-4. Memory Pool Management
+
+  Choice: Custom free list
+
+  Rationale:
+  - NFA states are created and destroyed in large numbers per row
+  - Avoids palloc/pfree overhead
+  - State size is variable (counts[] array), but within a single query
+    maxDepth is fixed, so all states have the same size
+
+XII-5. Execution Optimization Summary
+
+  The following optimizations make the NFA simulation practical.
+
+  -- Compile-time --
+
+  (1) AST Optimization (IV-3)
+
+    Simplifies the AST 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.
+
+    Significance: Reducing the element count directly shrinks the state
+    space, decreasing the cost of all subsequent runtime phases (match,
+    absorb, advance).
+
+  -- Runtime: advance phase --
+
+  (2) Group Skip (IX-4(b))
+
+    At the BEGIN of a group with min=0, uses jump to skip the entire
+    group. Moves directly to the first element outside the group without
+    exploring the group body. Greedy enters then skips; Reluctant skips
+    then enters.
+
+    Significance: For optional groups (min=0), immediately generates
+    a skip path without exploring the body, avoiding unnecessary DFS
+    expansion.
+
+  (3) State Deduplication (IX-5)
+
+    During advance, DFS may generate states with the same (elemIdx,
+    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).
+    This process can also produce duplicate states reaching the same END.
+    nfa_add_state_unique() blocks duplicate addition of identical states
+    in both cases.
+
+    Significance: Prevents exponential growth of the state count in
+    ALT branches and quantifier expansion. Since DFS order causes the
+    preferred branch's state to be registered first, identical states
+    from lower-priority branches are automatically discarded, thereby
+    also guaranteeing preferment.
+
+  (4) Cycle Detection and Fast-Forward (IX-6, IX-4(c))
+
+    When a nullable group body (e.g., A?) repeats empty matches,
+    the END -> BEGIN loop-back can continue indefinitely.
+
+    Two mechanisms resolve this:
+    - A visited bitmap (nfaVisitedElems) blocks revisitation of the
+      same element, preventing infinite loops (safety)
+    - At an END with the RPR_ELEM_EMPTY_LOOP flag set, when
+      count < min, the remaining required iterations are treated as
+      empty matches and a fast-forward exit path out of the group is
+      added (correctness)
+
+    Significance: Cycle detection guarantees termination, and
+    fast-forward guarantees that the min condition is satisfied.
+    Without these, patterns containing nullable groups would fall
+    into infinite loops or fail to match.
+
+  (5) Match Pruning (IX-4(e))
+
+    When a state reaches FIN during advance, all remaining unprocessed
+    states of that context are removed. Because of DFS order, the path
+    that reaches FIN first has the highest preferment, so the remaining
+    paths are inferior.
+
+    Significance: Once the best match is determined, exploration of
+    inferior paths is immediately terminated. This mechanism achieves
+    both preferment guarantees and performance optimization.
+
+  -- Runtime: inter-context --
+
+  (6) Early Termination (SKIP PAST LAST ROW)
+
+    In SKIP PAST LAST ROW mode, when a match is found, subsequent
+    contexts whose start rows fall within the match range are pruned
+    immediately without further processing.
+    In SKIP TO NEXT ROW mode, overlapping contexts are preserved
+    because each row requires its own independent match.
+
+    Significance: Prunes subsequent contexts whose start rows overlap
+    with a prior match range, avoiding unnecessary processing.
+
+  (7) Context Absorption (Chapter VIII)
+
+    If an independent context is created for each row, O(n^2) states
+    accumulate. By exploiting the monotonicity that an earlier-started
+    context subsumes the states of a later-started context, redundant
+    contexts are eliminated early.
+
+    Absorbability is determined per-element; comparison is performed
+    only at elements with the RPR_ELEM_ABSORBABLE flag (see IV-5).
+
+    Significance: Keeps the number of active contexts at a constant
+    level, achieving O(n^2) -> O(n) time complexity. Without this,
+    performance degrades sharply on long partitions.
+
+Appendix A. Key Function Index
+============================================================================
+
+  Function                      File                  Role
+  --------------------------------------------------------------------------
+  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
+  finalizeRPRPattern            rpr.c                 Finalization
+  computeAbsorbability          rpr.c                 Absorption analysis
+  update_reduced_frame          nodeWindowAgg.c       Execution main loop
+  nfa_evaluate_row              nodeWindowAgg.c       DEFINE evaluation
+  ExecRPRStartContext           execRPR.c             Context creation
+  ExecRPRProcessRow             execRPR.c             3-phase processing
+  nfa_match                     execRPR.c             Phase 1
+  nfa_absorb_contexts           execRPR.c             Phase 2
+  nfa_advance                   execRPR.c             Phase 3
+  nfa_advance_state             execRPR.c             Per-state branching
+  nfa_route_to_elem             execRPR.c             Element routing
+  nfa_advance_alt               execRPR.c             ALT handling
+  nfa_advance_begin             execRPR.c             BEGIN handling
+  nfa_advance_end               execRPR.c             END handling
+  nfa_advance_var               execRPR.c             VAR handling
+  nfa_add_state_unique          execRPR.c             Deduplication
+  nfa_states_covered            execRPR.c             Absorption check
+  nfa_reevaluate_dependent_vars execRPR.c             Per-context re-eval
+  ExecRPRGetHeadContext         execRPR.c             Context lookup
+  ExecRPRFreeContext            execRPR.c             Context deallocation
+  ExecRPRCleanupDeadContexts    execRPR.c             Dead context cleanup
+  ExecRPRFinalizeAllContexts    execRPR.c             Partition-end finalize
+  ExecRPRRecordContextSuccess   execRPR.c             Stats: match success
+  ExecRPRRecordContextFailure   execRPR.c             Stats: match failure
+  compute_nav_offsets           createplan.c          Trim offset computation
+
+Appendix B. Data Structure Relationship Diagram
+============================================================================
+
+  Parser Layer
+  --------
+  RPCommonSyntax
+    |--- rpSkipTo: RPSkipTo
+    |--- initial: bool
+    +--- rpPattern: RPRPatternNode* (tree)
+         |--- nodeType: VAR | SEQ | ALT | GROUP
+         |--- min, max: quantifier
+         |--- varName: variable name (VAR only)
+         +--- children: List* (SEQ/ALT/GROUP only)
+
+  Planner Layer
+  ----------
+  WindowAgg (plan node)
+    |--- rpSkipTo: RPSkipTo
+    |--- defineClause: List<TargetEntry>
+    +--- rpPattern: RPRPattern*
+         |--- numVars: int
+         |--- varNames: char**
+         |--- maxDepth: RPRDepth
+         |--- isAbsorbable: bool
+         |--- numElements: int
+         +--- elements: RPRPatternElement[]  (flat array)
+              |--- varId      (1B)
+              |--- depth      (1B)
+              |--- flags      (1B)
+              |--- reserved   (1B)
+              |--- min, max   (4B + 4B)
+              +--- next, jump (2B + 2B)
+
+  Executor Layer
+  ----------
+  WindowAggState
+    |--- rpSkipTo: RPSkipTo (AFTER MATCH SKIP mode)
+    |--- rpPattern: RPRPattern* (copied from plan)
+    |--- defineVariableList: List<String> (variable names, DEFINE order)
+    |--- defineClauseList: List<ExprState>
+    |--- nfaVarMatched: bool[] (per-row cache)
+    |--- nfaVisitedElems: bitmapword* (cycle detection)
+    |--- nfaStateSize: Size (pre-calculated RPRNFAState allocation size)
+    |--- nfaContext <-> nfaContextTail (doubly-linked list)
+    |   +--- RPRNFAContext
+    |       |--- states: RPRNFAState* (linked list)
+    |       |   |--- elemIdx
+    |       |   |--- counts[]
+    |       |   +--- isAbsorbable
+    |       |--- matchStartRow, matchEndRow
+    |       |--- lastProcessedRow
+    |       |--- matchedState (cloned on FIN arrival)
+    |       |--- hasAbsorbableState
+    |       +--- allStatesAbsorbable
+    |--- nfaContextFree (recycling pool)
+    +--- nfaStateFree (recycling pool)
+
+Appendix C. NFA Element Array Examples
+============================================================================
+
+C-1. PATTERN (A B C)
+
+  idx  varId  depth  min  max  next  jump
+  ------------------------------------------
+   0   A      0      1    1    1     -1
+   1   B      0      1    1    2     -1
+   2   C      0      1    1    3     -1
+   3   FIN    0      1    1    -1    -1
+
+C-2. PATTERN (A+ B*)
+
+  idx  varId  depth  min  max  next  jump  flags
+  ------------------------------------------------------------------------
+   0   A      0      1    INF  1     -1    ABSORBABLE | ABSORBABLE_BRANCH
+   1   B      0      0    INF  2     -1
+   2   FIN    0      1    1    -1    -1
+
+  Only A+ is the absorption point (Case 1). Once past A,
+  absorption is permanently disabled for that state.
+
+C-3. PATTERN (A | B | C)
+
+  idx  varId  depth  min  max  next  jump
+  ----------------------------------------
+   0   ALT    0      1    1    1     -1    alternation start
+   1   A      1      1    1    4     2     branch 1 -> FIN, jump -> branch 2
+   2   B      1      1    1    4     3     branch 2 -> FIN, jump -> branch 3
+   3   C      1      1    1    4     -1    branch 3 -> FIN
+   4   FIN    0      1    1    -1    -1
+
+C-4. PATTERN ((A B)+ C)
+
+  idx  varId    depth  min  max  next  jump  flags
+  --------------------------------------------------------------------------
+   0   BEGIN    0      1    INF  1     4     ABSORBABLE_BRANCH
+   1   A        1      1    1    2     -1    ABSORBABLE_BRANCH
+   2   B        1      1    1    3     -1    ABSORBABLE_BRANCH
+   3   END      0      1    INF  4     1     ABSORBABLE | ABSORBABLE_BRANCH
+   4   C        0      1    1    5     -1
+   5   FIN      0      1    1    -1    -1
+
+  Case 2: GROUP+ with {1,1} body VARs. A, B are branches;
+  END is the absorption point. Compare with C-6 (Case 3).
+
+C-5. PATTERN ((A | B)+? C)
+
+  idx  varId    depth  min  max   next  jump  flags
+  -------------------------------------------------------------------
+   0   BEGIN    0      1    INF   1     5     RELUCTANT, group start
+   1   ALT      1      1    1     2     -1    alternation start
+   2   A        2      1    1     4     3     branch 1
+   3   B        2      1    1     4     -1    branch 2
+   4   END      0      1    INF   5     1     RELUCTANT, group end
+   5   C        0      1    1     6     -1
+   6   FIN      0      1    1     -1    -1
+
+C-6. PATTERN ((A+ B)+ C)  -- Absorbability flag example
+
+  idx  varId    depth  min  max   next  jump  flags
+  ---------------------------------------------------------------------------
+   0   BEGIN    0      1    INF   1     4     ABSORBABLE_BRANCH, group start
+   1   A        1      1    INF   2     -1    ABSORBABLE | ABSORBABLE_BRANCH
+   2   B        1      1    1     3     -1
+   3   END      0      1    INF   4     1     group end
+   4   C        0      1    1     5     -1
+   5   FIN      0      1    1     -1    -1
+
+  Recurses from BEGIN into the body -> A matches Case 1 (simple VAR+).
+  A gets ABSORBABLE | ABSORBABLE_BRANCH, BEGIN gets ABSORBABLE_BRANCH.
+  B and END get no flags -> absorption stops once the state advances to B.
+  (See IV-5 Case 3)
+
+C-7. PATTERN ((A+ B | C*)+ D)  -- Per-branch absorption in ALT
+
+  idx  varId    depth  min  max   next  jump  flags
+  ---------------------------------------------------------------------------
+   0   BEGIN    0      1    INF   1     6     ABSORBABLE_BRANCH
+   1   ALT      1      1    1     2     -1    ABSORBABLE_BRANCH
+   2   A        2      1    INF   3     4     ABSORBABLE | ABSORBABLE_BRANCH
+   3   B        2      1    1     5     -1
+   4   C        2      0    INF   5     -1    ABSORBABLE | ABSORBABLE_BRANCH
+   5   END      0      1    INF   6     1     EMPTY_LOOP
+   6   D        0      1    1     7     -1
+   7   FIN      0      1    1     -1    -1
+
+  ALT branches are checked independently for absorbability.
+  Branch 1: A+ matches Case 1 -> A gets ABSORBABLE. B has no flag.
+  Branch 2: C* matches Case 1 -> C gets ABSORBABLE.
+  Both A and C get ABSORBABLE_BRANCH as part of their respective branch
+  paths.
+  END has EMPTY_LOOP: branch 2 (C*) is nullable, making the group body
+  nullable.
+  BEGIN and ALT get ABSORBABLE_BRANCH (on the path to absorbable elements).
+
+============================================================================
+  End of document
+============================================================================
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 15e439daaae..96cfbfbc0a2 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -30,1584 +30,8 @@
 #include "utils/memutils.h"
 
 /*
- * ============================================================================
- *   PostgreSQL Row Pattern Recognition: Flat-Array Stream NFA Guide
- * ============================================================================
- *
- *   Target audience: Developers with a basic understanding of the PostgreSQL
- *                    executor and planner architecture
- *
- *   Scope: The entire process from PATTERN/DEFINE clause parsing to NFA
- *          runtime execution
- *
- *   Related code:
- *     - src/backend/parser/parse_rpr.c          (parser phase)
- *     - src/backend/optimizer/plan/rpr.c        (optimizer phase)
- *     - src/backend/executor/nodeWindowAgg.c    (executor phase, window agg)
- *     - src/backend/executor/execRPR.c          (executor phase, NFA engine)
- *     - src/include/executor/execRPR.h          (NFA public API)
- *     - src/include/nodes/plannodes.h           (plan node definitions)
- *     - src/include/nodes/execnodes.h           (execution state definitions)
- *     - src/include/optimizer/rpr.h             (types and constants)
- *     - src/backend/optimizer/plan/createplan.c (nav offset computation)
- *
- * ============================================================================
- *
- * What is a Flat-Array Stream NFA?
- *
- *   The NFA in this implementation is not a traditional state-transition graph
- *   but a flat array of fixed-size 16-byte elements. At runtime, it processes
- *   the row stream in a forward-only manner, expanding epsilon transitions
- *   eagerly without backtracking.
- *
- *   - Flat-Array: Pattern compiled into a flat array,
- *                 not a graph (Chapter IV)
- *   - Stream:     Rows consumed sequentially in one direction,
- *                 never revisited (Chapter XII)
- *   - NFA:        Nondeterministic execution where multiple states
- *                 coexist within a single context (Chapter VI)
- *
- * Chapter I  Row Pattern Recognition Overview
- * ============================================================================
- *
- * Row Pattern Recognition (hereafter RPR) is a feature introduced in SQL:2016
- * that matches regex-based patterns against ordered row sets.
- *
- * The SQL standard defines two forms:
- *
- *   Feature R010: MATCH_RECOGNIZE (FROM clause)
- *     - Dedicated table operator
- *     - Provides dedicated functions such as MATCH_NUMBER(), CLASSIFIER()
- *     - Supports ONE ROW PER MATCH / ALL ROWS PER MATCH
- *
- *   Feature R020: RPR in a window (WINDOW clause)
- *     - Integrated into the existing window function framework
- *     - Supports ALL ROWS PER MATCH only
- *     - No MATCH_NUMBER()
- *
- * This implementation targets Feature R020.
- *
- * The basic syntax is as follows:
- *
- *   SELECT ...
- *   OVER (
- *     PARTITION BY ...
- *     ORDER BY ...
- *     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- *     [INITIAL | SEEK]   -- SEEK is defined in the standard but not implemented
- *     AFTER MATCH SKIP TO NEXT ROW | SKIP PAST LAST ROW
- *     PATTERN ( <regex> )
- *     DEFINE <variable> AS <condition>, ...
- *   )
- *
- * The PATTERN clause is a regular expression over row pattern variables.
- * The DEFINE clause specifies boolean conditions that determine whether each
- * variable evaluates to true for the current row.
- *
- * Example:
- *
- *   PATTERN (A+ B)
- *   DEFINE A AS price > PREV(price),
- *          B AS price < PREV(price)
- *
- * This pattern matches "a span where prices rise consecutively then drop."
- *
- * 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                 |
- *   +------------------------------------------------------------+
- *
- * Each phase uses independent data structures, and the interfaces between
- * phases are well-defined:
- *
- *   Parser -> Planner:    WindowClause.rpPattern (RPRPatternNode tree)
- *                         WindowClause.defineClause (TargetEntry list)
- *
- *   Planner -> Executor:  WindowAgg.rpPattern (RPRPattern struct)
- *                         WindowAgg.defineClause (TargetEntry list)
- *
- * Chapter III  Parsing Phase
- * ============================================================================
- *
- * III-1. Entry Point
- *
- *   transformWindowDefinitions() (parse_clause.c)
- *     +-- transformRPR() (parse_rpr.c)
- *
- * transformRPR() is invoked when RPCommonSyntax is present and performs the
- * following:
- *
- *   (1) Frame option validation
- *       - Only ROWS is allowed (RANGE, GROUPS are not)
- *       - The start boundary must be CURRENT ROW
- *       - EXCLUDE option is not allowed
- *
- *   (2) Transcription to WindowClause
- *       - Copies rpPattern, rpSkipTo, initial fields
- *
- *   (3) DEFINE clause transformation (transformDefineClause)
- *
- * III-2. PATTERN AST
- *
- * The parser transforms the PATTERN clause into an RPRPatternNode tree.
- * Each node has one of the following four types:
- *
- *   RPR_PATTERN_VAR    Variable reference. Name stored in varName field.
- *   RPR_PATTERN_SEQ    Concatenation. Children node list in children.
- *   RPR_PATTERN_ALT    Alternation. Branch node list in children.
- *   RPR_PATTERN_GROUP  Group (parentheses). Body node list in children.
- *
- * All nodes have min/max fields to express quantifiers:
- *
- *   A       -> VAR(A, min=1, max=1)
- *   A+      -> VAR(A, min=1, max=INF)
- *   A*      -> VAR(A, min=0, max=INF)
- *   A?      -> VAR(A, min=0, max=1)
- *   A{3,5}  -> VAR(A, min=3, max=5)
- *
- * If the reluctant field is true, the quantifier is reluctant (non-greedy).
- * (RPRPatternNode.reluctant is bool; reluctant_location is the separate
- * ParseLoc field holding the '?' token position, or -1 if absent.)
- *
- * Example: PATTERN ((A+ B) | C*)
- *
- *   ALT
- *   +-- SEQ
- *   |   +-- VAR(A, 1, INF)
- *   |   +-- VAR(B, 1, 1)
- *   +-- VAR(C, 0, INF)
- *
- * III-3. DEFINE Clause Transformation
- *
- * transformDefineClause() processes each DEFINE variable as follows:
- *
- *   (1) Checks for duplicate variable names
- *   (2) Transforms the expression via transformExpr()
- *   (3) Extracts Var nodes via pull_var_clause() and ensures each is
- *       present in the query targetlist, so the planner propagates the
- *       referenced columns through the plan tree
- *   (4) Wraps in a TargetEntry with the variable name set in resname
- *
- * After all variables are processed:
- *   (5) Coerces each expression to Boolean type (coerce_to_boolean)
- *
- * Variables that are used in PATTERN but not defined in DEFINE are implicitly
- * evaluated as TRUE (matching all rows).
- *
- * 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()
- *
- *   Phase 1: AST optimization (optimizeRPRPattern)
- *   Phase 2: Statistics collection (scanRPRPattern)
- *   Phase 3: Memory allocation (allocateRPRPattern)
- *   Phase 4: NFA element fill (fillRPRPattern)
- *   Phase 5: Finalization (finalizeRPRPattern)
- *   Phase 6: Absorbability analysis (computeAbsorbability)
- *
- * IV-3. Phase 1: AST Optimization
- *
- * After copying the parser-generated AST, the following optimizations are
- * applied:
- *
- *   (a) SEQ flattening: Unwrap nested SEQ nodes
- *       SEQ(A, SEQ(B, C)) -> SEQ(A, B, C)
- *
- *   (b) Consecutive variable merging: Merge consecutive occurrences of the
- *       same variable into a single quantifier
- *       A A -> A{2}
- *       A{2,3} A{1,2} -> A{3,5}
- *
- *   (c) Consecutive group merging: Merge repeated identical groups
- *       (A B)+ (A B)+ -> (A B){2,INF}
- *
- *   (d) Consecutive ALT merging: Merge repeated identical ALT nodes
- *       (A | B) (A | B) (A | B) -> (A | B){3}
- *
- *   (e) Prefix/suffix absorption: Absorb identical sequences before/after
- *       a group
- *       A B (A B)+ -> (A B){2,INF}
- *
- *   (f) ALT flattening and deduplication
- *       (A | (B | C)) -> (A | B | C)
- *       (A | B | A) -> (A | B)
- *
- *   (g) Quantifier multiplication: Collapse nested quantifiers when safe
- *       (A+)+ -> A+
- *       (A{2,3}){5} -> A{10,15}
- *
- *   (h) Single-child unwrap
- *       SEQ(A) -> A,  (A){1,1} -> A
- *
- * IV-4. Phase 4: NFA Element Array Generation
- *
- * Transforms the optimized AST into a flat array of RPRPatternElement.
- * This is the core data structure used for NFA simulation at runtime.
- *
- * RPRPatternElement struct (16 bytes):
- *
- *   Field      Size     Description
- *   ---------------------------------------------------------
- *   varId      1B      Variable ID (0-251) or control code (252-255)
- *   depth      1B      Group nesting depth
- *   flags      1B      Bit flags (see below)
- *   reserved   1B      Padding
- *   min        4B      Quantifier lower bound
- *   max        4B      Quantifier upper bound
- *   next       2B      Next element index (sequential flow)
- *   jump       2B      Branch target index (for ALT/GROUP)
- *
- * Control codes:
- *
- *   RPR_VARID_BEGIN (252)  Group start marker
- *   RPR_VARID_END   (253)  Group end marker
- *   RPR_VARID_ALT   (254)  Alternation start marker
- *   RPR_VARID_FIN   (255)  Pattern completion marker
- *
- * Element flags (1 byte, bitmask):
- *
- *   0x01  RPR_ELEM_RELUCTANT          (VAR, BEGIN, END)
- *         Non-greedy quantifier.  Prefers shorter match: try exit-loop
- *         first, then repeat.  Set on VAR for simple (A+?),
- *         on BEGIN+END for group ((...)+?).
- *
- *   0x02  RPR_ELEM_EMPTY_LOOP         (END)
- *         Group body can produce empty match (all children nullable).
- *         Creates a fast-forward exit clone alongside the normal
- *         loop-back so cycle detection doesn't kill legitimate
- *         matches. (IV-4b)
- *
- *   0x04  RPR_ELEM_ABSORBABLE_BRANCH  (VAR, BEGIN, END, ALT)
- *         Element lies within an absorbable region.  Used at runtime
- *         to track whether the current NFA state is in an absorbable
- *         context.
- *
- *   0x08  RPR_ELEM_ABSORBABLE         (VAR, END)
- *         Absorption judgment 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
- *
- *   Accessor macros:
- *     RPRElemIsReluctant(e)        (e)->flags & 0x01
- *     RPRElemCanEmptyLoop(e)       (e)->flags & 0x02
- *     RPRElemIsAbsorbableBranch(e) (e)->flags & 0x04
- *     RPRElemIsAbsorbable(e)       (e)->flags & 0x08
- *
- * Example: PATTERN (A+ B | C)
- *
- *   AST: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
- *
- *   Compilation result:
- *
- *   idx  varId  depth  min  max  next  jump  Description
- *   ------------------------------------------------------------
- *    0   ALT    0      1    1    1     -1    Alternation start
- *    1   A(0)   1      1    INF  2     3     Branch 1: A+
- *    2   B(1)   1      1    1    4     -1    Branch 1: B -> FIN
- *    3   C(2)   1      1    1    4     -1    Branch 2: C -> FIN
- *    4   FIN    0      1    1    -1    -1    Pattern completion
- *
- *   - idx 0: ALT marker. next(=1) is the start of the first branch
- *   - idx 1: Variable A. next(=2) is B, jump(=3) is the start of the second
- *            branch
- *   - idx 2: Variable B. next(=4) is FIN
- *   - idx 3: Variable C. next(=4) is FIN
- *   - idx 4: FIN marker. Match completion signal
- *
- * Roles of next and jump:
- *
- *   - next: The next element to move to "after consuming" the current element.
- *           For VAR, the next position after a successful match.
- *           For BEGIN/END, the next position inside/outside the group.
- *
- *   - jump: The element to "skip to."
- *           In ALT, a jump from one branch to the next branch.
- *           In BEGIN, a skip path to END+1 (for groups with min=0).
- *           In END, a loop-back to the start of the group body.
- *
- * Example: PATTERN ((A B)+)
- *
- *   idx  varId    depth  min  max  next  jump  Description
- *   --------------------------------------------------------------
- *    0   BEGIN    0      1    INF  1     4     Group start
- *    1   A(0)     1      1    1    2     -1    A
- *    2   B(1)     1      1    1    3     -1    B
- *    3   END      0      1    INF  4     1     Group end
- *    4   FIN      0      1    1    -1    -1    Pattern completion
- *
- *   - idx 0: BEGIN. next(=1) enters the group body.
- *            jump(=4) skips to after END = FIN (used when min=0).
- *   - idx 3: END. next(=4) exits the group.
- *            jump(=1) loops back to the start of the group body.
- *
- * 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:
- *
- *   Greedy (default):  try loop-back first, then exit  (prefer longer match)
- *   Reluctant:         try exit first, then loop-back   (prefer shorter match)
- *
- * The flag is set on all elements that carry the quantifier:
- *
- *   Simple VAR (A+?):     RPR_ELEM_RELUCTANT on the VAR element
- *   Group ((...)+?):      RPR_ELEM_RELUCTANT on BEGIN and END elements
- *
- * At runtime (nfa_advance), the flag controls DFS exploration order:
- *
- *   VAR with quantifier:
- *     Greedy:    primary path = next (continue), clone = jump (skip)
- *     Reluctant: primary path = jump (skip), clone = next (continue)
- *
- *   END element:
- *     Greedy:    primary path = jump (loop-back), clone = next (exit)
- *     Reluctant: primary path = next (exit), clone = jump (loop-back)
- *
- *   BEGIN with min=0:
- *     Greedy:    primary path = next (enter group), clone = jump (skip)
- *     Reluctant: primary path = jump (skip), clone = next (enter group)
- *
- * The absorption optimization requires greedy quantifiers. Reluctant
- * quantifiers are excluded from absorbability analysis (see IV-5).
- *
- * IV-4b. Empty Loop Flag (RPR_ELEM_EMPTY_LOOP)
- *
- * The empty-loop flag is set during Phase 4 (fillRPRPatternGroup) on the END
- * element when the group body is nullable -- i.e., every path through the
- * body can match zero rows (all children are nullable).
- *
- * Example patterns that trigger this flag:
- *
- *   (A?)*    A is nullable (min=0), so group body is nullable -> END gets flag
- *   (A? B?)+ Both children nullable -> body nullable -> END gets flag
- *   (A | B*) B* is nullable, making the ALT nullable -> END gets flag
- *
- * The flag works in conjunction with the empty match cycle detection
- * (elemIdx visited bitmap). Without this flag, cycle detection alone would
- * cause legitimate matches to fail.
- *
- * Problem example: (A*){2,3}
- *   - Iteration 1: A* consumes all available rows -> count=1, END reached
- *   - Loop-back for iteration 2: A* matches zero rows -> END reached again
- *   - Cycle detection sees the same elemIdx on the same row -> state killed
- *   - count never reaches min(2) -> match fails (incorrect)
- *
- * With the RPR_ELEM_EMPTY_LOOP flag, nfa_advance_end creates two paths:
- * the normal loop-back (which cycle detection will eventually kill) and
- * a fast-forward exit clone that bypasses the loop entirely.
- * (See IX-4(c) for detailed runtime behavior.)
- *
- * IV-5. Absorbability Analysis (RPR_ELEM_ABSORBABLE)
- *
- * Context absorption is an optimization technique that reduces O(n^2) to O(n).
- * (Runtime behavior is described in Chapter VIII.)
- *
- * This phase determines whether the pattern has a structure suitable for the
- * absorption optimization and sets flags on the relevant elements:
- *
- *   RPR_ELEM_ABSORBABLE         Absorption comparison point
- *   RPR_ELEM_ABSORBABLE_BRANCH  Element within an absorbable region
- *
- * Eligibility conditions:
- *
- *   (1) SKIP PAST LAST ROW (not NEXT ROW)
- *   (2) Frame end is UNBOUNDED FOLLOWING
- *
- * Structural conditions (isUnboundedStart + computeAbsorbabilityRecursive):
- *
- *   Case 1: Simple VAR+ (e.g., A+)
- *           -> ABSORBABLE | ABSORBABLE_BRANCH set on the VAR
- *   Case 2: GROUP+ with fixed-length children (min == max, recursively)
- *           e.g., (A B)+, (A B{2})+, ((A (B C){2}){2})+
- *           -> ABSORBABLE_BRANCH on all elements within the group,
- *              ABSORBABLE | ABSORBABLE_BRANCH on END
- *
- *           Why this is safe: when every child has min == max, the group
- *           is semantically equivalent to unrolling its body into {1,1}
- *           elements.  E.g., (A B{2})+ behaves like (A B B)+.  Each
- *           iteration consumes a fixed number of rows, so an earlier
- *           context's count always dominates a later one's (monotonicity).
- *
- *   Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
- *           -> Recurses from BEGIN into the body, applying Case 1.
- *             ABSORBABLE | ABSORBABLE_BRANCH set on A.
- *             B and END get no flags -> absorption stops once past A.
- *
- * Absorbability is determined per-element, not per-pattern.
- * Absorption comparison is performed only when a state resides at an
- * element with the RPR_ELEM_ABSORBABLE flag. Once a state leaves the
- * flagged region, absorption is permanently disabled for that state.
- *
- * Through this mechanism, the runtime guarantees monotonicity:
- * "a context that started earlier always subsumes a context that
- * started later."
- *
- * Chapter V  NFA Runtime Data Structures
- * ============================================================================
- *
- * V-1. RPRNFAState -- NFA State
- *
- * A single NFA state represents "how far the pattern has progressed."
- *
- *   Field         Description
- *   -----------------------------------------------------------
- *   elemIdx       Index of the current pattern element
- *   counts[]      Repetition count per group depth
- *   isAbsorbable  Whether the state is in an absorbable region
- *   next          Next state in the linked list
- *
- * The size of the counts array is rpPattern->maxDepth (= maximum group
- * nesting depth + 1), allocated as a flexible array member at the end of
- * the struct.
- *
- * Example: In PATTERN ((A B)+ C), a state waiting for B in the 3rd iteration
- *
- *   Element array: [0:BEGIN(d0) 1:A(d1) 2:B(d1) 3:END(d0) 4:C(d0) 5:FIN]
- *
- *   elemIdx = 2 (B, depth 1)
- *   counts[0] = 2 (depth 0: depth of END. Group completed 2 iterations)
- *   counts[1] = 1 (depth 1: depth of B. A matched in current iteration)
- *
- *   Counts are indexed by depth, not by elemIdx.
- *   counts[0] is incremented when passing through END(depth 0),
- *   and the group repetition count is preserved even when
- *   the state is at B(depth 1).
- *
- * Definition of two states being "equal":
- *
- *   Two states are equal if they have the same elemIdx and the same counts
- *   up to the depth of that element.
- *   nfa_states_equal() compares counts[0..elem->depth] using memcmp.
- *   Only counts at or below the depth of the current element are meaningful.
- *
- * V-2. RPRNFAContext -- Matching Context
- *
- * A single context represents "a matching attempt started from a specific
- * start row."
- *
- *   Field                 Description
- *   ---------------------------------------------------------------------
- *   states                Linked list of active NFA states
- *   matchStartRow         Row number where matching started
- *   matchEndRow           Row number where matching completed
- *                         (-1 if incomplete)
- *   lastProcessedRow      Last row processed
- *   matchedState          State that reached FIN (for greedy fallback)
- *   hasAbsorbableState    Whether this context can absorb other contexts
- *   allStatesAbsorbable   Whether this context can be absorbed
- *   next, prev            Doubly-linked list
- *
- * Since the NFA is nondeterministic, multiple states can coexist
- * simultaneously within a single context.
- *
- * Example: In PATTERN (A | B) C, if the first row matches both A and B,
- * two states coexist within the context:
- *
- *   State 1: elemIdx=3 (waiting for C, via branch A)
- *   State 2: elemIdx=3 (waiting for C, via branch B)
- *
- * In this case, since the (elemIdx, counts) of the two states are equal,
- * nfa_add_state_unique() retains only State 1 (branch A), which was
- * added first.
- * Because DFS processes the first branch of ALT first, the state via A
- * is registered first, and the state via B is discarded as a duplicate.
- * This is the preferment guarantee.
- *
- * V-3. RPR Fields of WindowAggState
- *
- *   nfaContext / nfaContextTail   Doubly-linked list of active contexts
- *   nfaContextFree                Reuse pool for contexts
- *   nfaStateFree                  Reuse pool for states
- *   nfaVarMatched                 Per-row cache: varMatched[varId]
- *   nfaVisitedElems               Bitmap for cycle detection
- *   nfaStateSize                  Precomputed size of RPRNFAState
- *
- * Memory management:
- *
- *   States and contexts are managed through their own free lists.
- *   Instead of palloc, they are obtained from the reuse pool, and
- *   returned to the pool upon deallocation.
- *   This reduces the overhead of frequent allocation/deallocation.
- *
- * Chapter VI  NFA Execution: 3-Phase Model
- * ============================================================================
- *
- * VI-1. Entry Point and Overall Flow
- *
- * When the window function processes each row, row_is_in_reduced_frame()
- * is called. This function determines whether the current row belongs to
- * a matched frame, and if necessary, calls update_reduced_frame() to
- * drive the NFA.
- *
- * Flow of update_reduced_frame():
- *
- *   (1) Find or create a context for the target row
- *   (2) Enter the row processing loop
- *   (3) After the loop ends, record the match result
- *
- * Pseudocode of the row processing loop:
- *
- *   targetCtx = ExecRPRGetHeadContext(pos)
- *   if targetCtx == NULL:
- *       targetCtx = ExecRPRStartContext(pos)
- *
- *   for currentPos = startPos; targetCtx->states != NULL; currentPos++:
- *       if not nfa_evaluate_row(currentPos):  -- row does not exist
- *           ExecRPRFinalizeAllContexts()      -- finalize all contexts
- *           ExecRPRCleanupDeadContexts()      -- clean up after finalization
- *           break
- *
- *       ExecRPRProcessRow(currentPos)         -- 3-phase processing
- *       ExecRPRStartContext(currentPos + 1)   -- pre-create next start point
- *       ExecRPRCleanupDeadContexts()          -- remove dead contexts
- *
- * Key point: Processing a single row may require processing multiple rows
- * ahead. Due to the nature of window functions, determining the frame for
- * row N requires looking at rows beyond N.
- *
- * VI-2. Context Creation: ExecRPRStartContext()
- *
- * Creates a new context and performs the initial advance.
- *
- *   (1) Allocate context via nfa_context_alloc()
- *   (2) Set matchStartRow = pos
- *   (3) Create initial state: elemIdx=0 (first pattern element),
- *       counts=all zero
- *   (4) Call nfa_advance(initialAdvance=true)
- *
- * The initial advance expands epsilon transitions at the beginning of
- * the pattern. For example, the initial advance for PATTERN ((A | B) C):
- *
- *   Start: elemIdx=0 (ALT)
- *     -> Expand ALT branches
- *       -> elemIdx=1 (A) -- VAR, so add state; stop here
- *       -> elemIdx=2 (B) -- VAR, so add state; stop here
- *
- *   Result: Two states in the context {waiting for A, waiting for B}
- *
- * During the initial advance, reaching FIN is not recorded as a match.
- * This is to prevent empty matches.
- *
- * VI-3. Row Evaluation: nfa_evaluate_row()
- *
- * Evaluates all variable conditions in the DEFINE clause at once for
- * the current row.
- *
- *   for each defineClause[i]:
- *       result = ExecEvalExpr(defineClause[i])
- *       varMatched[i] = (not null and true)
- *
- * To support row navigation operators (PREV, NEXT, FIRST, LAST),
- * a 1-slot model is used: only ecxt_outertuple is set to the current
- * row.  Navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
- * emitted during DEFINE expression compilation:
- *
- *   NAV_SET:     save ecxt_outertuple, swap in target row via nav_slot
- *   (evaluate):  argument expression reads from swapped slot
- *   NAV_RESTORE: restore original ecxt_outertuple
- *
- * Compound navigation (PREV(FIRST()), NEXT(FIRST()), PREV(LAST()),
- * NEXT(LAST())) is flattened by the parser into a single RPRNavExpr
- * with a compound kind (RPR_NAV_PREV_FIRST, etc.).  The executor
- * computes the target position in two steps: first the inner reference
- * point (match_start + N or currentpos - N) with match-range validation,
- * then the outer adjustment (+/- M) with partition-range validation.
- * If either step is out of range, the result is NULL.
- *
- * nav_slot caches the last fetched position (nav_slot_pos) to avoid
- * redundant tuplestore lookups when multiple navigation calls target
- * the same row.
- *
- * The varMatched array is referenced later in Phase 1 (Match).
- *
- * VI-4. Per-Context Re-evaluation (match_start-dependent variables)
- *
- * DEFINE variables that depend on match_start (those containing FIRST,
- * LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST)
- * are identified at plan time via defineMatchStartDependent.  The shared
- * evaluation in nfa_evaluate_row() uses the head context's matchStartRow
- * for FIRST/LAST base position.
- *
- * When processing a context whose matchStartRow differs from the shared
- * value, nfa_reevaluate_dependent_vars() temporarily sets nav_match_start
- * to that context's matchStartRow and re-evaluates only the dependent
- * variables.  The original nav_match_start and currentpos are saved and
- * restored after re-evaluation.
- *
- * Summary of evaluation strategy by navigation content:
- *
- *   Navigation content               evaluation
- *   -------------------------------------------------------
- *   No navigation                    shared (once per row)
- *   PREV/NEXT only                   shared (once per row)
- *   LAST (no offset)                 shared (once per row)
- *   LAST (with offset)               per-context
- *   FIRST (any)                      per-context
- *   Compound (inner FIRST)           per-context
- *   Compound (inner LAST, no off.)   shared (once per row)
- *   Compound (inner LAST, w/off.)    per-context
- *
- * VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
- *
- * Navigation functions require access to past rows via the tuplestore.
- * To allow tuplestore_trim() to free rows that are no longer reachable,
- * the planner computes two offsets (see compute_nav_offsets):
- *
- *   navMaxOffset (Nav Mark Lookback):
- *     Maximum backward reach from currentpos.  Contributed by PREV,
- *     LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
- *     Mark position: currentpos - navMaxOffset.
- *
- *   navFirstOffset (Nav Mark Lookahead):
- *     Minimum forward offset from match_start.  Contributed by FIRST
- *     and compound PREV_FIRST/NEXT_FIRST.  Can be negative when
- *     compound PREV_FIRST looks before match_start.
- *     Mark position: oldest_context->matchStartRow + navFirstOffset.
- *
- * The actual mark is set to: min(lookback_mark, lookahead_mark).
- * This ensures all rows reachable by any navigation function are retained.
- *
- * When offsets contain non-constant expressions (Param), the planner sets
- * navMaxOffsetKind/navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL and the
- * executor evaluates them at init time.  On overflow, the kind is set to
- * RPR_NAV_OFFSET_RETAIN_ALL, disabling trim for that dimension.
- *
- * VI-6. ExecRPRProcessRow(): 3-Phase Processing
- *
- * NFA processing for a single row is divided into three phases:
- *
- *   +--------------------------------------------+
- *   |  Phase 1: MATCH (convergence)              |
- *   |  Compare the current row against each VAR  |
- *   |  state. Remove states that fail to match.  |
- *   |                                            |
- *   |  Phase 2: ABSORB (absorption)              |
- *   |  Merge duplicate contexts to prevent       |
- *   |  state explosion.                          |
- *   |                                            |
- *   |  Phase 3: ADVANCE (expansion)              |
- *   |  Expand epsilon transitions to prepare     |
- *   |  for the next row.                         |
- *   +--------------------------------------------+
- *
- * This ordering is important:
- *
- *   - Match executes first to "consume the current row."
- *   - Absorb executes immediately after Match, when states have been updated.
- *   - Advance executes last to prepare "states waiting for the next row."
- *
- * Chapter VII  Phase 1: Match
- * ============================================================================
- *
- * nfa_match() iterates through each state in the context:
- *
- *   (1) Check whether the state's elemIdx is a VAR element
- *   (2) Compare against the current row using nfa_eval_var_match()
- *   (3) Match success: increment repetition count, retain state
- *   (4) Match failure: remove state
- *
- * Match determination (nfa_eval_var_match):
- *
- *   If varId is within the range of defineVariableList:
- *       Use the value of varMatched[varId]
- *
- *   If varId exceeds the range (variable not defined in DEFINE):
- *       Unconditionally true (matches all rows)
- *
- * Immediate advance for simple VARs:
- *
- *   For a VAR with min=1, max=1 where the next element is END,
- *   the Match phase processes through END immediately.
- *   This is necessary for accurate state comparison in Phase 2 (Absorb).
- *
- *   Example: In PATTERN ((A B)+), when A matches, it immediately advances
- *   to B, and when B matches, it immediately advances through END to
- *   complete the group count. This enables absorption comparison with
- *   other contexts.
- *
- * Chapter VIII  Phase 2: Absorb (Context Absorption)
- * ============================================================================
- *
- * VIII-1. Problem
- *
- * In the current implementation, a new context is started for each row
- * processed.
- * Applying PATTERN (A+) to 10 rows produces 10 contexts,
- * each of which tracks state independently.
- *
- * If there are N rows, the total number of states becomes O(N^2):
- *
- *   Context 1 (started at row 1): can match A up to N times
- *   Context 2 (started at row 2): can match A up to N-1 times
- *   ...
- *   Context N (started at row N): can match A 1 time
- *
- * VIII-2. Solution: Context Absorption
- *
- * Key observation: a context started earlier contains
- * all matches of a later-started context (monotonicity principle).
- *
- * If Context 1 started at row 1 and matched A 5 times,
- * the state where Context 2 (started at row 2) matched A 4 times
- * is already contained within Context 1.
- *
- * Therefore Context 2 can be "absorbed" into Context 1.
- *
- * VIII-3. Absorption Conditions
- *
- * Planner-time prerequisites (all must hold for absorption to be enabled):
- *
- *   (a) SKIP PAST LAST ROW.  SKIP TO NEXT ROW creates overlapping
- *       contexts that cannot be safely absorbed.
- *   (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
- *       FOLLOWING).  Limited frames apply differently to each context,
- *       breaking the monotonicity principle.
- *   (c) No match_start-dependent navigation in DEFINE.
- *
- *       Mechanism: each context has a different matchStartRow, so FIRST
- *       resolves to a different row for each context at the same
- *       currentpos.  An earlier context's DEFINE result no longer
- *       subsumes a later one's, making count-dominance comparison
- *       invalid.  Rather than comparing matchStartRow at runtime
- *       (which would complicate the absorb path), any match_start
- *       dependency disables absorption entirely.
- *
- *       Navigation content              match_start dep.  absorption
- *       ------------------------------------------------------------
- *       No navigation                   none              safe
- *       PREV/NEXT only                  none              safe
- *       LAST (no offset)                none              safe
- *       LAST (with offset)              boundary check    unsafe
- *       FIRST (any)                     direct            unsafe
- *       Compound (inner FIRST)          direct            unsafe
- *       Compound (inner LAST, no off.)  none              safe
- *       Compound (inner LAST, w/off.)   boundary chk      unsafe
- *
- * Runtime conditions (evaluated per context pair):
- *
- *   (1) The pattern is marked as isAbsorbable (see IV-5)
- *   (2) allStatesAbsorbable of the target context is true
- *   (3) An earlier context "covers" all states of the target
- *
- * Cover condition (nfa_states_covered):
- *
- *   A state with the same elemIdx exists in the earlier context,
- *   and the count at that depth is greater than or equal -- then it is covered.
- *
- * VIII-4. Dual-Flag Design
- *
- * Two boolean flags make the absorption decision efficient:
- *
- *   hasAbsorbableState (monotonic: only true->false transition possible)
- *     "Does this context have the ability to absorb other contexts?"
- *     true if at least one absorbable state exists.
- *     Transitions to false when states are removed leaving no absorbable
- *     states.
- *     Once false, it never becomes true again.
- *
- *   allStatesAbsorbable (dynamic: can fluctuate)
- *     "Can this context be absorbed?"
- *     true if all states are in an absorbable region.
- *     Becomes false when a non-absorbable state is added; reverts to true
- *     when it is removed.
- *
- * VIII-5. Absorption Order
- *
- * nfa_absorb_contexts() traverses from tail (newest) to head (oldest).
- *
- *   for ctx = tail to head:
- *       if ctx.allStatesAbsorbable:
- *           for older = ctx.prev to head:
- *               if older.hasAbsorbableState:
- *                   if nfa_states_covered(older, ctx):
- *                       free(ctx)  -- absorbed
- *                       break
- *
- * Since inspection starts from the newest context, the most recently started
- * (= having the shortest match) context is absorbed first.
- *
- * Chapter IX  Phase 3: Advance (Epsilon Transition Expansion)
- * ============================================================================
- *
- * IX-1. Overview
- *
- * nfa_advance() expands epsilon transitions from each state after Match,
- * generating "new states waiting for the next row."
- *
- * An epsilon transition is a transition that moves without consuming a row:
- *
- *   - ALT: branch to each alternative
- *   - BEGIN: enter group (or skip if min=0)
- *   - END: loop-back within group (or exit when condition is met)
- *   - FIN: record match completion
- *   - VAR loop/exit: repeat/exit according to the quantifier
- *
- * Expansion stops upon reaching a VAR element, and the state is added.
- * This is because VAR is the element that "will consume the next row."
- *
- * IX-2. Processing Order: DFS and Preferment
- *
- * advance processes states in lexicographic order,
- * performing Depth-First Search (DFS) on each state.
- *
- * This DFS order is what guarantees the SQL standard's "preferment":
- *
- *   The branch that appears first in the PATTERN text takes precedence.
- *
- * Example: PATTERN (A | B) C
- *
- *   The first branch A of the ALT takes precedence over the second branch B.
- *   When both A and B can match, the match via A is selected.
- *
- * nfa_add_state_unique() prevents duplicate addition of the same state,
- * so the state added first (= from the preferred branch) is retained.
- *
- * IX-3. Routing Function: nfa_route_to_elem()
- *
- * All inter-element transitions in the advance phase go through
- * nfa_route_to_elem().
- * This function branches its behavior based on the type of the next element:
- *
- *   If the next element is VAR:
- *     (1) Add the state to the context (nfa_add_state_unique)
- *     (2) If the VAR has min=0, also add a skip path (recurse via next)
- *     -> Expansion stops here (VAR is the element that "will consume the next
- *        row")
- *
- *   If the next element is non-VAR (ALT, BEGIN, END, FIN):
- *     -> Recursively call nfa_advance_state() to continue expansion
- *
- * With this structure, advance recursively follows epsilon transitions
- * until reaching a VAR, consistently stopping only at VAR elements.
- *
- * IX-4. Per-Element advance Behavior
- *
- * (a) ALT (nfa_advance_alt)
- *
- *   Upon encountering an ALT element, all branches are expanded in order.
- *   The first element of each branch is connected via a jump pointer.
- *
- *   idx=0 (ALT) -> branch 1 start (next) -> branch 2 start (jump) -> ...
- *
- *   nfa_advance_state() is recursively called for each branch.
- *
- * (b) BEGIN (nfa_advance_begin)
- *
- *   Handles group entry.
- *   jump points to the element after END (= first element outside the group).
- *
- *   Greedy (default):
- *     (1) Enter the group body (move via next, reset the count at that depth)
- *     (2) If min=0, also add a group skip path (move via jump)
- *
- *   Reluctant:
- *     Order reversed -- skip path first, group entry second.
- *     If the skip path reaches FIN, the group entry path is not generated
- *     (shortest match preferred).
- *
- * (c) END (nfa_advance_end)
- *
- *   Handles group termination. This is the core of the repetition logic.
- *
- *   Let count be the count at the current depth:
- *
- *   count < min:
- *     Loop-back (move via jump, repeat the group body)
- *
- *     If the RPR_ELEM_EMPTY_LOOP flag is set:
- *       In addition to loop-back, also add a fast-forward exit path.
- *       This is because the body may produce an empty match, causing count
- *       to never reach min. fast-forward resets counts[depth] to 0
- *       and exits via next (treating the remaining required iterations
- *       as empty matches).
- *
- *   min <= count < max:
- *     Greedy: loop-back first, exit second
- *     Reluctant: exit first, loop-back second
- *                If the exit path reaches FIN, loop-back is omitted.
- *
- *   count >= max:
- *     Unconditional exit (move via next)
- *
- *   On exit: reset counts[depth] = 0, and if the next element is an outer END,
- *   increment the count at the outer depth.
- *
- * (d) VAR (nfa_advance_var)
- *
- *   Handles repeat/exit for a VAR element with a quantifier.
- *
- *   Let count be the count at the current depth:
- *
- *   count < min:
- *     Unconditional loop (stay at the same elemIdx, wait for the next row)
- *
- *   min <= count < max:
- *     Greedy: loop first, exit (next) second
- *     Reluctant: exit first, loop second
- *                If the exit path reaches FIN, loop is omitted.
- *
- *   count >= max:
- *     Unconditional exit (move via next)
- *
- *   On exit: reset counts[depth] = 0.
- *
- * (e) FIN
- *
- *   Match success. The current state is moved to matchedState for recording,
- *   and matchEndRow is set to the current row.
- *
- *   Upon reaching FIN, all remaining unprocessed states are removed
- *   (early termination). By DFS order, the path that reached FIN first
- *   has the highest preferment, so the rest are inferior paths.
- *   This is the core mechanism that guarantees preferment.
- *
- *   In SKIP PAST LAST ROW mode, upon reaching FIN, subsequent contexts
- *   that started within the match range are immediately pruned.
- *
- * IX-5. State Deduplication: nfa_add_state_unique()
- *
- * When adding a new state to a context, it is compared against existing
- * states;
- * if an identical state already exists, it is not added.
- *
- * Comparison criteria: elemIdx + counts[0..elem->depth] (see V-1)
- *
- * This deduplication is the core mechanism that suppresses NFA state
- * explosion.
- * Because DFS order causes preferred-branch states to be added first,
- * identical states from lower-priority branches are automatically discarded.
- *
- * IX-6. Cycle Detection: nfaVisitedElems
- *
- * When a group body can produce an empty match,
- * looping back from END may cause an infinite loop.
- *
- * Example: PATTERN ((A?)*)
- *
- *   A? has min=0, so it can pass through without matching.
- *   If the outer group repeats: BEGIN -> A? skip -> END -> BEGIN -> ...
- *
- * To prevent this:
- *
- *   (1) At compile time: set the RPR_ELEM_EMPTY_LOOP flag on the END
- *       of groups whose body is nullable.
- *       The runtime effect of this flag is described in IX-4(c):
- *       when count < min, a fast-forward exit path is added,
- *       resolving the deadlock where count cannot increase due to empty
- *       matches.
- *
- *   (2) At runtime: initialize the nfaVisitedElems bitmap immediately before
- *       DFS expansion of each state within advance (once per state).
- *       During DFS, epsilon elements (END, ALT, BEGIN) are marked in the
- *       bitmap at nfa_advance_state entry.  VAR elements are marked later
- *       when added to the state list (nfa_add_state_unique), so that
- *       legitimate loop-back to the same VAR in a new group iteration
- *       (e.g., END -> ALT -> same VAR) is not blocked.
- *       If a previously visited elemIdx is revisited, that path is terminated.
- *
- *   Note: the bitmap tracks only elemIdx and does not consider counts.
- *   Therefore, legitimate revisits to the same elemIdx but with different
- *   counts may also be blocked.  This only occurs when the group body is
- *   nullable (all paths can match empty), causing END -> loop-back ->
- *   skip -> END within a single DFS.  In such cases the END element has
- *   the RPR_ELEM_EMPTY_LOOP flag, so the fast-forward exit (IX-4(c))
- *   provides an alternative path that bypasses the cycle.
- *
- * Chapter X  Match Result Processing
- * ============================================================================
- *
- * X-1. Match Result
- *
- * RPR tracks the current match result as a single entry in WindowAggState
- * with four fields: rpr_match_valid, rpr_match_matched, rpr_match_start,
- * and rpr_match_length.  When rpr_match_valid is true, the entry describes
- * the match result for the position at rpr_match_start: rpr_match_matched
- * indicates success or failure, and rpr_match_length gives the number of
- * rows consumed.  A match with rpr_match_length 0 represents an empty match
- * (pattern matched but consumed no rows).  When rpr_match_valid is false,
- * the position has not been evaluated yet (RF_NOT_DETERMINED).
- *
- * A row's status against the current match result can be obtained by
- * calling get_reduced_frame_status().
- *
- * X-2. AFTER MATCH SKIP
- *
- * Determines the starting point for the next match attempt after a successful
- * match:
- *
- *   SKIP TO NEXT ROW:
- *     New match attempt begins from the row after the match start row.
- *     Overlapping matches are possible.
- *
- *   SKIP PAST LAST ROW:
- *     New match attempt begins from the row after the match end row.
- *     Only non-overlapping matches are possible.
- *
- * X-3. INITIAL vs SEEK
- *
- *   Standard definition (section 6.12):
- *   INITIAL: "is used to look for a match whose first row is R."
- *   SEEK:    "is used to permit a search for the first match anywhere
- *            from R through the end of the full window frame."
- *   In either case, if there is no match, the reduced window frame is empty.
- *   The default is INITIAL.
- *
- *   Current implementation:
- *   SEEK is not supported (the parser raises an error).
- *   Only INITIAL is supported, searching only for matches starting at each
- *   row position pos.
- *
- * X-4. Bounded Frame Handling
- *
- *   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
- *   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
- *   frameOffset indicating the upper bound.  Before the match phase,
- *   any context whose match has exceeded the frame boundary
- *   (currentPos >= matchStartRow + frameOffset + 1) is finalized early
- *   by forcing a mismatch.  This prevents matches from extending beyond
- *   the window frame.  The sum is clamped to PG_INT64_MAX on overflow.
- *
- *   Note that bounded frames also disable context absorption at the
- *   planner level (see VIII-3(b)), since the frame boundary breaks the
- *   monotonicity assumption required for correct absorption.
- *
- * Chapter XI  Worked Example: Full Execution Trace
- * ============================================================================
- *
- * XI-1. Query
- *
- *   SELECT company, tdate, price,
- *          first_value(price) OVER w AS start_price,
- *          last_value(price) OVER w AS end_price
- *   FROM stock
- *   WINDOW w AS (
- *     PARTITION BY company
- *     ORDER BY tdate
- *     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
- *     AFTER MATCH SKIP PAST LAST ROW
- *     PATTERN (A+ B)
- *     DEFINE A AS price > PREV(price),
- *            B AS price < PREV(price)
- *   );
- *
- * XI-2. Data
- *
- *   Row#    tdate       price
- *   --------------------------
- *   0       2024-01-01  100
- *   1       2024-01-02  110
- *   2       2024-01-03  120
- *   3       2024-01-04  115
- *   4       2024-01-05  130
- *
- * XI-3. Compilation Result
- *
- *   PATTERN (A+ B) -> unchanged after optimization
- *
- *   idx  varId  depth  min  max  next  jump
- *   -----------------------------------------
- *    0   A(0)   0      1    INF  1     -1     A+
- *    1   B(1)   0      1    1    2     -1     B
- *    2   FIN    0      1    1    -1    -1
- *
- *   DEFINE: A -> "price > PREV(price)", B -> "price < PREV(price)"
- *   isAbsorbable = true (A+ is a simple unbounded VAR)
- *
- * XI-4. Execution Trace
- *
- * --- Row 0 (price=100) ---
- *
- *   update_reduced_frame(0) called.
- *
- *   Context C0 created (matchStartRow=0).
- *   Initial advance: elemIdx=0(A) -> VAR, so state is added.
- *   C0.states = [{elemIdx=0, counts=[0]}]
- *
- *   nfa_evaluate_row(0):
- *     A: price(100) > PREV(price) -> no PREV -> false
- *     B: price(100) < PREV(price) -> no PREV -> false
- *     varMatched = [false, false]
- *
- *   ExecRPRProcessRow(0):
- *     Phase 1 (Match): A(0) state vs varMatched[0]=false -> state removed
- *     C0.states = [] (empty)
- *
- *     Phase 2 (Absorb): skipped (no states)
- *     Phase 3 (Advance): skipped (no states)
- *
- *   C0.states is empty, so the loop terminates.
- *   matchEndRow < matchStartRow -> unmatched.
- *
- * --- Row 1 (price=110) ---
- *
- *   update_reduced_frame(1) called.
- *
- *   Context C1 created (matchStartRow=1).
- *   Initial advance: C1.states = [{elemIdx=0, counts=[0]}]
- *
- *   nfa_evaluate_row(1):
- *     A: 110 > PREV(100) -> true
- *     B: 110 < PREV(100) -> false
- *     varMatched = [true, false]
- *
- *   ExecRPRProcessRow(1):
- *     Phase 1 (Match): A(0) match succeeds -> counts[0]++ -> counts=[1]
- *     C1.states = [{elemIdx=0, counts=[1]}]
- *
- *     Phase 3 (Advance):
- *       State {elemIdx=0, counts=[1]}: A+ (min=1, count=1, max=INF)
- *         count >= min, so:
- *         Greedy -> loop first: keep {elemIdx=0, counts=[1]}
- *                   exit: reset counts[0]=0, next(=1) -> {elemIdx=1,
- *                         counts=[0]}
- *     C1.states = [{elemIdx=0, counts=[1]}, {elemIdx=1, counts=[0]}]
- *
- * --- Row 2 (price=120) ---
- *
- *   Context C2 created (matchStartRow=2).
- *   Initial advance: C2.states = [{elemIdx=0, counts=[0]}]
- *
- *   nfa_evaluate_row(2):
- *     A: 120 > PREV(110) -> true
- *     B: 120 < PREV(110) -> false
- *     varMatched = [true, false]
- *
- *   C1 ExecRPRProcessRow(2):
- *     Phase 1 (Match):
- *       {elemIdx=0, counts=[1]}: A matches -> counts=[2]
- *       {elemIdx=1, counts=[0]}: B does not match -> removed
- *     C1.states = [{elemIdx=0, counts=[2]}]
- *
- *   C2 ExecRPRProcessRow(2):
- *     Phase 1 (Match):
- *       {elemIdx=0, counts=[0]}: A matches -> counts=[1]
- *     C2.states = [{elemIdx=0, counts=[1]}]
- *
- *     Phase 2 (Absorb):
- *       Does C1 (started earlier) cover C2?
- *         C1: {elemIdx=0, counts=[2]}, C2: {elemIdx=0, counts=[1]}
- *         Same elemIdx, C1.counts >= C2.counts -> covered
- *       C2 absorbed. -> removed.
- *
- *     Phase 3 (Advance):
- *       {elemIdx=0, counts=[2]}: Greedy -> loop + exit
- *         Loop: {elemIdx=0, counts=[2]}
- *         Exit: reset counts[0]=0, next(=1) -> {elemIdx=1, counts=[0]}
- *     C1.states = [{elemIdx=0, counts=[2]}, {elemIdx=1, counts=[0]}]
- *
- *   Context C3 created (matchStartRow=3).
- *
- * --- Row 3 (price=115) ---
- *
- *   nfa_evaluate_row(3):
- *     A: 115 > PREV(120) -> false
- *     B: 115 < PREV(120) -> true
- *     varMatched = [false, true]
- *
- *   ExecRPRProcessRow(3):
- *     Phase 1 (Match):
- *       {elemIdx=0, counts=[2]}: A does not match -> removed
- *       {elemIdx=1, counts=[0]}: B matches -> counts=[1]
- *     C1.states = [{elemIdx=1, counts=[1]}]
- *
- *     Phase 3 (Advance):
- *       {elemIdx=1, counts=[1]}: B (min=1, max=1)
- *         count(1) >= max(1) -> unconditional exit
- *         Reset counts[0]=0, next = 2 (FIN)
- *       FIN reached -> matchEndRow = 3, matchedState recorded.
- *       Early termination: no remaining states, so completed immediately.
- *     C1.states = [] (empty after reaching FIN)
- *
- *   C1.states is empty and matchEndRow=3 >= matchStartRow=1 -> match succeeds.
- *
- *   rpr_match_start = 1, rpr_match_length = 3
- *
- * --- Row 4 (price=130) ---
- *
- *   update_reduced_frame(4) called.
- *   C3 was already created but matchStartRow=3, so it is not applicable.
- *   New context C4 created (matchStartRow=4).
- *
- *   nfa_evaluate_row(4):
- *     A: 130 > PREV(115) -> true
- *     B: 130 < PREV(115) -> false
- *
- *   ... No subsequent rows, so ExecRPRFinalizeAllContexts() is called.
- *   Match incomplete -> unmatched.
- *
- * XI-5. Final Result
- *
- *   Row 0: unmatched     -> frame = the row itself
- *   Row 1: match head    -> frame = rows 1 through 3
- *   Row 2: inside match  -> skipped
- *   Row 3: inside match  -> skipped
- *   Row 4: unmatched     -> frame = the row itself
- *
- * Chapter XII  Summary of Key Design Decisions
- * ============================================================================
- *
- * XII-1. Flat Array vs Tree-Based NFA
- *
- *   Choice: Flat array (RPRPatternElement[])
- *
- *   Rationale:
- *   - Cache-friendly: 16-byte fixed size, contiguous memory
- *   - Index-based references: 2-byte indices instead of pointers
- *   - Easy to serialize: can use memcpy when passing to plan nodes
- *
- * XII-2. Forward-only Execution vs Backtracking
- *
- *   Choice: Forward-only (state set tracking)
- *
- *   Rationale:
- *   - Backtracking takes exponential time in the worst case
- *   - NFA simulation guarantees polynomial time
- *   - DFS order naturally guarantees preferment.
- *     Greedy/reluctant per quantifier requires only reversing the DFS order
- *   - Window functions receive sorted rows sequentially.
- *     Forward-only fits directly into this pipeline,
- *     whereas backtracking requires re-fetching previous rows
- *   - DEFINE conditions are SQL expressions (PREV, RUNNING aggregates, etc.)
- *     with high re-evaluation cost. Forward-only requires only one evaluation
- *     per row
- *
- * XII-3. Per-Context Management
- *
- *   Choice: Independent context per start row
- *
- *   Rationale:
- *   - Supports overlapping matches under SKIP TO NEXT ROW
- *   - Determines the frame for each row independently
- *   - Absorption optimization can eliminate redundant contexts in O(n)
- *
- * XII-4. Memory Pool Management
- *
- *   Choice: Custom free list
- *
- *   Rationale:
- *   - NFA states are created and destroyed in large numbers per row
- *   - Avoids palloc/pfree overhead
- *   - State size is variable (counts[] array), but within a single query
- *     maxDepth is fixed, so all states have the same size
- *
- * XII-5. Execution Optimization Summary
- *
- *   The following optimizations make the NFA simulation practical.
- *
- *   -- Compile-time --
- *
- *   (1) AST Optimization (IV-3)
- *
- *     Simplifies the AST 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.
- *
- *     Significance: Reducing the element count directly shrinks the state
- *     space, decreasing the cost of all subsequent runtime phases (match,
- *     absorb, advance).
- *
- *   -- Runtime: advance phase --
- *
- *   (2) Group Skip (IX-4(b))
- *
- *     At the BEGIN of a group with min=0, uses jump to skip the entire
- *     group. Moves directly to the first element outside the group without
- *     exploring the group body. Greedy enters then skips; Reluctant skips
- *     then enters.
- *
- *     Significance: For optional groups (min=0), immediately generates
- *     a skip path without exploring the body, avoiding unnecessary DFS
- *     expansion.
- *
- *   (3) State Deduplication (IX-5)
- *
- *     During advance, DFS may generate states with the same (elemIdx,
- *     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).
- *     This process can also produce duplicate states reaching the same END.
- *     nfa_add_state_unique() blocks duplicate addition of identical states
- *     in both cases.
- *
- *     Significance: Prevents exponential growth of the state count in
- *     ALT branches and quantifier expansion. Since DFS order causes the
- *     preferred branch's state to be registered first, identical states
- *     from lower-priority branches are automatically discarded, thereby
- *     also guaranteeing preferment.
- *
- *   (4) Cycle Detection and Fast-Forward (IX-6, IX-4(c))
- *
- *     When a nullable group body (e.g., A?) repeats empty matches,
- *     the END -> BEGIN loop-back can continue indefinitely.
- *
- *     Two mechanisms resolve this:
- *     - A visited bitmap (nfaVisitedElems) blocks revisitation of the
- *       same element, preventing infinite loops (safety)
- *     - At an END with the RPR_ELEM_EMPTY_LOOP flag set, when
- *       count < min, the remaining required iterations are treated as
- *       empty matches and a fast-forward exit path out of the group is
- *       added (correctness)
- *
- *     Significance: Cycle detection guarantees termination, and
- *     fast-forward guarantees that the min condition is satisfied.
- *     Without these, patterns containing nullable groups would fall
- *     into infinite loops or fail to match.
- *
- *   (5) Match Pruning (IX-4(e))
- *
- *     When a state reaches FIN during advance, all remaining unprocessed
- *     states of that context are removed. Because of DFS order, the path
- *     that reaches FIN first has the highest preferment, so the remaining
- *     paths are inferior.
- *
- *     Significance: Once the best match is determined, exploration of
- *     inferior paths is immediately terminated. This mechanism achieves
- *     both preferment guarantees and performance optimization.
- *
- *   -- Runtime: inter-context --
- *
- *   (6) Early Termination (SKIP PAST LAST ROW)
- *
- *     In SKIP PAST LAST ROW mode, when a match is found, subsequent
- *     contexts whose start rows fall within the match range are pruned
- *     immediately without further processing.
- *     In SKIP TO NEXT ROW mode, overlapping contexts are preserved
- *     because each row requires its own independent match.
- *
- *     Significance: Prunes subsequent contexts whose start rows overlap
- *     with a prior match range, avoiding unnecessary processing.
- *
- *   (7) Context Absorption (Chapter VIII)
- *
- *     If an independent context is created for each row, O(n^2) states
- *     accumulate. By exploiting the monotonicity that an earlier-started
- *     context subsumes the states of a later-started context, redundant
- *     contexts are eliminated early.
- *
- *     Absorbability is determined per-element; comparison is performed
- *     only at elements with the RPR_ELEM_ABSORBABLE flag (see IV-5).
- *
- *     Significance: Keeps the number of active contexts at a constant
- *     level, achieving O(n^2) -> O(n) time complexity. Without this,
- *     performance degrades sharply on long partitions.
- *
- * Appendix A. Key Function Index
- * ============================================================================
- *
- *   Function                      File                  Role
- *   --------------------------------------------------------------------------
- *   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
- *   finalizeRPRPattern            rpr.c                 Finalization
- *   computeAbsorbability          rpr.c                 Absorption analysis
- *   update_reduced_frame          nodeWindowAgg.c       Execution main loop
- *   nfa_evaluate_row              nodeWindowAgg.c       DEFINE evaluation
- *   ExecRPRStartContext           execRPR.c             Context creation
- *   ExecRPRProcessRow             execRPR.c             3-phase processing
- *   nfa_match                     execRPR.c             Phase 1
- *   nfa_absorb_contexts           execRPR.c             Phase 2
- *   nfa_advance                   execRPR.c             Phase 3
- *   nfa_advance_state             execRPR.c             Per-state branching
- *   nfa_route_to_elem             execRPR.c             Element routing
- *   nfa_advance_alt               execRPR.c             ALT handling
- *   nfa_advance_begin             execRPR.c             BEGIN handling
- *   nfa_advance_end               execRPR.c             END handling
- *   nfa_advance_var               execRPR.c             VAR handling
- *   nfa_add_state_unique          execRPR.c             Deduplication
- *   nfa_states_covered            execRPR.c             Absorption check
- *   nfa_reevaluate_dependent_vars execRPR.c             Per-context re-eval
- *   ExecRPRGetHeadContext         execRPR.c             Context lookup
- *   ExecRPRFreeContext            execRPR.c             Context deallocation
- *   ExecRPRCleanupDeadContexts    execRPR.c             Dead context cleanup
- *   ExecRPRFinalizeAllContexts    execRPR.c             Partition-end finalize
- *   ExecRPRRecordContextSuccess   execRPR.c             Stats: match success
- *   ExecRPRRecordContextFailure   execRPR.c             Stats: match failure
- *   compute_nav_offsets           createplan.c          Trim offset computation
- *
- * Appendix B. Data Structure Relationship Diagram
- * ============================================================================
- *
- *   Parser Layer
- *   --------
- *   RPCommonSyntax
- *     |--- rpSkipTo: RPSkipTo
- *     |--- initial: bool
- *     +--- rpPattern: RPRPatternNode* (tree)
- *          |--- nodeType: VAR | SEQ | ALT | GROUP
- *          |--- min, max: quantifier
- *          |--- varName: variable name (VAR only)
- *          +--- children: List* (SEQ/ALT/GROUP only)
- *
- *   Planner Layer
- *   ----------
- *   WindowAgg (plan node)
- *     |--- rpSkipTo: RPSkipTo
- *     |--- defineClause: List<TargetEntry>
- *     +--- rpPattern: RPRPattern*
- *          |--- numVars: int
- *          |--- varNames: char**
- *          |--- maxDepth: RPRDepth
- *          |--- isAbsorbable: bool
- *          |--- numElements: int
- *          +--- elements: RPRPatternElement[]  (flat array)
- *               |--- varId      (1B)
- *               |--- depth      (1B)
- *               |--- flags      (1B)
- *               |--- reserved   (1B)
- *               |--- min, max   (4B + 4B)
- *               +--- next, jump (2B + 2B)
- *
- *   Executor Layer
- *   ----------
- *   WindowAggState
- *     |--- rpSkipTo: RPSkipTo (AFTER MATCH SKIP mode)
- *     |--- rpPattern: RPRPattern* (copied from plan)
- *     |--- defineVariableList: List<String> (variable names, DEFINE order)
- *     |--- defineClauseList: List<ExprState>
- *     |--- nfaVarMatched: bool[] (per-row cache)
- *     |--- nfaVisitedElems: bitmapword* (cycle detection)
- *     |--- nfaStateSize: Size (pre-calculated RPRNFAState allocation size)
- *     |--- nfaContext <-> nfaContextTail (doubly-linked list)
- *     |   +--- RPRNFAContext
- *     |       |--- states: RPRNFAState* (linked list)
- *     |       |   |--- elemIdx
- *     |       |   |--- counts[]
- *     |       |   +--- isAbsorbable
- *     |       |--- matchStartRow, matchEndRow
- *     |       |--- lastProcessedRow
- *     |       |--- matchedState (cloned on FIN arrival)
- *     |       |--- hasAbsorbableState
- *     |       +--- allStatesAbsorbable
- *     |--- nfaContextFree (recycling pool)
- *     +--- nfaStateFree (recycling pool)
- *
- * Appendix C. NFA Element Array Examples
- * ============================================================================
- *
- * C-1. PATTERN (A B C)
- *
- *   idx  varId  depth  min  max  next  jump
- *   ------------------------------------------
- *    0   A      0      1    1    1     -1
- *    1   B      0      1    1    2     -1
- *    2   C      0      1    1    3     -1
- *    3   FIN    0      1    1    -1    -1
- *
- * C-2. PATTERN (A+ B*)
- *
- *   idx  varId  depth  min  max  next  jump  flags
- *   ------------------------------------------------------------------------
- *    0   A      0      1    INF  1     -1    ABSORBABLE | ABSORBABLE_BRANCH
- *    1   B      0      0    INF  2     -1
- *    2   FIN    0      1    1    -1    -1
- *
- *   Only A+ is the absorption point (Case 1). Once past A,
- *   absorption is permanently disabled for that state.
- *
- * C-3. PATTERN (A | B | C)
- *
- *   idx  varId  depth  min  max  next  jump
- *   ----------------------------------------
- *    0   ALT    0      1    1    1     -1    alternation start
- *    1   A      1      1    1    4     2     branch 1 -> FIN, jump -> branch 2
- *    2   B      1      1    1    4     3     branch 2 -> FIN, jump -> branch 3
- *    3   C      1      1    1    4     -1    branch 3 -> FIN
- *    4   FIN    0      1    1    -1    -1
- *
- * C-4. PATTERN ((A B)+ C)
- *
- *   idx  varId    depth  min  max  next  jump  flags
- *   --------------------------------------------------------------------------
- *    0   BEGIN    0      1    INF  1     4     ABSORBABLE_BRANCH
- *    1   A        1      1    1    2     -1    ABSORBABLE_BRANCH
- *    2   B        1      1    1    3     -1    ABSORBABLE_BRANCH
- *    3   END      0      1    INF  4     1     ABSORBABLE | ABSORBABLE_BRANCH
- *    4   C        0      1    1    5     -1
- *    5   FIN      0      1    1    -1    -1
- *
- *   Case 2: GROUP+ with {1,1} body VARs. A, B are branches;
- *   END is the absorption point. Compare with C-6 (Case 3).
- *
- * C-5. PATTERN ((A | B)+? C)
- *
- *   idx  varId    depth  min  max   next  jump  flags
- *   -------------------------------------------------------------------
- *    0   BEGIN    0      1    INF   1     5     RELUCTANT, group start
- *    1   ALT      1      1    1     2     -1    alternation start
- *    2   A        2      1    1     4     3     branch 1
- *    3   B        2      1    1     4     -1    branch 2
- *    4   END      0      1    INF   5     1     RELUCTANT, group end
- *    5   C        0      1    1     6     -1
- *    6   FIN      0      1    1     -1    -1
- *
- * C-6. PATTERN ((A+ B)+ C)  -- Absorbability flag example
- *
- *   idx  varId    depth  min  max   next  jump  flags
- *   ---------------------------------------------------------------------------
- *    0   BEGIN    0      1    INF   1     4     ABSORBABLE_BRANCH, group start
- *    1   A        1      1    INF   2     -1    ABSORBABLE | ABSORBABLE_BRANCH
- *    2   B        1      1    1     3     -1
- *    3   END      0      1    INF   4     1     group end
- *    4   C        0      1    1     5     -1
- *    5   FIN      0      1    1     -1    -1
- *
- *   Recurses from BEGIN into the body -> A matches Case 1 (simple VAR+).
- *   A gets ABSORBABLE | ABSORBABLE_BRANCH, BEGIN gets ABSORBABLE_BRANCH.
- *   B and END get no flags -> absorption stops once the state advances to B.
- *   (See IV-5 Case 3)
- *
- * C-7. PATTERN ((A+ B | C*)+ D)  -- Per-branch absorption in ALT
- *
- *   idx  varId    depth  min  max   next  jump  flags
- *   ---------------------------------------------------------------------------
- *    0   BEGIN    0      1    INF   1     6     ABSORBABLE_BRANCH
- *    1   ALT      1      1    1     2     -1    ABSORBABLE_BRANCH
- *    2   A        2      1    INF   3     4     ABSORBABLE | ABSORBABLE_BRANCH
- *    3   B        2      1    1     5     -1
- *    4   C        2      0    INF   5     -1    ABSORBABLE | ABSORBABLE_BRANCH
- *    5   END      0      1    INF   6     1     EMPTY_LOOP
- *    6   D        0      1    1     7     -1
- *    7   FIN      0      1    1     -1    -1
- *
- *   ALT branches are checked independently for absorbability.
- *   Branch 1: A+ matches Case 1 -> A gets ABSORBABLE. B has no flag.
- *   Branch 2: C* matches Case 1 -> C gets ABSORBABLE.
- *   Both A and C get ABSORBABLE_BRANCH as part of their respective branch
- *   paths.
- *   END has EMPTY_LOOP: branch 2 (C*) is nullable, making the group body
- *   nullable.
- *   BEGIN and ALT get ABSORBABLE_BRANCH (on the path to absorbable elements).
- *
- * ============================================================================
- *   End of document
- * ============================================================================
+ * For the design and execution model of the NFA engine implemented
+ * in this file, see src/backend/executor/README.rpr.
  */
 
 /* Bitmap macros for NFA cycle detection (cf. bitmapset.c, tidbitmap.c) */
-- 
2.50.1 (Apple Git-155)

From 441039aba513d6ff0f4de79c8e3a8765f0fbaa73 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 01:17:16 +0900
Subject: [PATCH 39/40] Apply pgindent canonicalization to RPR comment blocks

Two RPR source files carried comment blocks that pre-dated their
last pgindent pass: execRPR.c (in nfa_match and nfa_advance_end)
and rpr.c (in tryMultiplyQuantifiers).  Run pgindent to bring the
prose paragraphs into line with the project-wide 78-column style.

For two of those blocks the structure is meaningful (numbered list
in nfa_advance_end, "Case 2 / Case 3 / Unsafe example" labels in
tryMultiplyQuantifiers), so the labeled items are wrapped in
/*---------- ... *---------- ... */ to disable pgindent reflow on
the hanging indents while leaving plain prose paragraphs inside
the same block at the canonical 78-column wrap.

These adjustments were deferred from earlier delta patches in this
series (Use Max()/Min() macros, Replace non-ASCII characters) so
those patches could remain focused on their stated logical changes.
With the design notes now extracted into README.rpr, this is a
natural moment to clear the remaining formatting debt.

Whitespace-only change; no functional or test impact.
---
 src/backend/executor/execRPR.c   | 30 +++++++++++++++---------------
 src/backend/optimizer/plan/rpr.c |  9 +++++----
 2 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 96cfbfbc0a2..242ae9c6dcf 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -810,10 +810,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 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).
 				 */
 				if (RPRElemIsAbsorbableBranch(elem) &&
 					!RPRElemIsAbsorbable(elem) &&
@@ -1035,9 +1035,9 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		RPRPatternElement *jumpElem;
 		RPRNFAState *ffState = NULL;
 
-		/*
-		 * Two paths are explored in parallel when the group body is
-		 * nullable (RPR_ELEM_EMPTY_LOOP):
+		/*----------
+		 * Two paths are explored in parallel when the group body is nullable
+		 * (RPR_ELEM_EMPTY_LOOP):
 		 *
 		 * 1. Primary path: loop back and attempt real matches in the
 		 *    next iteration (state, modified below).
@@ -1046,8 +1046,9 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 *    treating all remaining required iterations as empty
 		 *    matches (ffState, handled after the primary path).
 		 *
-		 * The snapshot must be taken BEFORE modifying state for the
-		 * loop-back, since both paths diverge from the same point.
+		 * The snapshot must be taken BEFORE modifying state for the loop-back,
+		 * since both paths diverge from the same point.
+		 *----------
 		 */
 		if (RPRElemCanEmptyLoop(elem))
 			ffState = nfa_state_create(winstate, state->elemIdx,
@@ -1062,12 +1063,11 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 						  currentPos);
 
 		/*
-		 * Fast-forward path for nullable bodies.  E.g. (A?){2,3} when
-		 * A doesn't match: the primary loop-back produces empty
-		 * iterations that cycle detection would kill.  Instead, exit
-		 * directly with count satisfied.  Route to elem->next (not
-		 * nfa_advance_end) to avoid creating competing greedy/reluctant
-		 * loop states.
+		 * Fast-forward path for nullable bodies.  E.g. (A?){2,3} when A
+		 * doesn't match: the primary loop-back produces empty iterations that
+		 * cycle detection would kill.  Instead, exit directly with count
+		 * satisfied.  Route to elem->next (not nfa_advance_end) to avoid
+		 * creating competing greedy/reluctant loop states.
 		 */
 		if (ffState != NULL)
 		{
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 754fcd53099..2543170c374 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -825,7 +825,7 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 		return child;
 	}
 
-	/*
+	/*----------
 	 * Case 2: Outer exact (min == max): (A{2,3}){4} -> A{8,12}.
 	 *         Safe because every iteration produces the same range.
 	 *
@@ -833,9 +833,10 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 	 *         Safe because the child contributes exactly one per
 	 *         iteration, so the outer range maps directly.
 	 *
-	 * Unsafe example: (A{2}){2,3} produces counts 4 or 6 only, not
-	 * the full range 4..6, so we cannot flatten when child has a
-	 * non-trivial range AND outer is also a range.
+	 * Unsafe example: (A{2}){2,3} produces counts 4 or 6 only, not the full
+	 * range 4..6, so we cannot flatten when child has a non-trivial range AND
+	 * outer is also a range.
+	 *----------
 	 */
 	if (child->max != RPR_QUANTITY_INF &&
 		(pattern->min == pattern->max ||
-- 
2.50.1 (Apple Git-155)

From 8339a5783d3c9738f043e8aea9d02ecfffe42af0 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 01:19:37 +0900
Subject: [PATCH 40/40] Reword RPR navigation function descriptions in
 pg_proc.dat

The descr fields for prev/next/first/last in pg_proc.dat were
written tersely (e.g. 'previous value', 'first value in match'),
inconsistent with the surrounding window-function entries that all
follow a 'fetch the ... row value' style (lag, lead, first_value,
last_value, nth_value).

Reword the eight RPR navigation entries to match that pattern,
while distinguishing partition-scoped (prev/next) from match-scoped
(first/last) navigation in the wording.

  prev / prev_offset:   fetch the (Nth) preceding row value
  next / next_offset:   fetch the (Nth) following row value
  first / first_offset: fetch the (Nth) first row value within match
  last / last_offset:   fetch the (Nth-from-)last row value within match

Catalog descr only; no functional or test impact.  Visible to users
through psql's \df+ output.
---
 src/include/catalog/pg_proc.dat | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6d70ed23aeb..55c41e66100 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10972,28 +10972,28 @@
 { 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 => 'previous value',
+{ oid => '8126', descr => 'fetch the preceding row value',
   proname => 'prev', provolatile => 's', prorettype => 'anyelement',
   proargtypes => 'anyelement', prosrc => 'window_prev' },
-{ oid => '8128', descr => 'previous value at offset',
+{ 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 => 'next value',
+{ oid => '8127', descr => 'fetch the following row value',
   proname => 'next', provolatile => 's', prorettype => 'anyelement',
   proargtypes => 'anyelement', prosrc => 'window_next' },
-{ oid => '8129', descr => 'next value at offset',
+{ 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 => 'first value in match',
+{ oid => '8130', descr => 'fetch the first row value within match',
   proname => 'first', provolatile => 's', prorettype => 'anyelement',
   proargtypes => 'anyelement', prosrc => 'window_first' },
-{ oid => '8132', descr => 'first value in match at offset',
+{ 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 => 'last value in match',
+{ oid => '8131', descr => 'fetch the last row value within match',
   proname => 'last', provolatile => 's', prorettype => 'anyelement',
   proargtypes => 'anyelement', prosrc => 'window_last' },
-{ oid => '8133', descr => 'last value in match at offset',
+{ 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' },
 
-- 
2.50.1 (Apple Git-155)

---

From: Tatsuo Ishii <[email-masked]>
Date: Sat, 02 May 2026 13:17:18 +0900 (JST)

Hi Henson,

Thank you for the patch sets! Quick question:

I know in v47 JIT compilation supports PREV/NEXT. What about
FIRST/LAST and compound forms?

Regards,

---

From: Henson Choi <[email-masked]>
Date: Sat, 2 May 2026 13:30:25 +0900

Hi Tatsuo,

> FIRST/LAST and compound forms?

Yes, those are JIT-compiled too in v47.

The JIT side only deals with two opcodes -- EEOP_RPR_NAV_SET
and EEOP_RPR_NAV_RESTORE.

FIRST/LAST and the compound forms differ only in how
ExecEvalRPRNavSet() computes target_pos for the slot swap;
the opcode interface and the call into ExecEvalRPRNavSet /
ExecEvalRPRNavRestore are unchanged.

So 0018's JIT support carries over to FIRST/LAST and
compounds without any JIT-side change.

Regards,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Sat, 02 May 2026 13:38:28 +0900 (JST)

Hi Henson,

> compounds without any JIT-side change.

Ok, from user's point of view, PREV/NEXT/FIRST/LAST and compound forms
are all JIT-compiled. I will add it to the "major changes" section in
the v47 patches posting.

Regards,

---

From: Tatsuo Ishii <[email-masked]>
Date: Sat, 02 May 2026 14:03:04 +0900 (JST)

Attached is the v47 patches for Row pattern recognition (SQL/RPR).

Major changes from v46 include:

- Change implementation of row pattern navigation operations using
  "1-slot model", which allows to implement more standard compliant
  features such as an offset argument, more row pattern navigation
  operations (FIRST, LAST) and compound forms.

- Row pattern navigation operations now support FIRST, LAST and
  compound forms

- Add JIT compilation support for all row pattern navigation
  operations (including compound forms)

- Add tuplestore trim optimization for RPR PREV navigation

- Window function last_value() now allows to set mark in certain cases

- Change the implementation of reduced frame map. Now consumes less CPU and memory

- Add more optimization (absorption). e.g. (A B B)+

- Add planner integration tests (rpr_integration.sql)

- Add src/backend/executor/README.rpr (previously was in ExecRPR.c)

Current status:

The series of patches are to implement the row pattern recognition
(SQL/RPR) feature. Currently the implementation is a subset of SQL/RPR
(ISO/IEC 19075-2:2016). Namely, implementation of some features of
R020 (WINDOW clause). R010 (MATCH_RECOGNIZE) is out of the scope of
the patches.

Currently following features are implemented in the patches.

- PATTERN
- PATTERN regular expressions (+, *, ?)
  alternation (|), grouping () , {n}, {n,}, {n,m}, {,m}
  reluctant quantifiers (*? etc.),
- DEFINE
- INITIAL
- AFTER MATCH SKIP TO PAST LAST ROW
- AFTER MATCH SKIP TO NEXT ROW
- Row pattern navigation (FIRST, LAST, PREV, NEXT and their compound forms)

Currently following features are not implemented in the patches.

- MEASURES
- Pattern variable name qualified column reference (e.g. A.price)
- SUBSET
- SEEK
- AFTER MATCH SKIP TO
- AFTER MATCH SKIP TO FIRST
- AFTER MATCH SKIP TO LAST
- PATTERN regular expression  {- and -}, () (empty pattern)
  Anchors (^, $) are not permitted with RPR in Window clause by the
  standard.
- PERMUTE
- CLASSIFIER

Regards,

---

From: Henson Choi <[email-masked]>
Date: Sun, 3 May 2026 14:34:47 +0900

Hi Tatsuo, hi hackers,

I'd like to ask for a bit of review help before PGConf.dev 2026
(Vancouver, May 19–22).

For the SQL/RPR R020 poster going on display at the conference, I
have prepared a companion deep-dive page to sit behind the on-site
QR code, building on the patch series Tatsuo has been driving in
this thread:

  https://assam258-5892.github.io/postgresql/rpr/

It walks through the standard scope (R010 vs R020), worked examples,
NFA semantics, navigation functions, parser-to-executor design and
pruning rules, plus an interactive NFA simulator. To widen reach,
the page is also available in 23 AI-pretranslated languages from
the switcher:
العربية, Čeština, Dansk, Deutsch, Español, Suomi, Français,
עברית, Magyar, Italiano, 日本語, 한국어, Nederlands, Norsk,
Polski, Português (BR), Română, Русский, Slovenčina, Svenska,
Türkçe, 中文 (简体), 繁體中文.
Each translated page carries a clear "AI pre-translated, errors
possible" notice.

The ask (light, entirely optional):

  - English original has had one review pass; Korean I reviewed
    myself.
  - Tatsuo — if you ever find a free moment, a quick glance at the
    Japanese version would be very much appreciated, but absolutely
    no rush.
  - Native speakers of any of the other 22 languages — incidental
    corrections welcome (this thread or off-list), but nothing is
    expected.

References (also linked from the deep-dive page):
  - thread:
https://www.postgresql.org/message-id/flat/20230625.210509.1276733411677577841.t-ishii%40sranhm.sra.co.jp
  - latest patch (v47):
https://postgr.es/m/[email-masked]
  - commitfest #4460: https://commitfest.postgresql.org/patch/4460/

Best regards,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 05 May 2026 09:01:24 +0900 (JST)

> Attached is the v47 patches for Row pattern recognition (SQL/RPR).

v47 starts to check whether range variable qualified expressions are
used in DEFINE clause. If used, raise an error. This is good because
we don't support the syntax yet (pattern variable range var), or it's
prohibited (from clause range var). However, the error message may not
be appropreate for the case when complex data type is involved.

CREATE TEMP TABLE item (name TEXT, amount INT);
CREATE TABLE
CREATE TEMP TABLE sales(items item);
CREATE TABLE
SELECT (items).name, (items).amount, count(*) OVER w
FROM sales WINDOW w AS (
    ORDER BY (items).name
    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
    AFTER MATCH SKIP PAST LAST ROW
    PATTERN (A+)
    DEFINE A AS (A.items).amount > 10
);
ERROR:  pattern variable qualified column reference "a.items" is not supported in DEFINE clause
LINE 7:     DEFINE A AS (A.items).amount > 10
                         ^
If I change the DEFINE clause to:

    DEFINE A AS (sales.items).amount > 10

I get:

ERROR:  range variable qualified column reference "sales.items" is not allowed in DEFINE clause
LINE 8:     DEFINE A AS (sales.items).amount > 10
                         ^

In both cases, "a.items" or "sales.items" in the error messages are
not column names, therefore the wording "column reference" in the
error messages are not appropriate.

The checks are in transformColumnRef().  Its argument "ColumnRef
*cref->fields" is a list of column ref fields in DEFINE expression in
the raw parse tree.  Usually it's something like range_var.col_name or
just col_name etc. So it works. However when complex data types are
involved, cref->fields is something like sales.items. No col_name
included. In this case, ColumnRef is under Indirection node, and the
col_name is stored in Indirection->indirection, not in ColumnRef.
Therefore there's no way for transformColumnRef() to know the col_name.

In order to fix the issue, I think we need to add code to understand
range var qualification form "A.item" or "sales.items" in complex data
types case. But is it worth the trouble? The reword is just nicer
error messages. If we support MEASURES, the code is useful. But so far
we have decided to not support it in the first cut of RPR.

Maybe we should just change the error messages something like below
for now?

ERROR:  pattern variable qualified expression "a.items" is not supported in DEFINE clause
ERROR:  range variable qualified expression "sales.items" is not allowed in DEFINE clause

Regards,

---

From: jian he <[email-masked]>
Date: Tue, 5 May 2026 11:53:07 +0800

> > Attached is the v47 patches for Row pattern recognition (SQL/RPR).

Hi
The patchset is too big, I only downloaded v47-0001...
Just a quick skim of the code on my end.
I noticed two code style issues, currently we don't have a code style
reference in the manual.

1. We need to add trailing commas to enum definitions. See
https://git.postgresql.org/cgit/postgresql.git/commit/?id=611806cd726fc92989ac918eac48fd8d684869c7

2.
+ | '{' ',' Iconst '}' Op
+ {
+ if (strcmp($5, "?") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid token after range quantifier"),
+ errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+ parser_errposition(@5)));
+ if ($3 <= 0 || $3 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+ parser_errposition(@3));
+ $$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);

+ (errcode(ERRCODE_SYNTAX_ERROR),
The leading parenthesis is optional and can be removed, fewer
parentheses are always better.
See related discussion:
https://postgr.es/m/[email-masked]

Since v47-0001 introduces many ereport(ERROR) messages, it makes sense
to move the related regression tests from the other patch into
v47-0001, IMHO.

--
jian
https://www.enterprisedb.com/

---

From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 13:26:56 +0900

Hi Tatsuo,

Thanks for the close look.  Inline below, then a sketch of what I
plan to put in v48 so you can pick what to pull first.

> error messages are not appropriate.

Agreed.  Even for the simple non-composite cases the noun is fuzzy
once A_Indirection enters the picture.  "expression" reads naturally
in both cases.

> we have decided to not support it in the first cut of RPR.

Not now.  Teaching the pre-check about the surrounding A_Indirection
duplicates work that MEASURES will need anyway, and the only visible
gain before MEASURES is a more accurate echo of the source text.

Instead I'll mark the limitation in three places so future MEASURES
work can't miss it: a block comment above the pre-check that names
the limitation and the revisit point, an XXX cross-reference in
parse_rpr.c pointing back to the pre-check, and composite-type cases
in rpr_base whose expected output quotes only the ColumnRef portion
-- so when indirection-aware quoting lands, those outputs churn as
a tripwire.

> ERROR:  range variable qualified expression "sales.items" is not allowed

Yes -- planned for v48.  While I'm in that path I'll also clean up
two adjacent issues:

  - the pre-check is binary, so an unknown qualifier (typo,
    undefined name) is misreported as a range-variable reference
    with SYNTAX_ERROR instead of falling through to the standard
    "column does not exist" / "missing FROM-clause entry"
    diagnostic;

  - parse_rpr.c and the SELECT docs claim DEFINE-name "collection"
    and "filtering during planning"; the actual behavior is
    validate-and-reject at parse analysis.

------------------------------------------------------------------
v48 follow-up plan
------------------------------------------------------------------

The items below are independent, so they can ship as separate
patches or as a single batched posting -- whichever you prefer.
Order is just for narrative; nothing depends on anything else.

  [A] Sizable refactor: collapse the four DEFINE walkers across
      parser/planner/executor into a single phase-tagged
      traversal.  On that base, reject volatile / NextValueExpr
      in DEFINE (the NFA may re-evaluate predicates during
      backtracking; STABLE / IMMUTABLE remain accepted), and
      bundle a STABLE/IMMUTABLE baseline test as a guard against
      accidental over-rejection.

  [B] Make the empty-match path observable in tests: replace the
      stale XXX comments in rpr_nfa with the actual behavior, and
      add EXPLAIN ANALYZE coverage in rpr_explain that surfaces
      "NFA: N matched (len 0/0/0.0)" so the NFA-found-but-window-
      empty case is regression-visible.

  [C] Trim the per-advance NFA visited-bitmap reset to a high-water
      mark range instead of the full bitmap.  Tradeoff: two int16
      comparisons added per visit, paying off for larger NFAs but
      added overhead for single-word bitmaps; semantics unchanged.
      I'll leave the decision to apply to your judgment.

  [D] DEFINE qualifier diagnostic: tri-classify (pattern var /
      range var / fall-through), reword to "expression", add
      unknown-qualifier and composite-type tests, and sync the
      adjacent stale comments and SELECT doc.

If any of [A]..[D] looks misjudged or you'd prefer a different
slicing, I'll reshape before posting.

Best,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 05 May 2026 17:08:45 +0900 (JST)

Hi Jian,

> The patchset is too big, I only downloaded v47-0001...

Yeah, since the feature is complex...

> https://git.postgresql.org/cgit/postgresql.git/commit/?id=611806cd726fc92989ac918eac48fd8d684869c7

I forgot that. Thanks for reminding.

> https://postgr.es/m/[email-masked]

Right. I will review all other patches.

> to move the related regression tests from the other patch into

Thanks for the suggestion but I do not agree the direction.  It will
make the patch maintenance work harder. Also it will make it difficult
to understand the regression test organization.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 18:15:29 +0900

Hi Tatsuo, Jian,

Both coding-style items from Jian's review will be addressed and
included in the next patch series.

> I forgot that. Thanks for reminding.

Three enum types introduced by the patch lack a trailing comma on the
last enumerator: RPRNavKind, RPRNavOffsetKind, and RPRPatternNodeType.
All three will be corrected in the next series.

> Right. I will review all other patches.

The outer-parentheses pattern appears in 17 ereport() calls in
parse_rpr.c and 2 in optimizer/plan/rpr.c.  All 19 sites will be
cleaned up in the next series.

Jian, I will review your other patches as well.

Best,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 05 May 2026 18:42:28 +0900 (JST)

Hi Henson,

> cleaned up in the next series.

I see them in gram.y as well. See attached patch.

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f3cedfbbb18..aa587e6aced 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17610,10 +17610,10 @@ opt_row_pattern_initial_or_seek:
 			| SEEK
 				{
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("SEEK is not supported"),
-							 errhint("Use INITIAL instead."),
-							 parser_errposition(@1)));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("SEEK is not supported"),
+							errhint("Use INITIAL instead."),
+							parser_errposition(@1));
 				}
 			| /*EMPTY*/		{ $$ = true; }
 		;
@@ -17740,40 +17740,40 @@ row_pattern_quantifier_opt:
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
 					else
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("unsupported quantifier \"%s\"", $1),
-								 errhint("Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions."),
-								 parser_errposition(@1)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("unsupported quantifier \"%s\"", $1),
+								errhint("Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions."),
+								parser_errposition(@1));
 				}
 			/* RELUCTANT quantifiers (when lexer separates tokens) */
 			| '*' Op
 				{
 					if (strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after \"*\" quantifier"),
-								 errhint("Did you mean \"*?\" for reluctant quantifier?"),
-								 parser_errposition(@2)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after \"*\" quantifier"),
+								errhint("Did you mean \"*?\" for reluctant quantifier?"),
+								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
 				}
 			| '+' Op
 				{
 					if (strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after \"+\" quantifier"),
-								 errhint("Did you mean \"+?\" for reluctant quantifier?"),
-								 parser_errposition(@2)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after \"+\" quantifier"),
+								errhint("Did you mean \"+?\" for reluctant quantifier?"),
+								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
 				}
 			| Op Op
 				{
 					if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid quantifier combination"),
-								 errhint("Did you mean \"??\" for reluctant quantifier?"),
-								 parser_errposition(@1)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid quantifier combination"),
+								errhint("Did you mean \"??\" for reluctant quantifier?"),
+								parser_errposition(@1));
 					$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
 				}
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
@@ -17823,10 +17823,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($4, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n} to make it reluctant."),
-								 parser_errposition(@4)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n} to make it reluctant."),
+								parser_errposition(@4));
 					if ($2 <= 0 || $2 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17838,10 +17838,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($5, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
-								 parser_errposition(@5)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+								parser_errposition(@5));
 					if ($2 < 0 || $2 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17853,10 +17853,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($5, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
-								 parser_errposition(@5)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+								parser_errposition(@5));
 					if ($3 <= 0 || $3 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17868,10 +17868,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($6, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
-								 parser_errposition(@6)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
+								parser_errposition(@6));
 					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),

---

From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 18:59:12 +0900

Hi Tatsuo,

Thank you for the patch. It will be included in the next series.

Best,
Henson

---

From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:41:46 +0900

Hi Tatsuo, hi Jian,

Attached are 10 incremental patches on top of v47, covering the
follow-up plan ([A]..[D]) I sketched on 2026-05-05, Jian's two
coding-style items (trailing commas, outer parentheses; gram.y
folded into 0007), one comment-only patch (0009), and one trivial
cleanup (0010).  Packaging into the next version is at your
discretion.

Patch summary:

  - 0001 Add DEFINE non-volatile baseline to rpr_integration B9
      Adds a STABLE/IMMUTABLE baseline ahead of the volatile
      case so the rejection in 0002 is visible against a working
      reference.

  - 0002 Unify RPR DEFINE walkers and reject volatile callees
      [A].  Collapses the four DEFINE walkers (parser / planner
      / executor) into one phase-tagged traversal and, on that
      base, rejects volatile / NextValueExpr at parse-analysis.
      STABLE / IMMUTABLE accepted.  The offset runtime-constant
      check is folded in; B9 flips to error-case and its XXX is
      dropped.

  - 0003 Cover RPR empty-match path with EXPLAIN tests; fix
         stale XXX comments
      [B].  Adds rpr_explain coverage that surfaces "NFA: N
      matched (len 0/0/0.0)" so the NFA-found-but-empty-frame
      path is regression-visible; replaces stale rpr_nfa XXXs.

  - 0004 Reclassify DEFINE qualifier check and reword
         diagnostic to "expression"
      [D].  Tri-classifies (pattern var / range var / fall-
      through) so unknown qualifiers fall through to the
      standard "column does not exist" diagnostic.  Reworded to
      "expression" since the quoted token may include
      indirection on composite types (e.g. (A.items).amount).

  - 0005 Sync stale comments on DEFINE/PATTERN handling
      validateRPRPatternVarCount() rejects DEFINE names not in
      PATTERN, but comments / SELECT doc described it as
      "collection" / "filtered during planning".  Wording
      aligned; XXX note's "column references" bumped to
      "expressions" to match 0004.  Comment + doc only.

  - 0006 Add trailing commas to RPR enum definitions
      Per Jian.  RPRNavKind, RPRNavOffsetKind, RPRPatternNodeType.

  - 0007 Remove optional outer parentheses from ereport() calls
         in RPR files
      Per Jian + Tatsuo's gram.y patch.  19 sites in
      parse_rpr.c / optimizer/plan/rpr.c plus the gram.y sites.

  - 0008 Add high-water mark tracking to NFA visited bitmap
         reset
      [C].  Resets only the touched span (O(span_words) instead
      of O(numElements/64)), at the cost of two int16 comparisons
      per visit.  Semantics unchanged; happy to drop if you
      prefer the simpler bulk reset.

  - 0009 Document DEFINE subquery rejection as intentional
         over-rejection
      Comment-only.  Records that ISO/IEC 9075-2 §4.18.4 /
      6.17.4 (R010 / R020) permits a non-RPR, non-pattern-var-
      correlated subquery in DEFINE, and that our blanket
      rejection is deliberate over-rejection.  The case
      distinction (walk subquery Query for nested RPR; match
      ColumnRef qualifiers against ancestor p_rpr_pattern_vars)
      is doable with existing infrastructure and left as future
      work, not blocked on any other feature.

  - 0010 Remove duplicate #include in nodeWindowAgg.c
      "common/int.h" was included twice and the first occurrence
      was misplaced between catalog/* headers.  Drops the
      misplaced first; the second sits at the correct
      alphabetical position.

Still deferred:
  - B7 Recursive CTE XXX: pending community input on the
    §4.18.5 / 6.17.5 interpretation.

Please let me know if any of 0001-0010 looks misjudged or if you
would prefer a different slicing.

Best regards,
Henson
From e29920bfc2c9beec4f0f85e8547d21e19ca23526 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 19:09:34 +0900
Subject: [PATCH 1/9] Add DEFINE non-volatile baseline to rpr_integration B9
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

B9 in src/test/regress/sql/rpr_integration.sql today only exercises a
volatile callee (random()) inside DEFINE.  Add a baseline query in the
same section that uses STABLE (to_char) and IMMUTABLE (length) callees,
which must remain accepted when the upcoming volatile-only prohibition
lands.  This guards against the prohibition being broadened by accident
(e.g. contain_volatile_functions → contain_mutable_functions); the
volatile case alone would not catch over-rejection.

Ordered baseline-first then volatile, matching other B sections.
---
 src/test/regress/expected/rpr_integration.out | 26 +++++++++++++++++++
 src/test/regress/sql/rpr_integration.sql      | 13 ++++++++++
 2 files changed, 39 insertions(+)

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 0cc79b75601..ef6a157f45d 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1421,6 +1421,32 @@ DROP INDEX rpr_integ_id_idx;
 -- pattern matching non-deterministic.  When the prohibition lands,
 -- this test must be replaced with an error-case test that expects
 -- random() in DEFINE to be rejected.
+-- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
+-- This locks the boundary of the volatile-only prohibition.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val)
+                AND length('x') = 1
+                AND to_char(date '2026-01-01', 'YYYY') = '2026')
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- Volatile (random) is the prohibition target; today still accepted.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 6d47728e911..d9748979d54 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -884,6 +884,19 @@ DROP INDEX rpr_integ_id_idx;
 -- this test must be replaced with an error-case test that expects
 -- random() in DEFINE to be rejected.
 
+-- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
+-- This locks the boundary of the volatile-only prohibition.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val)
+                AND length('x') = 1
+                AND to_char(date '2026-01-01', 'YYYY') = '2026')
+ORDER BY id;
+
+-- Volatile (random) is the prohibition target; today still accepted.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
-- 
2.50.1 (Apple Git-155)

From 8811de4fea828b5415c3d8fe414665b6efcd5b9d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 21:37:59 +0900
Subject: [PATCH 2/9] Unify RPR DEFINE walkers and reject volatile callees

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

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

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

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 99de36b57f2..1a754bcdac5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3312,8 +3312,12 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 											es);
 						break;
 					case RPR_NAV_OFFSET_FIXED:
-						ExplainPropertyInteger("Nav Mark Lookahead", NULL,
-											   firstOffset, es);
+						if (firstOffset == INT64_MAX)
+							ExplainPropertyText("Nav Mark Lookahead", "infinite",
+												es);
+						else
+							ExplainPropertyInteger("Nav Mark Lookahead", NULL,
+												   firstOffset, es);
 						break;
 					default:
 						elog(ERROR, "unrecognized RPR nav offset kind: %d",
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 93cb9bbdd11..af2351bccb8 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -246,8 +246,7 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
 static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
 
 /* Forward declarations - navigation offset evaluation */
-static void eval_nav_max_offset(WindowAggState *winstate, List *defineClause);
-static void eval_nav_first_offset(WindowAggState *winstate, List *defineClause);
+static void eval_define_offsets(WindowAggState *winstate, List *defineClause);
 
 /*
  * Not null info bit array consists of 2-bit items
@@ -2579,12 +2578,10 @@ ExecWindowAgg(PlanState *pstate)
 			{
 				int64		firstreach;
 
-				if (winstate->navFirstOffset > -winstate->nfaContext->matchStartRow)
-					firstreach = winstate->nfaContext->matchStartRow
-						+ winstate->navFirstOffset;
-				else
-					firstreach = 0;
-				navmarkpos = Min(navmarkpos, firstreach);
+				if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
+										 winstate->navFirstOffset,
+										 &firstreach))
+					navmarkpos = Min(navmarkpos, Max(firstreach, 0));
 			}
 
 			if (navmarkpos > winstate->nav_winobj->markpos)
@@ -3037,17 +3034,13 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->rpSkipTo = node->rpSkipTo;
 	/* Set up row pattern recognition PATTERN clause (compiled NFA) */
 	winstate->rpPattern = node->rpPattern;
-	/* Set up nav offsets for tuplestore trim */
+	/* Set up nav offsets for tuplestore trim; resolve any NEEDS_EVAL kinds */
 	winstate->navMaxOffsetKind = node->navMaxOffsetKind;
 	winstate->navMaxOffset = node->navMaxOffset;
-	if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
-		eval_nav_max_offset(winstate, node->defineClause);
 	winstate->hasFirstNav = node->hasFirstNav;
 	winstate->navFirstOffsetKind = node->navFirstOffsetKind;
 	winstate->navFirstOffset = node->navFirstOffset;
-	if (winstate->hasFirstNav &&
-		winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
-		eval_nav_first_offset(winstate, node->defineClause);
+	eval_define_offsets(winstate, node->defineClause);
 
 	/* Copy match_start dependency bitmapset for per-context evaluation */
 	winstate->defineMatchStartDependent = bms_copy(node->defineMatchStartDependent);
@@ -3997,42 +3990,64 @@ eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
 typedef struct
 {
 	WindowAggState *winstate;
-	int64		maxOffset;
-	bool		overflow;		/* true if overflow detected */
-} EvalNavMaxContext;
+	int64		maxOffset;		/* max backward-reach offset across all nav
+								 * exprs */
+	bool		maxOverflow;	/* true if backward-reach overflow detected */
+	int64		minFirstOffset; /* min forward-from-match_start offset; may be
+								 * negative (PREV_FIRST: inner - outer < 0) */
+} EvalDefineOffsetsContext;
 
 /*
- * eval_nav_max_offset_walker
- *		Walk expression tree evaluating backward-reach offsets at runtime.
+ * visit_nav_exec
+ *		nav_traversal_walker callback (NavVisitFn) for the executor side.
+ *		At each RPRNavExpr, evaluates the nav's offset expression(s) at
+ *		runtime via eval_nav_offset_helper and accumulates:
+ *
+ *		  - maxOffset (backward reach): PREV, LAST-with-offset, compound
+ *			PREV_LAST (sets maxOverflow on int64 overflow), compound
+ *			NEXT_LAST (= max(inner - outer, 0))
+ *		  - minFirstOffset (forward reach from match_start): FIRST,
+ *			compound PREV_FIRST (= inner - outer, may be negative),
+ *			compound NEXT_FIRST (= inner + outer, clamped to INT64_MAX on
+ *			overflow; always >= 0 so never updates minFirstOffset in practice)
  *
- * Handles simple PREV, LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
+ * Counterpart of visit_nav_plan but using runtime evaluation instead of
+ * Const folding; runs only for offsets the planner marked NEEDS_EVAL.
+ * Match-start dependency is not recomputed here -- the planner's bitmapset
+ * is reused via winstate->defineMatchStartDependent.
  */
-static bool
-eval_nav_max_offset_walker(Node *node, void *ctx)
+static void
+visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 {
-	EvalNavMaxContext *context = (EvalNavMaxContext *) ctx;
-
-	if (node == NULL)
-		return false;
+	EvalDefineOffsetsContext *context = (EvalDefineOffsetsContext *) t->data;
 
-	/* Short-circuit if overflow already detected */
-	if (context->overflow)
-		return false;
+	/*
+	 * Parser guarantee (mirrors visit_nav_plan): nav's direct children are
+	 * never RPRNavExpr -- compound nesting is flattened in place and any
+	 * other nesting is rejected.  Outer-kind dispatch is sufficient.
+	 */
+	Assert(nav->arg == NULL || !IsA(nav->arg, RPRNavExpr));
+	Assert(nav->offset_arg == NULL || !IsA(nav->offset_arg, RPRNavExpr));
+	Assert(nav->compound_offset_arg == NULL ||
+		   !IsA(nav->compound_offset_arg, RPRNavExpr));
 
-	if (IsA(node, RPRNavExpr))
+	/* Backward reach: PREV, LAST-with-offset */
+	if (!context->maxOverflow)
 	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
 		int64		reach = 0;
+		bool		gotReach = false;
 
 		if (nav->kind == RPR_NAV_PREV)
 		{
 			reach = eval_nav_offset_helper(context->winstate,
 										   nav->offset_arg, 1);
+			gotReach = true;
 		}
 		else if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
 		{
 			reach = eval_nav_offset_helper(context->winstate,
 										   nav->offset_arg, 0);
+			gotReach = true;
 		}
 		else if (nav->kind == RPR_NAV_PREV_LAST ||
 				 nav->kind == RPR_NAV_NEXT_LAST)
@@ -4045,168 +4060,123 @@ eval_nav_max_offset_walker(Node *node, void *ctx)
 			if (nav->kind == RPR_NAV_PREV_LAST)
 			{
 				if (pg_add_s64_overflow(inner, outer, &reach))
-				{
-					context->overflow = true;
-					return false;
-				}
+					context->maxOverflow = true;
+				else
+					gotReach = true;
 			}
 			else
-				reach = (inner > outer) ? inner - outer : 0;
+			{
+				reach = Max(inner - outer, 0);
+				gotReach = true;
+			}
 		}
 
-		context->maxOffset = Max(context->maxOffset, reach);
-
-		return false;			/* don't walk into children */
+		if (gotReach)
+			context->maxOffset = Max(context->maxOffset, reach);
 	}
 
-	return expression_tree_walker(node, eval_nav_max_offset_walker, ctx);
-}
-
-/*
- * eval_nav_max_offset
- *		Evaluate non-constant backward-reach offsets at executor init time.
- *
- * Called when the planner set navMaxOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
- * because some offset in PREV, LAST-with-offset, or compound PREV_LAST/
- * NEXT_LAST contains a parameter or non-foldable expression.
- *
- * On overflow, sets navMaxOffsetKind to RPR_NAV_OFFSET_RETAIN_ALL so that
- * tuplestore trim is disabled for backward navigation.
- */
-static void
-eval_nav_max_offset(WindowAggState *winstate, List *defineClause)
-{
-	EvalNavMaxContext ctx;
-	ListCell   *lc;
-
-	ctx.winstate = winstate;
-	ctx.maxOffset = 0;
-	ctx.overflow = false;
-
-	foreach(lc, defineClause)
+	/* Forward reach from match_start: FIRST, compound PREV_FIRST/NEXT_FIRST */
+	if (nav->kind == RPR_NAV_FIRST)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
+		int64		reach;
 
-		eval_nav_max_offset_walker((Node *) te->expr, &ctx);
-	}
-
-	if (ctx.overflow)
-	{
-		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
-		winstate->navMaxOffset = 0;
+		reach = eval_nav_offset_helper(context->winstate,
+									   nav->offset_arg, 0);
+		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
-	else
+	else if (nav->kind == RPR_NAV_PREV_FIRST ||
+			 nav->kind == RPR_NAV_NEXT_FIRST)
 	{
-		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navMaxOffset = ctx.maxOffset;
-	}
-}
+		int64		inner = eval_nav_offset_helper(context->winstate,
+												   nav->offset_arg, 0);
+		int64		outer = eval_nav_offset_helper(context->winstate,
+												   nav->compound_offset_arg, 1);
+		int64		reach;
 
-typedef struct
-{
-	WindowAggState *winstate;
-	int64		minOffset;
-	bool		found;
-} EvalNavFirstContext;
-
-/*
- * eval_nav_first_offset_walker
- *		Walk expression tree evaluating forward-from-match_start offsets.
- *
- * Handles simple FIRST and compound PREV_FIRST/NEXT_FIRST.
- */
-static bool
-eval_nav_first_offset_walker(Node *node, void *ctx)
-{
-	EvalNavFirstContext *context = (EvalNavFirstContext *) ctx;
-
-	if (node == NULL)
-		return false;
-
-	if (IsA(node, RPRNavExpr))
-	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-		int64		combined = INT64_MAX;
-
-		if (nav->kind == RPR_NAV_FIRST)
+		if (nav->kind == RPR_NAV_PREV_FIRST)
 		{
-			context->found = true;
-			combined = eval_nav_offset_helper(context->winstate,
-											  nav->offset_arg, 0);
+			/*
+			 * reach = inner - outer.  Both are non-negative, so the result >=
+			 * -INT64_MAX, which cannot underflow int64.
+			 */
+			reach = inner - outer;
 		}
-		else if (nav->kind == RPR_NAV_PREV_FIRST ||
-				 nav->kind == RPR_NAV_NEXT_FIRST)
+		else
 		{
-			int64		inner = eval_nav_offset_helper(context->winstate,
-													   nav->offset_arg, 0);
-			int64		outer = eval_nav_offset_helper(context->winstate,
-													   nav->compound_offset_arg, 1);
-
-			context->found = true;
-			if (nav->kind == RPR_NAV_PREV_FIRST)
-			{
-				/*
-				 * combined = inner - outer.  Both are non-negative, so the
-				 * result >= -INT64_MAX, which cannot underflow int64.
-				 */
-				combined = inner - outer;
-			}
-			else
-			{
-				/*
-				 * NEXT_FIRST: combined = inner + outer.  This can overflow,
-				 * but the result is always >= 0, so it never updates
-				 * minOffset (which tracks the minimum).  Clamp to INT64_MAX
-				 * on overflow.
-				 */
-				if (pg_add_s64_overflow(inner, outer, &combined))
-					combined = INT64_MAX;
-			}
+			/*
+			 * NEXT_FIRST: reach = inner + outer.  This can overflow, but the
+			 * result is always >= 0, so it never updates minFirstOffset
+			 * (which tracks the minimum).  Clamp to INT64_MAX on overflow.
+			 */
+			if (pg_add_s64_overflow(inner, outer, &reach))
+				reach = INT64_MAX;
 		}
-
-		context->minOffset = Min(context->minOffset, combined);
-
-		return false;
+		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
-
-	return expression_tree_walker(node, eval_nav_first_offset_walker, ctx);
 }
 
 /*
- * eval_nav_first_offset
- *		Evaluate non-constant forward-from-match_start offsets at executor
- *		init time.
+ * eval_define_offsets
+ *		Evaluate non-constant nav offsets at executor init time.
+ *
+ * Called when the planner set navMaxOffsetKind and/or navFirstOffsetKind
+ * to RPR_NAV_OFFSET_NEEDS_EVAL because some offset contains a parameter
+ * or non-foldable expression.  Updates only the fields whose kind was
+ * NEEDS_EVAL; FIXED kinds are left unchanged.
  *
- * Called when the planner set navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
- * because some offset in FIRST or compound PREV_FIRST/NEXT_FIRST contains
- * a parameter or non-foldable expression.
+ * On backward-reach overflow, sets navMaxOffsetKind to
+ * RPR_NAV_OFFSET_RETAIN_ALL so that tuplestore trim is disabled for
+ * backward navigation.
  */
 static void
-eval_nav_first_offset(WindowAggState *winstate, List *defineClause)
+eval_define_offsets(WindowAggState *winstate, List *defineClause)
 {
-	EvalNavFirstContext ctx;
+	EvalDefineOffsetsContext ctx;
+	NavTraversal trav;
 	ListCell   *lc;
+	bool		needsMax = (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
+	bool		needsFirst = (winstate->hasFirstNav &&
+							  winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
+
+	if (!needsMax && !needsFirst)
+		return;
 
 	ctx.winstate = winstate;
-	ctx.minOffset = INT64_MAX;
-	ctx.found = false;
+	ctx.maxOffset = 0;
+	ctx.maxOverflow = false;
+	ctx.minFirstOffset = INT64_MAX;
+
+	trav.visit = visit_nav_exec;
+	trav.data = &ctx;

-		eval_nav_first_offset_walker((Node *) te->expr, &ctx);
+		nav_traversal_walker((Node *) te->expr, &trav);
 	}
 
-	if (ctx.found && ctx.minOffset < INT64_MAX)
+	if (needsMax)
 	{
-		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navFirstOffset = ctx.minOffset;
+		if (ctx.maxOverflow)
+		{
+			winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
+			winstate->navMaxOffset = 0;
+		}
+		else
+		{
+			winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+			winstate->navMaxOffset = ctx.maxOffset;
+		}
 	}
-	else
+
+	if (needsFirst)
 	{
 		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navFirstOffset = 0;
+		if (ctx.minFirstOffset < INT64_MAX)
+			winstate->navFirstOffset = ctx.minFirstOffset;
+		else
+			winstate->navFirstOffset = INT64_MAX;
 	}
 }
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 52205cc7159..5f154c8fe12 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -294,6 +294,9 @@ static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 RPRPattern *compiledPattern,
 								 List *defineClause,
 								 Bitmapset *defineMatchStartDependent,
+								 RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
+								 bool hasFirstNav,
+								 RPRNavOffsetKind navFirstOffsetKind, int64 navFirstOffset,
 								 List *qual, bool topWindow,
 								 Plan *lefttree);
 static Group *make_group(List *tlist, List *qual, int numGroupCols,
@@ -2464,13 +2467,20 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 }
 
 /*
- * NavOffsetContext - context for compute_nav_offsets walker.
+ * DefineMetadataContext - context for compute_define_metadata walker.
  *
- * Collects both backward reach (PREV, LAST-with-offset, compound
- * PREV_LAST/NEXT_LAST) and forward-from-match-start reach (FIRST,
- * compound PREV_FIRST/NEXT_FIRST) in a single tree walk.
+ * Collects three pieces of metadata from the DEFINE clause in a single
+ * tree walk per variable:
+ *   - backward reach (PREV, LAST-with-offset, compound PREV_LAST/NEXT_LAST)
+ *   - forward-from-match-start reach (FIRST, compound PREV_FIRST/NEXT_FIRST)
+ *   - per-variable match_start dependency (variables containing FIRST,
+ *     LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/
+ *     NEXT_LAST-with-offset require per-context re-evaluation)
+ *
+ * The driver sets curVarIdx to the index of the variable being walked
+ * before each invocation; the walker uses it to populate matchStartDependent.
  */
-typedef struct NavOffsetContext
+typedef struct DefineMetadataContext
 {
 	int64		maxOffset;		/* max PREV/LAST backward offset (>= 0) */
 	bool		maxNeedsEval;	/* non-constant PREV/LAST offset found */
@@ -2479,7 +2489,10 @@ typedef struct NavOffsetContext
 								 * PREV_FIRST) */
 	bool		hasFirst;		/* any FIRST node found */
 	bool		firstNeedsEval; /* non-constant FIRST offset found */
-} NavOffsetContext;
+	int			curVarIdx;		/* DEFINE variable currently being walked */
+	Bitmapset  *matchStartDependent;	/* variables that depend on
+										 * match_start */
+} DefineMetadataContext;
 
 /*
  * Helper: extract constant offset from an expression, handling NULL/negative.
@@ -2514,175 +2527,207 @@ extract_const_offset(Expr *expr, int64 defaultOffset, int64 *result)
 }
 
 /*
- * nav_offset_walker
- *		Expression tree walker for compute_nav_offsets.
+ * visit_nav_plan
+ *		nav_traversal_walker callback (NavVisitFn) for the planner side.
+ *		At each RPRNavExpr in a DEFINE expression, computes:
+ *
+ *		  1. backward reach (maxOffset) for tuplestore trim:
+ *			 - PREV(v, N), LAST(v, N)              → N (default 1)
+ *			 - compound PREV_LAST(v, N, M)         → N + M (overflow → maxOverflow)
+ *			 - compound NEXT_LAST(v, N, M)         → max(N - M, 0)
  *
- * For each RPRNavExpr found, extract its constant offset(s) and update the
- * NavOffsetContext with the maximum backward reach (maxOffset) and minimum
- * forward reach (firstOffset).  Handles simple navigation (PREV, NEXT,
- * FIRST, LAST) and compound forms (PREV_FIRST, NEXT_FIRST, PREV_LAST,
- * NEXT_LAST) by combining inner and outer offsets.
+ *		  2. forward reach (firstOffset) for tuplestore trim:
+ *			 - FIRST(v, N)                         → N (default 0)
+ *			 - compound PREV_FIRST(v, N, M)        → N - M (may be negative)
+ *			 - compound NEXT_FIRST(v, N, M)        → N + M
  *
- * Non-constant offsets set maxNeedsEval or firstNeedsEval.  Overflow sets
- * maxOverflow or firstOverflow for RETAIN_ALL fallback.
+ *		  3. per-variable match_start dependency for absorption suppression:
+ *			 outer nav kinds that reach match_start (FIRST, LAST-with-offset,
+ *			 PREV_FIRST, NEXT_FIRST, PREV_LAST/NEXT_LAST-with-offset) add
+ *			 curVarIdx to matchStartDependent.
+ *
+ * Constant offsets are extracted via extract_const_offset; non-constant
+ * offsets set maxNeedsEval / firstNeedsEval so the executor can resolve
+ * them at init time (see visit_nav_exec).  Classification uses only the
+ * outer nav kind: parser nesting restrictions prevent FIRST/LAST inside
+ * a PREV/NEXT value subexpression.
  */
-static bool
-nav_offset_walker(Node *node, void *ctx)
+static void
+visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 {
-	NavOffsetContext *context = (NavOffsetContext *) ctx;
+	DefineMetadataContext *context = (DefineMetadataContext *) t->data;
 
-	if (node == NULL)
-		return false;
+	/*
+	 * Parser guarantee: by the time the planner sees a DEFINE expression,
+	 * compound nesting has been flattened into a single RPRNavExpr and any
+	 * other RPRNavExpr nesting has been rejected.  So nav's direct child
+	 * fields are not themselves RPRNavExpr nodes, and outer-kind dispatch
+	 * below is sufficient.
+	 */
+	Assert(nav->arg == NULL || !IsA(nav->arg, RPRNavExpr));
+	Assert(nav->offset_arg == NULL || !IsA(nav->offset_arg, RPRNavExpr));
+	Assert(nav->compound_offset_arg == NULL ||
+		   !IsA(nav->compound_offset_arg, RPRNavExpr));
 
-	if (IsA(node, RPRNavExpr))
+	/*
+	 * Simple PREV(v, N) and LAST(v, N): backward reach from currentpos. LAST
+	 * without offset = currentpos, no backward reach. NEXT: forward only,
+	 * irrelevant for trim.
+	 */
+	if (nav->kind == RPR_NAV_PREV ||
+		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
 	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-
-		/*
-		 * Simple PREV(v, N) and LAST(v, N): backward reach from currentpos.
-		 * LAST without offset = currentpos, no backward reach. NEXT: forward
-		 * only, irrelevant for trim.
-		 */
-		if (nav->kind == RPR_NAV_PREV ||
-			(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
+		if (!context->maxNeedsEval)
 		{
-			if (!context->maxNeedsEval)
-			{
-				int64		offset;
+			int64		offset;
 
-				if (extract_const_offset(nav->offset_arg, 1, &offset))
-					context->maxOffset = Max(context->maxOffset, offset);
-				else
-					context->maxNeedsEval = true;
-			}
+			if (extract_const_offset(nav->offset_arg, 1, &offset))
+				context->maxOffset = Max(context->maxOffset, offset);
+			else
+				context->maxNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Simple FIRST(v, N): forward reach from match_start. Smaller N means
-		 * older rows needed.
-		 */
-		if (nav->kind == RPR_NAV_FIRST)
-		{
-			context->hasFirst = true;
+	/*
+	 * Simple FIRST(v, N): forward reach from match_start. Smaller N means
+	 * older rows needed.
+	 */
+	if (nav->kind == RPR_NAV_FIRST)
+	{
+		context->hasFirst = true;
 
-			if (!context->firstNeedsEval)
-			{
-				int64		offset;
+		if (!context->firstNeedsEval)
+		{
+			int64		offset;
 
-				if (extract_const_offset(nav->offset_arg, 0, &offset))
-					context->firstOffset = Min(context->firstOffset, offset);
-				else
-					context->firstNeedsEval = true;
-			}
+			if (extract_const_offset(nav->offset_arg, 0, &offset))
+				context->firstOffset = Min(context->firstOffset, offset);
+			else
+				context->firstNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Compound PREV_LAST / NEXT_LAST: base = currentpos. PREV_LAST(v, N,
-		 * M): target = currentpos - N - M -> lookback = N + M NEXT_LAST(v, N,
-		 * M): target = currentpos - N + M -> lookback = max(N - M, 0)
-		 */
-		if (nav->kind == RPR_NAV_PREV_LAST ||
-			nav->kind == RPR_NAV_NEXT_LAST)
+	/*
+	 * Compound PREV_LAST / NEXT_LAST: base = currentpos. PREV_LAST(v, N, M):
+	 * target = currentpos - N - M -> lookback = N + M NEXT_LAST(v, N, M):
+	 * target = currentpos - N + M -> lookback = max(N - M, 0)
+	 */
+	if (nav->kind == RPR_NAV_PREV_LAST ||
+		nav->kind == RPR_NAV_NEXT_LAST)
+	{
+		if (!context->maxNeedsEval)
 		{
-			if (!context->maxNeedsEval)
-			{
-				int64		inner,
-							outer,
-							combined;
+			int64		inner;
+			int64		outer;
+			int64		reach;
 
-				if (extract_const_offset(nav->offset_arg, 0, &inner) &&
-					extract_const_offset(nav->compound_offset_arg, 1, &outer))
+			if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+				extract_const_offset(nav->compound_offset_arg, 1, &outer))
+			{
+				if (nav->kind == RPR_NAV_PREV_LAST)
 				{
-					if (nav->kind == RPR_NAV_PREV_LAST)
-					{
-						if (pg_add_s64_overflow(inner, outer, &combined))
-						{
-							context->maxOverflow = true;
-							return false;
-						}
-					}
+					if (pg_add_s64_overflow(inner, outer, &reach))
+						context->maxOverflow = true;
 					else
-						combined = (inner > outer) ? inner - outer : 0;
-
-					context->maxOffset = Max(context->maxOffset, combined);
+						context->maxOffset = Max(context->maxOffset, reach);
 				}
 				else
-					context->maxNeedsEval = true;
+				{
+					reach = Max(inner - outer, 0);
+					context->maxOffset = Max(context->maxOffset, reach);
+				}
 			}
+			else
+				context->maxNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Compound PREV_FIRST / NEXT_FIRST: base = match_start. PREV_FIRST(v,
-		 * N, M): target = match_start + N - M NEXT_FIRST(v, N, M): target =
-		 * match_start + N + M The combined offset (N+/-M) from match_start
-		 * can be negative, meaning rows before match_start are needed.
-		 */
-		if (nav->kind == RPR_NAV_PREV_FIRST ||
-			nav->kind == RPR_NAV_NEXT_FIRST)
+	/*
+	 * Compound PREV_FIRST / NEXT_FIRST: base = match_start. PREV_FIRST(v, N,
+	 * M): target = match_start + N - M NEXT_FIRST(v, N, M): target =
+	 * match_start + N + M The combined offset (N+/-M) from match_start can be
+	 * negative, meaning rows before match_start are needed.
+	 */
+	if (nav->kind == RPR_NAV_PREV_FIRST ||
+		nav->kind == RPR_NAV_NEXT_FIRST)
+	{
+		context->hasFirst = true;
+
+		if (!context->firstNeedsEval)
 		{
-			context->hasFirst = true;
+			int64		inner;
+			int64		outer;
+			int64		reach;
 
-			if (!context->firstNeedsEval)
+			if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+				extract_const_offset(nav->compound_offset_arg, 1, &outer))
 			{
-				int64		inner,
-							outer,
-							combined;
-
-				if (extract_const_offset(nav->offset_arg, 0, &inner) &&
-					extract_const_offset(nav->compound_offset_arg, 1, &outer))
+				if (nav->kind == RPR_NAV_PREV_FIRST)
 				{
-					if (nav->kind == RPR_NAV_PREV_FIRST)
-					{
-						/*
-						 * combined = inner - outer.  Both are non-negative,
-						 * so the result >= -INT64_MAX, which cannot underflow
-						 * int64.  No overflow check needed.
-						 */
-						combined = inner - outer;
-					}
-					else
-					{
-						/*
-						 * NEXT_FIRST: combined = inner + outer.  This can
-						 * overflow, but the result is always >= 0, so it
-						 * never updates firstOffset (which tracks the
-						 * minimum).  Clamp to INT64_MAX on overflow.
-						 */
-						if (pg_add_s64_overflow(inner, outer, &combined))
-							combined = INT64_MAX;
-					}
-
-					context->firstOffset = Min(context->firstOffset, combined);
+					/*
+					 * reach = inner - outer.  Both are non-negative, so the
+					 * result >= -INT64_MAX, which cannot underflow int64. No
+					 * overflow check needed.
+					 */
+					reach = inner - outer;
 				}
 				else
-					context->firstNeedsEval = true;
+				{
+					/*
+					 * NEXT_FIRST: reach = inner + outer.  This can overflow,
+					 * but the result is always >= 0, so it never updates
+					 * firstOffset (which tracks the minimum).  Clamp to
+					 * INT64_MAX on overflow.
+					 */
+					if (pg_add_s64_overflow(inner, outer, &reach))
+						reach = INT64_MAX;
+				}
+
+				context->firstOffset = Min(context->firstOffset, reach);
 			}
+			else
+				context->firstNeedsEval = true;
 		}
-
-		/* Don't walk into RPRNavExpr children */
-		return false;
 	}
 
-	return expression_tree_walker(node, nav_offset_walker, ctx);
+	/* Match-start dependency: classify the outer nav kind. */
+	if (nav->kind == RPR_NAV_FIRST ||
+		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL) ||
+		nav->kind == RPR_NAV_PREV_FIRST ||
+		nav->kind == RPR_NAV_NEXT_FIRST ||
+		((nav->kind == RPR_NAV_PREV_LAST ||
+		  nav->kind == RPR_NAV_NEXT_LAST) &&
+		 nav->offset_arg != NULL))
+		context->matchStartDependent =
+			bms_add_member(context->matchStartDependent,
+						   context->curVarIdx);
 }
 
 /*
- * compute_nav_offsets
- *		Compute navigation offsets for tuplestore trim in a single pass.
+ * compute_define_metadata
+ *		Compute navigation offsets and match_start dependency for the
+ *		DEFINE clause in a single pass per variable.
  *
- * Walks all DEFINE clause expressions once, computing:
+ * Walks each DEFINE variable expression once, computing:
  *   - maxOffset: max backward reach from PREV, LAST-with-offset,
  *     compound PREV_LAST/NEXT_LAST
  *   - hasFirst/firstOffset: min forward-from-match-start reach from
  *     FIRST, compound PREV_FIRST/NEXT_FIRST
+ *   - matchStartDependent: bitmapset of variable indices whose
+ *     expressions contain navigation that depends on match_start
+ *     (FIRST, LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/
+ *     PREV_LAST/NEXT_LAST-with-offset).  Such variables require
+ *     per-context re-evaluation during NFA processing.
  */
 static void
-compute_nav_offsets(List *defineClause,
-					RPRNavOffsetKind *maxKind, int64 *maxResult,
-					bool *hasFirst,
-					RPRNavOffsetKind *firstKind, int64 *firstResult)
+compute_define_metadata(List *defineClause,
+						RPRNavOffsetKind *maxKind, int64 *maxResult,
+						bool *hasFirst,
+						RPRNavOffsetKind *firstKind, int64 *firstResult,
+						Bitmapset **matchStartDependent)
 {
-	NavOffsetContext ctx;
+	DefineMetadataContext ctx;
+	NavTraversal trav;
 	ListCell   *lc;
 
 	ctx.maxOffset = 0;
@@ -2691,14 +2736,22 @@ compute_nav_offsets(List *defineClause,
 	ctx.firstOffset = INT64_MAX;	/* sentinel: no FIRST found yet */
 	ctx.hasFirst = false;
 	ctx.firstNeedsEval = false;
+	ctx.curVarIdx = 0;
+	ctx.matchStartDependent = NULL;
+
+	trav.visit = visit_nav_plan;
+	trav.data = &ctx;

-		nav_offset_walker((Node *) te->expr, &ctx);
+		nav_traversal_walker((Node *) te->expr, &trav);
+		ctx.curVarIdx++;
 	}
 
+	*matchStartDependent = ctx.matchStartDependent;
+
 	/* Max backward offset */
 	if (ctx.maxOverflow)
 	{
@@ -2725,15 +2778,11 @@ compute_nav_offsets(List *defineClause,
 			*firstKind = RPR_NAV_OFFSET_NEEDS_EVAL;
 			*firstResult = 0;
 		}
-		else if (ctx.firstOffset == INT64_MAX)
-		{
-			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = 0;	/* only implicit FIRST(v) */
-		}
 		else
 		{
 			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = ctx.firstOffset; /* may be negative */
+			*firstResult = ctx.firstOffset; /* may be negative; INT64_MAX if
+											 * overflowed */
 		}
 	}
 	else
@@ -2743,83 +2792,6 @@ compute_nav_offsets(List *defineClause,
 	}
 }
 
-/*
- * has_match_start_dependency
- *		Check if an expression tree contains navigation that depends on
- *		match_start: FIRST, LAST-with-offset, or compound PREV_FIRST/
- *		NEXT_FIRST/PREV_LAST/NEXT_LAST with offset.  Such expressions
- *		require per-context re-evaluation during NFA processing.
- *
- * LAST without offset always resolves to currentpos and is
- * match_start-independent.
- */
-static bool
-has_match_start_dependency(Node *node, void *context)
-{
-	if (node == NULL)
-		return false;
-
-	if (IsA(node, RPRNavExpr))
-	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-
-		if (nav->kind == RPR_NAV_FIRST)
-			return true;
-		if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
-			return true;
-
-		/* Compound kinds with FIRST base depend on match_start */
-		if (nav->kind == RPR_NAV_PREV_FIRST ||
-			nav->kind == RPR_NAV_NEXT_FIRST)
-			return true;
-
-		/*
-		 * PREV_LAST/NEXT_LAST: inner is LAST, which uses currentpos.
-		 * match_start-dependent only if inner has offset (clamped to
-		 * match_start).
-		 */
-		if ((nav->kind == RPR_NAV_PREV_LAST ||
-			 nav->kind == RPR_NAV_NEXT_LAST) &&
-			nav->offset_arg != NULL)
-			return true;
-
-		/* Check children (arg may contain further nav expressions) */
-		return has_match_start_dependency((Node *) nav->arg, context);
-	}
-
-	return expression_tree_walker(node, has_match_start_dependency, NULL);
-}
-
-/*
- * compute_match_start_dependent
- *		Build a Bitmapset of DEFINE variable indices whose expressions
- *		depend on match_start (contain FIRST, LAST-with-offset, or
- *		compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST with offset).
- *
- * Variables in this set require per-context re-evaluation during NFA
- * processing, because different contexts may have different match_start
- * values.
- */
-static Bitmapset *
-compute_match_start_dependent(List *defineClause)
-{
-	Bitmapset  *result = NULL;
-	ListCell   *lc;
-	int			varIdx = 0;
-
-	foreach(lc, defineClause)
-	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
-		if (has_match_start_dependency((Node *) te->expr, NULL))
-			result = bms_add_member(result, varIdx);
-
-		varIdx++;
-	}
-
-	return result;
-}
-
 /*
  * create_windowagg_plan
  *
@@ -2848,6 +2820,11 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 	List	   *filteredDefineClause = NIL;
 	RPRPattern *compiledPattern = NULL;
 	Bitmapset  *matchStartDependent = NULL;
+	RPRNavOffsetKind navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+	int64		navMaxOffset = 0;
+	bool		hasFirstNav = false;
+	RPRNavOffsetKind navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
+	int64		navFirstOffset = 0;

 	/*
@@ -2910,8 +2887,16 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		buildDefineVariableList(wc->defineClause, &defineVariableList);
 		filteredDefineClause = wc->defineClause;
 
-		/* Identify match_start-dependent DEFINE variables */
-		matchStartDependent = compute_match_start_dependent(wc->defineClause);
+		/*
+		 * Walk DEFINE once: collect nav offsets (for tuplestore trim) and the
+		 * bitmapset of match_start-dependent variables (for absorption
+		 * suppression in buildRPRPattern).
+		 */
+		compute_define_metadata(wc->defineClause,
+								&navMaxOffsetKind, &navMaxOffset,
+								&hasFirstNav,
+								&navFirstOffsetKind, &navFirstOffset,
+								&matchStartDependent);
 
 		/* Compile and optimize RPR patterns */
 		compiledPattern = buildRPRPattern(wc->rpPattern,
@@ -2937,6 +2922,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  compiledPattern,
 						  filteredDefineClause,
 						  matchStartDependent,
+						  navMaxOffsetKind, navMaxOffset,
+						  hasFirstNav,
+						  navFirstOffsetKind, navFirstOffset,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -7011,6 +6999,9 @@ make_windowagg(List *tlist, WindowClause *wc,
 			   RPRPattern *compiledPattern,
 			   List *defineClause,
 			   Bitmapset *defineMatchStartDependent,
+			   RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
+			   bool hasFirstNav,
+			   RPRNavOffsetKind navFirstOffsetKind, int64 navFirstOffset,
 			   List *qual, bool topWindow, Plan *lefttree)
 {
 	WindowAgg  *node = makeNode(WindowAgg);
@@ -7048,11 +7039,12 @@ make_windowagg(List *tlist, WindowClause *wc,
 	/* Store pre-computed match_start dependency bitmapset */
 	node->defineMatchStartDependent = defineMatchStartDependent;
 
-	/* Compute nav offsets for tuplestore trim optimization */
-	compute_nav_offsets(defineClause,
-						&node->navMaxOffsetKind, &node->navMaxOffset,
-						&node->hasFirstNav,
-						&node->navFirstOffsetKind, &node->navFirstOffset);
+	/* Store pre-computed nav offsets for tuplestore trim optimization */
+	node->navMaxOffsetKind = navMaxOffsetKind;
+	node->navMaxOffset = navMaxOffset;
+	node->hasFirstNav = hasFirstNav;
+	node->navFirstOffsetKind = navFirstOffsetKind;
+	node->navFirstOffset = navFirstOffset;
 
index 2543170c374..a817eb4a63f 100644
@@ -41,6 +41,7 @@
 
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
 #include "optimizer/rpr.h"
 
 /* Forward declarations - pattern comparison */
@@ -1991,3 +1992,36 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 
 	return result;
 }
+
+/*
+ * nav_traversal_walker
+ *		Shared expression-tree walker that locates RPRNavExpr nodes in a
+ *		DEFINE expression and dispatches each one to a caller-supplied
+ *		visitor.  Used by:
+ *		  - planner (visit_nav_plan in createplan.c) to collect tuplestore
+ *		    trim offsets and per-variable match_start dependency
+ *		  - executor (visit_nav_exec in nodeWindowAgg.c) to evaluate
+ *		    non-constant nav offsets at WindowAggState init time
+ *
+ * The driver wraps a mode-specific context in a NavTraversal and passes
+ * it as ctx; the visitor casts t->data to its own context type.  Children
+ * of an RPRNavExpr are not walked: the parser's nesting restrictions
+ * ensure offsets and dependencies are fully captured by the outer nav
+ * kind, so the visitor only needs to inspect the RPRNavExpr itself.
+ */
+bool
+nav_traversal_walker(Node *node, void *ctx)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		NavTraversal *t = (NavTraversal *) ctx;
+
+		t->visit(t, (RPRNavExpr *) node);
+		return false;
+	}
+
+	return expression_tree_walker(node, nav_traversal_walker, ctx);
+}
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index f56b7db5bc8..32701f9b74b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -25,6 +25,7 @@
 
 #include "postgres.h"
 
+#include "catalog/pg_proc.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -35,14 +36,33 @@
 #include "parser/parse_expr.h"
 #include "parser/parse_rpr.h"
 #include "parser/parse_target.h"
+#include "utils/lsyscache.h"
+
+/* DEFINE clause walker context -- see define_walker for usage. */
+typedef enum
+{
+	DEFINE_PHASE_BODY,			/* top-level DEFINE expression */
+	DEFINE_PHASE_NAV_ARG,		/* inside an outer nav's arg subtree */
+	DEFINE_PHASE_NAV_OFFSET,	/* inside an outer nav's offset_arg /
+								 * compound_offset_arg */
+} DefinePhase;
+
+typedef struct
+{
+	ParseState *pstate;
+	DefinePhase phase;
+	int			nav_count;		/* RPRNavExpr nodes seen in current nav.arg */
+	bool		has_column_ref; /* Var seen in current nav scope */
+	RPRNavKind	inner_kind;		/* kind of first nested nav in current arg */
+} DefineWalkCtx;
 
 /* Forward declarations */
 static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 									   List *rpDefs, List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
 								   WindowDef *windef, List **targetlist);
-static void check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate);
-static bool check_rpr_nav_nesting_walker(Node *node, void *context);
+static bool define_walker(Node *node, void *context);
+static bool nav_volatile_func_checker(Oid funcid, void *context);
 
 /*
  * transformRPR
@@ -412,9 +432,22 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	foreach_ptr(TargetEntry, te, defineClause)
 		te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
 
-	/* check for nested PREV/NEXT and missing column references */
+	/*
+	 * Validate DEFINE expressions: nested PREV/NEXT, column references,
+	 * compound flatten, volatile callees -- all in a single walk per
+	 * variable.
+	 */
 	foreach_ptr(TargetEntry, te, defineClause)
-		(void) check_rpr_nav_nesting_walker((Node *) te->expr, pstate);
+	{
+		DefineWalkCtx ctx;
+
+		ctx.pstate = pstate;
+		ctx.phase = DEFINE_PHASE_BODY;
+		ctx.nav_count = 0;
+		ctx.has_column_ref = false;
+		ctx.inner_kind = 0;
+		(void) define_walker((Node *) te->expr, &ctx);
+	}
 
 	/* mark column origins */
 	markTargetListOrigins(pstate, defineClause);
@@ -426,169 +459,239 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 }
 
 /*
- * check_rpr_nav_expr
- *		Validate a single RPRNavExpr node by walking its arg and offset_arg
- *		subtrees in a single pass each.  Check for illegal nesting, missing
- *		column references, and non-constant offset expressions.
+ * Single-pass DEFINE clause validator.
  *
- * Nesting rules (SQL standard 5.6.4):
- *   - PREV/NEXT wrapping FIRST/LAST: allowed (compound navigation)
- *   - FIRST/LAST wrapping PREV/NEXT: prohibited
- *   - Same-category nesting (PREV inside PREV, FIRST inside FIRST, etc.):
- *     prohibited
+ * One walker function (define_walker) visits every node in a DEFINE
+ * expression exactly once and enforces every rule:
+ *   - Volatile callees and NextValueExpr are rejected at parse time
+ *     (RPR's NFA may evaluate the same row's predicate multiple times
+ *     during backtracking, so a volatile result would make matching
+ *     non-deterministic).
+ *   - For each outer RPRNavExpr (per SQL 5.6.4 nesting rules):
+ *     * arg must contain at least one column reference
+ *     * PREV/NEXT wrapping FIRST/LAST flattens to a compound kind
+ *     * Other nestings are rejected (FIRST(PREV()), PREV(PREV()), ...)
+ *     * offset_arg / compound_offset_arg must not contain column refs
+ *
+ * The walker uses a phase tag to know which subtree it is in: DEFINE
+ * body (top-level), inside a nav.arg, or inside a nav.offset_arg /
+ * compound_offset_arg.  When entering an outer nav (PHASE_BODY), it
+ * walks nav.arg in PHASE_NAV_ARG to collect nesting/column-ref state,
+ * applies compound flatten or raises a nesting error, then walks the
+ * (post-flatten) offset(s) in PHASE_NAV_OFFSET to enforce the
+ * constant-offset rule.  No subtree is walked twice.
  */
-typedef struct
+
+/*
+ * nav_volatile_func_checker
+ *		check_functions_in_node callback: true if funcid is VOLATILE.
+ */
+static bool
+nav_volatile_func_checker(Oid funcid, void *context)
 {
-	int			nav_count;		/* number of RPRNavExpr nodes found */
-	bool		has_column_ref; /* Var found */
-	RPRNavKind	inner_kind;		/* kind of first (outermost) nested RPRNavExpr */
-} NavCheckResult;
+	return (func_volatile(funcid) == PROVOLATILE_VOLATILE);
+}
 
+/*
+ * define_walker
+ *		Single-pass DEFINE clause validator.  At each node, enforces:
+ *
+ *		  [1] no volatile callees (and no NextValueExpr) -- anywhere in
+ *			  the tree, regardless of phase
+ *		  [2] for each outer RPRNavExpr (PHASE_BODY → PHASE_NAV_ARG):
+ *			  - nav.arg must contain at least one column reference
+ *			  - PREV/NEXT wrapping FIRST/LAST is flattened in place
+ *				to a compound kind (PREV_FIRST, PREV_LAST, NEXT_FIRST,
+ *				NEXT_LAST)
+ *			  - any other nesting is rejected (FIRST(PREV()),
+ *				PREV(PREV()), FIRST(FIRST()), three-or-more deep)
+ *		  [3] for each nav offset (PHASE_NAV_OFFSET):
+ *			  - must be a run-time constant (no column references)
+ *
+ * Var sightings feed the column-ref rule for the enclosing nav scope;
+ * RPRNavExpr sightings inside PHASE_NAV_ARG feed the nesting decision.
+ * See the comment block above DefinePhase for the overall design and
+ * how each subtree is walked exactly once.
+ */
 static bool
-nav_check_walker(Node *node, void *context)
+define_walker(Node *node, void *context)
 {
-	NavCheckResult *result = (NavCheckResult *) context;
+	DefineWalkCtx *ctx = (DefineWalkCtx *) context;
 
 	if (node == NULL)
 		return false;
-	if (IsA(node, RPRNavExpr))
-	{
-		if (result->nav_count == 0)
-			result->inner_kind = ((RPRNavExpr *) node)->kind;
-		result->nav_count++;
-	}
-	if (IsA(node, Var))
-		result->has_column_ref = true;
-
-	return expression_tree_walker(node, nav_check_walker, context);
-}
 
-static void
-check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
-{
-	NavCheckResult result;
-	bool		outer_is_physical = (nav->kind == RPR_NAV_PREV ||
-									 nav->kind == RPR_NAV_NEXT);
+	/*
+	 * Reject volatile callees and sequence operations anywhere in the DEFINE
+	 * clause: they are non-deterministic across the multiple predicate
+	 * evaluations that NFA backtracking and PREV/NEXT navigation may trigger
+	 * for a single row.
+	 */
+	if (check_functions_in_node(node, nav_volatile_func_checker, NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("volatile functions are not allowed in DEFINE clause"),
+				 parser_errposition(ctx->pstate, exprLocation(node))));
+	if (IsA(node, NextValueExpr))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("sequence operations are not allowed in DEFINE clause"),
+				 parser_errposition(ctx->pstate, exprLocation(node))));
 
-	/* Check arg subtree: nesting + column reference in one walk */
-	memset(&result, 0, sizeof(result));
-	(void) nav_check_walker((Node *) nav->arg, &result);
+	/* Var sighting feeds the column-ref rule for the enclosing nav scope. */
+	if (IsA(node, Var) &&
+		(ctx->phase == DEFINE_PHASE_NAV_ARG ||
+		 ctx->phase == DEFINE_PHASE_NAV_OFFSET))
+		ctx->has_column_ref = true;
 
-	if (result.nav_count > 0)
+	if (IsA(node, RPRNavExpr))
 	{
-		bool		inner_is_physical = (result.inner_kind == RPR_NAV_PREV ||
-										 result.inner_kind == RPR_NAV_NEXT);
+		RPRNavExpr *nav = (RPRNavExpr *) node;
 
-		if (outer_is_physical && !inner_is_physical)
+		if (ctx->phase == DEFINE_PHASE_NAV_ARG)
 		{
 			/*
-			 * PREV/NEXT wrapping FIRST/LAST: compound navigation per SQL
-			 * standard 5.6.4.  Flatten the nested RPRNavExpr into a single
-			 * compound node.  The inner RPRNavExpr must be the direct arg of
-			 * the outer; expressions like PREV(val + FIRST(v)) are not valid
-			 * compound navigation.
+			 * Nested nav inside an outer nav.arg: record for the outer's
+			 * compound / nesting decision, then keep recursing so deeper Vars
+			 * and volatile callees are still observed.
 			 */
-			RPRNavExpr *inner;
-
-			/* Reject triple-or-deeper nesting (e.g. PREV(FIRST(PREV(x)))) */
-			if (result.nav_count > 1)
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("cannot nest row pattern navigation more than two levels deep"),
-						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-						 parser_errposition(pstate, nav->location)));
-
-			if (!IsA(nav->arg, RPRNavExpr))
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
-						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-						 parser_errposition(pstate, nav->location)));
-			inner = (RPRNavExpr *) nav->arg;
-
-			/* Determine compound kind */
-			if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
-				nav->kind = RPR_NAV_PREV_FIRST;
-			else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
-				nav->kind = RPR_NAV_PREV_LAST;
-			else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
-				nav->kind = RPR_NAV_NEXT_FIRST;
-			else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
-				nav->kind = RPR_NAV_NEXT_LAST;
-
-			/* Move outer offset to compound_offset_arg */
-			nav->compound_offset_arg = nav->offset_arg;
-
-			/* Move inner offset and arg up */
-			nav->offset_arg = inner->offset_arg;
-			nav->arg = inner->arg;
-
-			/* No further nesting check needed - already validated */
-			return;
-		}
-		else if (!outer_is_physical && inner_is_physical)
-		{
-			/* FIRST/LAST wrapping PREV/NEXT: prohibited by standard */
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
-					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-					 parser_errposition(pstate, nav->location)));
+			if (ctx->nav_count == 0)
+				ctx->inner_kind = nav->kind;
+			ctx->nav_count++;
+			return expression_tree_walker(node, define_walker, ctx);
 		}
-		else if (outer_is_physical && inner_is_physical)
+
+		if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
 		{
-			/* PREV/NEXT wrapping PREV/NEXT: prohibited */
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
-					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-					 parser_errposition(pstate, nav->location)));
+			/*
+			 * Navs inside offset_arg are unusual but not directly banned; the
+			 * constant-offset rule will catch any Var or volatile they
+			 * contain.
+			 */
+			return expression_tree_walker(node, define_walker, ctx);
 		}
-		else
+
+		/*
+		 * PHASE_BODY: this is an outer nav at top level.  Walk arg first to
+		 * collect nesting / column-ref state, then validate and (for compound
+		 * forms) flatten, then walk offset(s).
+		 */
 		{
-			/* FIRST/LAST wrapping FIRST/LAST: prohibited */
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
-					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-					 parser_errposition(pstate, nav->location)));
-		}
-	}
-	if (!result.has_column_ref)
-		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("argument of row pattern navigation operation must include at least one column reference"),
-				 parser_errposition(pstate, nav->location)));
+			DefineWalkCtx saved = *ctx;
+			bool		outer_phys = (nav->kind == RPR_NAV_PREV ||
+									  nav->kind == RPR_NAV_NEXT);
+			bool		flattened = false;
+
+			ctx->phase = DEFINE_PHASE_NAV_ARG;
+			ctx->nav_count = 0;
+			ctx->has_column_ref = false;
+			ctx->inner_kind = 0;
+			(void) define_walker((Node *) nav->arg, ctx);
+
+			if (ctx->nav_count > 0)
+			{
+				bool		inner_phys = (ctx->inner_kind == RPR_NAV_PREV ||
+										  ctx->inner_kind == RPR_NAV_NEXT);
 
-	/* Check offset_arg: column ref + volatile in one walk */
-	if (nav->offset_arg != NULL)
-	{
-		memset(&result, 0, sizeof(result));
-		(void) nav_check_walker((Node *) nav->offset_arg, &result);
-
-		if (result.has_column_ref ||
-			contain_volatile_functions((Node *) nav->offset_arg))
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("row pattern navigation offset must be a run-time constant"),
-					 parser_errposition(pstate, nav->location)));
-	}
-}
+				if (outer_phys && !inner_phys)
+				{
+					RPRNavExpr *inner;
 
-/*
- * check_rpr_nav_nesting_walker
- *		Walk the DEFINE clause expression tree and validate each RPRNavExpr.
- */
-static bool
-check_rpr_nav_nesting_walker(Node *node, void *context)
-{
-	if (node == NULL)
-		return false;
-	if (IsA(node, RPRNavExpr))
-	{
-		check_rpr_nav_expr((RPRNavExpr *) node, (ParseState *) context);
-		/* don't recurse into arg; nesting already checked above */
-		return false;
+					/* Reject triple-or-deeper nesting */
+					if (ctx->nav_count > 1)
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("cannot nest row pattern navigation more than two levels deep"),
+								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								 parser_errposition(ctx->pstate, nav->location)));
+
+					if (!IsA(nav->arg, RPRNavExpr))
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								 parser_errposition(ctx->pstate, nav->location)));
+
+					inner = (RPRNavExpr *) nav->arg;
+
+					if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
+						nav->kind = RPR_NAV_PREV_FIRST;
+					else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
+						nav->kind = RPR_NAV_PREV_LAST;
+					else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
+						nav->kind = RPR_NAV_NEXT_FIRST;
+					else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
+						nav->kind = RPR_NAV_NEXT_LAST;
+
+					nav->compound_offset_arg = nav->offset_arg;
+					nav->offset_arg = inner->offset_arg;
+					nav->arg = inner->arg;
+					flattened = true;
+				}
+				else if (!outer_phys && inner_phys)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							 parser_errposition(ctx->pstate, nav->location)));
+				else if (outer_phys && inner_phys)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							 parser_errposition(ctx->pstate, nav->location)));
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							 parser_errposition(ctx->pstate, nav->location)));
+			}
+			else if (!ctx->has_column_ref)
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("argument of row pattern navigation operation must include at least one column reference"),
+						 parser_errposition(ctx->pstate, nav->location)));
+			}
+
+			/*
+			 * Walk offset arg(s) in PHASE_NAV_OFFSET to enforce the
+			 * constant-offset rule.  For compound forms, both the inner
+			 * (post-flatten nav->offset_arg) and outer (compound_offset_arg)
+			 * offsets must be constants; the inner's column-ref status was
+			 * not separately tracked during the PHASE_NAV_ARG walk (which
+			 * only checks that nav.arg as a whole has at least one Var), so
+			 * it is re-walked here to catch column references the inner
+			 * offset would have leaked.
+			 */
+			ctx->phase = DEFINE_PHASE_NAV_OFFSET;
+
+			if (nav->offset_arg != NULL)
+			{
+				ctx->has_column_ref = false;
+				(void) define_walker((Node *) nav->offset_arg, ctx);
+				if (ctx->has_column_ref)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("row pattern navigation offset must be a run-time constant"),
+							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg))));
+			}
+			if (flattened && nav->compound_offset_arg != NULL)
+			{
+				ctx->has_column_ref = false;
+				(void) define_walker((Node *) nav->compound_offset_arg, ctx);
+				if (ctx->has_column_ref)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("row pattern navigation offset must be a run-time constant"),
+							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg))));
+			}
+
+			*ctx = saved;
+			return false;
+		}
 	}
-	return expression_tree_walker(node, check_rpr_nav_nesting_walker, context);
+
+	return expression_tree_walker(node, define_walker, ctx);
 }
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 0a14cfad79b..63c4b09daff 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -62,4 +62,26 @@ extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariable
 								   RPSkipTo rpSkipTo, int frameOptions,
 								   bool hasMatchStartDependent);
 
+/*
+ * Shared traversal walker for DEFINE clause RPRNavExpr collection.
+ *
+ * Both planner (nav-offset / match_start dependency analysis) and executor
+ * (runtime offset evaluation) need to walk DEFINE expressions and dispatch
+ * per RPRNavExpr.  They differ only in what they do at each nav node, so
+ * the traversal frame is shared (nav_traversal_walker, defined in rpr.c)
+ * and the per-nav action is supplied as a callback.  The driver allocates
+ * a mode-specific context, points NavTraversal.data at it, and casts
+ * inside its visitor.
+ */
+struct NavTraversal;
+typedef void (*NavVisitFn) (struct NavTraversal *t, RPRNavExpr *nav);
+
+typedef struct NavTraversal
+{
+	NavVisitFn	visit;
+	void	   *data;			/* mode-specific context */
+} NavTraversal;
+
+extern bool nav_traversal_walker(Node *node, void *ctx);
+
 #endif							/* OPTIMIZER_RPR_H */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 85384f6b096..8793dda3cc3 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1144,7 +1144,31 @@ WINDOW w AS (
 );
 ERROR:  row pattern navigation offset must be a run-time constant
 LINE 7:     DEFINE A AS PREV(price, price) > 0
-                        ^
+                                    ^
+-- Non-constant offset: column reference in compound inner offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, price), 2) > 0
+);
+ERROR:  row pattern navigation offset must be a run-time constant
+LINE 7:     DEFINE A AS PREV(LAST(price, price), 2) > 0
+                                         ^
+-- Non-constant offset: column reference in compound outer offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, 1), price) > 0
+);
+ERROR:  row pattern navigation offset must be a run-time constant
+LINE 7:     DEFINE A AS PREV(LAST(price, 1), price) > 0
+                                             ^
 -- Non-constant offset: volatile function as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -1154,9 +1178,9 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS PREV(price, random()::int) > 0
 );
-ERROR:  row pattern navigation offset must be a run-time constant
+ERROR:  volatile functions are not allowed in DEFINE clause
 LINE 7:     DEFINE A AS PREV(price, random()::int) > 0
-                        ^
+                                    ^
 -- Non-constant offset: subquery as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -1181,7 +1205,7 @@ WINDOW w AS (
 ERROR:  cannot use subquery in DEFINE expression
 LINE 7:     DEFINE A AS PREV(price + (SELECT 1)) > 0
                                      ^
--- First arg: volatile function is allowed (evaluated on target row)
+-- Volatile function inside nav.arg is rejected at parse time
 SELECT company, tdate, price,
        first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
 FROM stock
@@ -1191,30 +1215,24 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price + random() * 0) >= 0
 );
- company  |   tdate    | price | first_value | last_value | count 
-----------+------------+-------+-------------+------------+-------
- company1 | 07-01-2023 |   100 |             |            |     0
- company1 | 07-02-2023 |   200 |         200 |        130 |     9
- company1 | 07-03-2023 |   150 |             |            |     0
- company1 | 07-04-2023 |   140 |             |            |     0
- company1 | 07-05-2023 |   150 |             |            |     0
- company1 | 07-06-2023 |    90 |             |            |     0
- company1 | 07-07-2023 |   110 |             |            |     0
- company1 | 07-08-2023 |   130 |             |            |     0
- company1 | 07-09-2023 |   120 |             |            |     0
- company1 | 07-10-2023 |   130 |             |            |     0
- company2 | 07-01-2023 |    50 |             |            |     0
- company2 | 07-02-2023 |  2000 |        2000 |       1300 |     9
- company2 | 07-03-2023 |  1500 |             |            |     0
- company2 | 07-04-2023 |  1400 |             |            |     0
- company2 | 07-05-2023 |  1500 |             |            |     0
- company2 | 07-06-2023 |    60 |             |            |     0
- company2 | 07-07-2023 |  1100 |             |            |     0
- company2 | 07-08-2023 |  1300 |             |            |     0
- company2 | 07-09-2023 |  1200 |             |            |     0
- company2 | 07-10-2023 |  1300 |             |            |     0
-(20 rows)
-
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 8:     DEFINE A AS PREV(price + random() * 0) >= 0
+                                     ^
+-- nextval is volatile (per pg_proc), so it is rejected via the FuncExpr
+-- path with the "volatile functions" message
+CREATE SEQUENCE rpr_seq;
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > nextval('rpr_seq')
+);
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 7:     DEFINE A AS price > nextval('rpr_seq')
+                                ^
+DROP SEQUENCE rpr_seq;
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index dc3522f930f..0a049d1beba 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -4744,9 +4744,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s
 (5 rows)
 
--- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
--- N + M overflows int64, but target is forward from match_start so it never
--- constrains trim.  Lookahead remains at default (0).
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> infinite
+-- N + M overflows int64; forward reach is unbounded, displayed as infinite.
 EXPLAIN (COSTS OFF) SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
 WINDOW w AS (
@@ -4760,7 +4759,7 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+
    Nav Mark Lookback: 0
-   Nav Mark Lookahead: 0
+   Nav Mark Lookahead: infinite
    ->  Function Scan on generate_series s
 (6 rows)
 
@@ -4803,7 +4802,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
 
 RESET plan_cache_mode;
 DEALLOCATE test_overflow_lookback;
--- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> infinite
 PREPARE test_overflow_lookahead(int8, int8) AS
 SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
@@ -4821,7 +4820,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+
    Nav Mark Lookback: 0
-   Nav Mark Lookahead: 0
+   Nav Mark Lookahead: infinite
    Storage: Memory  Maximum Storage: 17kB
    NFA States: 1 peak, 11 total, 0 merged
    NFA Contexts: 2 peak, 11 total, 10 pruned
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index ef6a157f45d..905bd3538de 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1406,23 +1406,12 @@ DROP INDEX rpr_integ_id_idx;
 -- ============================================================
 -- B9. RPR + Volatile function in DEFINE
 -- ============================================================
--- Records the current behaviour: DEFINE today accepts volatile
--- functions such as random() and the query runs to completion.
--- To keep the expected output deterministic the predicate uses
--- "random() >= 0.0", which is structurally equivalent to TRUE and
--- therefore does not perturb the match result.  The interesting
--- property is that volatile invocation does not crash or short-
--- circuit pattern matching.
---
--- XXX: volatile functions in DEFINE are slated to be rejected at
--- parse time.  Under RPR's NFA engine the same row's DEFINE
--- predicate may be evaluated multiple times (backtracking,
--- PREV/NEXT navigation), so a truly volatile result would make
--- pattern matching non-deterministic.  When the prohibition lands,
--- this test must be replaced with an error-case test that expects
--- random() in DEFINE to be rejected.
+-- Volatile functions in DEFINE are rejected at parse time.  Under
+-- RPR's NFA engine the same row's DEFINE predicate may be evaluated
+-- multiple times (backtracking, PREV/NEXT navigation), so a volatile
+-- result would make pattern matching non-deterministic.  STABLE and
+-- IMMUTABLE callees are accepted.
 -- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
--- This locks the boundary of the volatile-only prohibition.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -1446,7 +1435,7 @@ ORDER BY id;
  10 |  45 |   0
 (10 rows)
 
--- Volatile (random) is the prohibition target; today still accepted.
+-- Volatile (random) is rejected.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -1454,20 +1443,9 @@ WINDOW w AS (ORDER BY id
     PATTERN (A B+)
     DEFINE B AS val > PREV(val) AND random() >= 0.0)
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   2
-  2 |  20 |   0
-  3 |  15 |   2
-  4 |  25 |   0
-  5 |   5 |   3
-  6 |  30 |   0
-  7 |  35 |   0
-  8 |  20 |   3
-  9 |  40 |   0
- 10 |  45 |   0
-(10 rows)
-
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 6:     DEFINE B AS val > PREV(val) AND random() >= 0.0)
+                                            ^
 -- ============================================================
 -- B10. RPR + Correlated subquery in WHERE
 -- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 5563e062cde..e4790f75b0a 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -541,6 +541,26 @@ WINDOW w AS (
     DEFINE A AS PREV(price, price) > 0
 );
 
+-- Non-constant offset: column reference in compound inner offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, price), 2) > 0
+);
+
+-- Non-constant offset: column reference in compound outer offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, 1), price) > 0
+);
+
 -- Non-constant offset: volatile function as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -571,7 +591,7 @@ WINDOW w AS (
     DEFINE A AS PREV(price + (SELECT 1)) > 0
 );
 
--- First arg: volatile function is allowed (evaluated on target row)
+-- Volatile function inside nav.arg is rejected at parse time
 SELECT company, tdate, price,
        first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
 FROM stock
@@ -582,6 +602,19 @@ WINDOW w AS (
     DEFINE A AS PREV(price + random() * 0) >= 0
 );
 
+-- nextval is volatile (per pg_proc), so it is rejected via the FuncExpr
+-- path with the "volatile functions" message
+CREATE SEQUENCE rpr_seq;
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > nextval('rpr_seq')
+);
+DROP SEQUENCE rpr_seq;
+
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index a3789e92631..e123be60aea 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2699,9 +2699,8 @@ WINDOW w AS (
     DEFINE A AS PREV(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
 );
 
--- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
--- N + M overflows int64, but target is forward from match_start so it never
--- constrains trim.  Lookahead remains at default (0).
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> infinite
+-- N + M overflows int64; forward reach is unbounded, displayed as infinite.
 EXPLAIN (COSTS OFF) SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
 WINDOW w AS (
@@ -2728,7 +2727,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
 RESET plan_cache_mode;
 DEALLOCATE test_overflow_lookback;
 
--- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> infinite
 PREPARE test_overflow_lookahead(int8, int8) AS
 SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index d9748979d54..29b2db2f7bb 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -868,24 +868,13 @@ DROP INDEX rpr_integ_id_idx;
 -- ============================================================
 -- B9. RPR + Volatile function in DEFINE
 -- ============================================================
--- Records the current behaviour: DEFINE today accepts volatile
--- functions such as random() and the query runs to completion.
--- To keep the expected output deterministic the predicate uses
--- "random() >= 0.0", which is structurally equivalent to TRUE and
--- therefore does not perturb the match result.  The interesting
--- property is that volatile invocation does not crash or short-
--- circuit pattern matching.
---
--- XXX: volatile functions in DEFINE are slated to be rejected at
--- parse time.  Under RPR's NFA engine the same row's DEFINE
--- predicate may be evaluated multiple times (backtracking,
--- PREV/NEXT navigation), so a truly volatile result would make
--- pattern matching non-deterministic.  When the prohibition lands,
--- this test must be replaced with an error-case test that expects
--- random() in DEFINE to be rejected.
+-- Volatile functions in DEFINE are rejected at parse time.  Under
+-- RPR's NFA engine the same row's DEFINE predicate may be evaluated
+-- multiple times (backtracking, PREV/NEXT navigation), so a volatile
+-- result would make pattern matching non-deterministic.  STABLE and
+-- IMMUTABLE callees are accepted.
 
 -- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
--- This locks the boundary of the volatile-only prohibition.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -896,7 +885,7 @@ WINDOW w AS (ORDER BY id
                 AND to_char(date '2026-01-01', 'YYYY') = '2026')
 ORDER BY id;
 
--- Volatile (random) is the prohibition target; today still accepted.
+-- Volatile (random) is rejected.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4b8225cbf3a..855913f03b7 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -663,7 +663,10 @@ DecodingWorkerShared
 DefElem
 DefElemAction
 DefaultACLInfo
+DefineMetadataContext
+DefinePhase
 DefineStmt
+DefineWalkCtx
 DefnDumperPtr
 DeleteStmt
 DependenciesParseState
@@ -761,8 +764,7 @@ ErrorData
 ErrorSaveContext
 EstimateDSMForeignScan_function
 EstimationInfo
-EvalNavFirstContext
-EvalNavMaxContext
+EvalDefineOffsetsContext
 EventTriggerCacheEntry
 EventTriggerCacheItem
 EventTriggerCacheStateType
@@ -1822,8 +1824,8 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
-NavCheckResult
-NavOffsetContext
+NavTraversal
+NavVisitFn
 NestLoop
 NestLoopParam
 NestLoopState
-- 
2.50.1 (Apple Git-155)

From 138f08ab9b44002893678b2ad1c9b9700de5f3ca Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 14:29:14 +0900
Subject: [PATCH 3/9] Cover RPR empty-match path with EXPLAIN tests; fix stale
 XXX comments

The test_728_* cases in rpr_nfa.sql claimed (via XXX) that the visited
bitmap blocks empty iterations.  In fact the NFA finds the empty
matches; window aggregates just return 0 / NULL over the length-0
frame, indistinguishable from "no match" without MATCH_NUMBER().

Replace the XXX comments with the actual behavior, and add paired
EXPLAIN ANALYZE tests in rpr_explain.sql that lock in
"NFA: 3 matched (len 0/0/0.0)" as observable regression coverage.
---
 src/test/regress/expected/rpr_explain.out | 200 ++++++++++++++++++++++
 src/test/regress/expected/rpr_nfa.out     |  29 ++--
 src/test/regress/sql/rpr_explain.sql      | 116 +++++++++++++
 src/test/regress/sql/rpr_nfa.sql          |  29 ++--
 4 files changed, 340 insertions(+), 34 deletions(-)

diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 0a049d1beba..c4516d3c756 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2102,6 +2102,206 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=0.00 loops=1)
 (5 rows)
 
+-- Empty matches (length 0): mirror the test_728_* cases in rpr_nfa.sql.
+-- Window aggregates over a length-0 frame return 0 / NULL, so the SELECT
+-- result alone cannot distinguish "no match" from "empty match"; the
+-- "NFA: N matched (len 0/0/0.0)" line in EXPLAIN is the only observable
+-- proof that the empty matches were found.
+-- (A?){0,3}: min=0, A never matches -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min0 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min0'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){0,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){0,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 20 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){1,3}: min=1, one empty iteration satisfies min -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min1'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){1,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){1,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 16 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min2'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 16 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){2,3} mixed: rows 1-2 match A (real), rows 3-4 fall back to empty
+CREATE VIEW rpr_ev_edge_empty_match_mixed AS
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_mixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=4.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 8 peak, 26 total, 5 merged
+   NFA Contexts: 4 peak, 5 total, 1 pruned
+   NFA: 3 matched (len 0/2/1.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=4.00 loops=1)
+(9 rows)
+
+-- (A? B?){2,3}: pure empty multi-element body -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_multi AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN ((a? b?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a? b?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 20 total, 0 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
 -- Single row
 CREATE VIEW rpr_ev_edge_single_row AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index a19b26c3b94..4cff7cfbbd7 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -4395,9 +4395,9 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){0,3}: min=0, nullable inner.
--- A never matches. A? matches empty, min=0 satisfied immediately.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches but A? matches empty, satisfying min=0 immediately.
+-- NFA reports 3 length-0 matches (one per row); first_value / last_value
+-- are NULL because the window frame for an empty match has no rows.
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4425,9 +4425,8 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){1,3}: min=1, nullable inner.
--- A never matches. Need 1 empty iteration to satisfy min=1.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches; one empty iteration satisfies min=1.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4454,11 +4453,9 @@ WINDOW w AS (
   3 | {B}   |             |          
 (3 rows)
 
--- (A?){2,3}: min=2, nullable inner.
--- A never matches. Need 2 empty iterations to satisfy min=2.
--- Per standard: STR06=(STRE STRE) is valid for min=2.
--- Expected: empty match for every row
--- XXX: visited bitmap blocks second empty iteration -> match failure
+-- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- is valid: two empty iterations satisfy min=2.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4486,9 +4483,8 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){2,3} mixed: some rows match A, some don't
--- Rows 1-2: A matches, greedy takes 2 -> min satisfied
--- Row 3: A doesn't match, needs 2 empty iterations for min=2
--- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
+-- Rows 1-2: A matches, greedy takes 2 -> min satisfied (real match)
+-- Row 3: A doesn't match, two empty iterations satisfy min=2 (length-0 match)
 -- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
@@ -4559,9 +4555,8 @@ WINDOW w AS (
   6 | {B}   |           6 |         6
 (6 rows)
 
--- (A? B?){2,3}: pure empty body (nothing matches)
--- XXX: All NULL: same issue as test_728_min2 (empty match at context
--- start yields UNMATCHED via startPos-1 initial advance)
+-- (A? B?){2,3}: pure empty body (nothing matches A or B).
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_multi_empty AS (
     SELECT * FROM (VALUES
         (1, ARRAY['C']),
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index e123be60aea..d339a80a673 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -1178,6 +1178,122 @@ WINDOW w AS (
     DEFINE A AS v = 1, B AS v = 2
 );');
 
+-- Empty matches (length 0): mirror the test_728_* cases in rpr_nfa.sql.
+-- Window aggregates over a length-0 frame return 0 / NULL, so the SELECT
+-- result alone cannot distinguish "no match" from "empty match"; the
+-- "NFA: N matched (len 0/0/0.0)" line in EXPLAIN is the only observable
+-- proof that the empty matches were found.
+
+-- (A?){0,3}: min=0, A never matches -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min0 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min0'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){1,3}: min=1, one empty iteration satisfies min -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min1'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min2'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){2,3} mixed: rows 1-2 match A (real), rows 3-4 fall back to empty
+CREATE VIEW rpr_ev_edge_empty_match_mixed AS
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_mixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);');
+
+-- (A? B?){2,3}: pure empty multi-element body -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_multi AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);');
+
 -- Single row
 CREATE VIEW rpr_ev_edge_single_row AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 1d27e0dc09e..29ec4a9dacb 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -3234,9 +3234,9 @@ WINDOW w AS (
 );
 
 -- (A?){0,3}: min=0, nullable inner.
--- A never matches. A? matches empty, min=0 satisfied immediately.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches but A? matches empty, satisfying min=0 immediately.
+-- NFA reports 3 length-0 matches (one per row); first_value / last_value
+-- are NULL because the window frame for an empty match has no rows.
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3258,9 +3258,8 @@ WINDOW w AS (
 );
 
 -- (A?){1,3}: min=1, nullable inner.
--- A never matches. Need 1 empty iteration to satisfy min=1.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches; one empty iteration satisfies min=1.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3281,11 +3280,9 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- (A?){2,3}: min=2, nullable inner.
--- A never matches. Need 2 empty iterations to satisfy min=2.
--- Per standard: STR06=(STRE STRE) is valid for min=2.
--- Expected: empty match for every row
--- XXX: visited bitmap blocks second empty iteration -> match failure
+-- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- is valid: two empty iterations satisfy min=2.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3307,9 +3304,8 @@ WINDOW w AS (
 );
 
 -- (A?){2,3} mixed: some rows match A, some don't
--- Rows 1-2: A matches, greedy takes 2 -> min satisfied
--- Row 3: A doesn't match, needs 2 empty iterations for min=2
--- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
+-- Rows 1-2: A matches, greedy takes 2 -> min satisfied (real match)
+-- Row 3: A doesn't match, two empty iterations satisfy min=2 (length-0 match)
 -- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
@@ -3364,9 +3360,8 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
--- (A? B?){2,3}: pure empty body (nothing matches)
--- XXX: All NULL: same issue as test_728_min2 (empty match at context
--- start yields UNMATCHED via startPos-1 initial advance)
+-- (A? B?){2,3}: pure empty body (nothing matches A or B).
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_multi_empty AS (
     SELECT * FROM (VALUES
         (1, ARRAY['C']),
-- 
2.50.1 (Apple Git-155)

From e87dd318dd4a9d50ec85ef02c689d3b943e17c45 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 11:36:53 +0900
Subject: [PATCH 4/9] Reclassify DEFINE qualifier check and reword diagnostic
 to "expression"

Split the pre-check into three branches: pattern variable ->
FEATURE_NOT_SUPPORTED, range variable (via refnameNamespaceItem) ->
SYNTAX_ERROR, anything else -> fall through to normal resolution.
Reword "qualified column reference" to "qualified expression" since
the quoted token may include an indirection target on composite
types (e.g. (A.items).amount).
---
 src/backend/parser/parse_expr.c        | 31 +++++++---
 src/test/regress/expected/rpr_base.out | 80 +++++++++++++++++++++++---
 src/test/regress/sql/rpr_base.sql      | 61 ++++++++++++++++++--
 3 files changed, 153 insertions(+), 19 deletions(-)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index f145342e1fb..9997d6bc4c5 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -628,11 +628,24 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	if (node != NULL)
 		return node;
 
-	/*
-	 * Qualified column references in DEFINE are not supported.  This covers
-	 * both FROM-clause range variables (prohibited by §6.5) and pattern
-	 * variable qualified names (e.g. UP.price), which are valid per §4.16
-	 * but not yet implemented.
+	/*----------
+	 * Qualified references in DEFINE need a tri-classification:
+	 *
+	 *	  pattern variable qualifier (e.g. UP.price): valid per §4.16 but
+	 *	  not yet implemented -- raise FEATURE_NOT_SUPPORTED.
+	 *
+	 *	  FROM-clause range variable qualifier: prohibited by §6.5 -- raise
+	 *	  SYNTAX_ERROR.
+	 *
+	 *	  any other qualifier (typo, undefined name): fall through and let
+	 *	  normal column resolution produce a sensible error.
+	 *
+	 * The quoted text reflects only the ColumnRef portion; a trailing field
+	 * selection on a composite type (e.g. ".amount" in "(A.items).amount")
+	 * lives in the surrounding A_Indirection node and is not included here.
+	 * That can be revisited when MEASURES support adds indirection-aware
+	 * traversal.
+	 *----------
 	 */
 	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
 		list_length(cref->fields) != 1)
@@ -653,15 +666,17 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		if (is_pattern_var)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("pattern variable qualified column reference \"%s\" is not supported in DEFINE clause",
+					 errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
 							NameListToString(cref->fields)),
 					 parser_errposition(pstate, cref->location)));
-		else
+		else if (refnameNamespaceItem(pstate, NULL, qualifier,
+									  cref->location, NULL) != NULL)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("range variable qualified column reference \"%s\" is not allowed in DEFINE clause",
+					 errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
 							NameListToString(cref->fields)),
 					 parser_errposition(pstate, cref->location)));
+		/* else: unknown qualifier -- fall through to normal resolution */
 	}
 
 	/*----------
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index a63211ff364..86abb96c177 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3074,10 +3074,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
-ERROR:  pattern variable qualified column reference "a.val" is not supported in DEFINE clause
+ERROR:  pattern variable qualified expression "a.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS A.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified column reference "a.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3087,10 +3087,10 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
-ERROR:  pattern variable qualified column reference "b.val" is not supported in DEFINE clause
+ERROR:  pattern variable qualified expression "b.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS B.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3103,7 +3103,7 @@ WINDOW w AS (
 ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3113,10 +3113,76 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
-ERROR:  range variable qualified column reference "rpr_err.val" is not allowed in DEFINE clause
+ERROR:  range variable qualified expression "rpr_err.val" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS rpr_err.val > 0
                         ^
--- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
+-- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
+-- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
+-- must fall through so that normal column resolution produces a sensible error.
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS nosuch.val > 0
+);
+ERROR:  missing FROM-clause entry for table "nosuch"
+LINE 7:     DEFINE A AS nosuch.val > 0
+                        ^
+-- Expected: ERROR: missing FROM-clause entry for table "nosuch"
+-- Unqualified composite field access in DEFINE works: no qualifier means no
+-- pattern/range-var navigation, so the pre-check skips and normal resolution
+-- handles "(items).amount" via A_Indirection on the current row.
+CREATE TYPE rpr_item AS (name TEXT, amount INT);
+CREATE TEMP TABLE rpr_composite (id int, items rpr_item);
+INSERT INTO rpr_composite VALUES (1, ROW('a',5)), (2, ROW('b',15)), (3, ROW('c',25));
+SELECT id, (items).amount, COUNT(*) OVER w AS cnt
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS (items).amount > 10
+);
+ id | amount | cnt 
+----+--------+-----
+  1 |      5 |   0
+  2 |     15 |   2
+  3 |     25 |   1
+(3 rows)
+
+-- Expected: rows where (items).amount > 10 form matches; counts reflect frame size
+-- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or
+-- "rpr_composite.items") is what gets quoted; the trailing ".amount" lives in
+-- the surrounding A_Indirection node and is not visible to the pre-check.
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (A.items).amount > 10
+);
+ERROR:  pattern variable qualified expression "a.items" is not supported in DEFINE clause
+LINE 7:     DEFINE A AS (A.items).amount > 10
+                         ^
+-- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (rpr_composite.items).amount > 10
+);
+ERROR:  range variable qualified expression "rpr_composite.items" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS (rpr_composite.items).amount > 10
+                         ^
+-- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
+DROP TABLE rpr_composite;
+DROP TYPE rpr_item;
 -- Semantic errors
 -- Undefined column in DEFINE
 SELECT COUNT(*) OVER w
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 86ed06fec68..e8c72706720 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2092,7 +2092,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "a.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
@@ -2103,7 +2103,7 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
@@ -2114,7 +2114,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0, B AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
 SELECT COUNT(*) OVER w
@@ -2125,7 +2125,60 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
--- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
+-- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
+
+-- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
+-- must fall through so that normal column resolution produces a sensible error.
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS nosuch.val > 0
+);
+-- Expected: ERROR: missing FROM-clause entry for table "nosuch"
+
+-- Unqualified composite field access in DEFINE works: no qualifier means no
+-- pattern/range-var navigation, so the pre-check skips and normal resolution
+-- handles "(items).amount" via A_Indirection on the current row.
+CREATE TYPE rpr_item AS (name TEXT, amount INT);
+CREATE TEMP TABLE rpr_composite (id int, items rpr_item);
+INSERT INTO rpr_composite VALUES (1, ROW('a',5)), (2, ROW('b',15)), (3, ROW('c',25));
+SELECT id, (items).amount, COUNT(*) OVER w AS cnt
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS (items).amount > 10
+);
+-- Expected: rows where (items).amount > 10 form matches; counts reflect frame size
+
+-- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or
+-- "rpr_composite.items") is what gets quoted; the trailing ".amount" lives in
+-- the surrounding A_Indirection node and is not visible to the pre-check.
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (A.items).amount > 10
+);
+-- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (rpr_composite.items).amount > 10
+);
+-- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
+DROP TABLE rpr_composite;
+DROP TYPE rpr_item;
 
 -- Semantic errors
 
-- 
2.50.1 (Apple Git-155)

From 06c493668b65335c8cee8b2f58836d46ad57215f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 11:44:45 +0900
Subject: [PATCH 5/9] Sync stale comments on DEFINE/PATTERN handling

validateRPRPatternVarCount() validates DEFINE names against the
PATTERN list and rejects any not used in PATTERN; the surrounding
comments said it "collected" them.  Align the function header,
inline block comment, and the call-site comment.  Also fix the
matching SELECT doc ("filtered during planning" -> "rejected with
an error") and the XXX note's wording.
---
 doc/src/sgml/ref/select.sgml   |  4 ++--
 src/backend/parser/parse_rpr.c | 28 ++++++++++++++--------------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 5272d6c0bfa..e4708331439 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1192,8 +1192,8 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
 </synopsis>
 
     Conversely, variables defined in the <literal>DEFINE</literal> clause
-    but not used in the <literal>PATTERN</literal> clause are filtered out
-    during query planning.
+    but not used in the <literal>PATTERN</literal> clause are rejected
+    with an error.
    </para>
 
    <para>
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 32701f9b74b..9ed12c7ec61 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -183,11 +183,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  * Recursively traverses the pattern tree, collecting unique variable names.
  * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
  *
- * If rpDefs is non-NULL, DEFINE variable names are also collected into
- * varNames so that transformColumnRef can distinguish pattern variable
- * qualifiers from FROM-clause range variables.
- *
- * varNames is both input and output: existing names are preserved, new ones added.
+ * 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.
  */
 static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
@@ -244,10 +244,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	}
 
 	/*
-	 * After the top-level call, also collect DEFINE variable names that are
-	 * not already in the list.  This is only done once at the outermost
-	 * recursion level, detected by rpDefs being non-NULL (recursive calls
-	 * pass NULL).
+	 * 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)
 	{
@@ -293,9 +293,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
  * Variables in DEFINE but not in PATTERN are rejected as an error.
  *
- * XXX Pattern variable qualified column references in DEFINE (e.g.
- * "A.price") are not yet supported.  Currently rejected by
- * transformColumnRef in parse_expr.c via the p_rpr_pattern_vars check.
+ * XXX Pattern variable qualified expressions in DEFINE (e.g. "A.price")
+ * are not yet supported.  Currently rejected by transformColumnRef in
+ * parse_expr.c via the p_rpr_pattern_vars check.
  */
 static List *
 transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
@@ -317,8 +317,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	Assert(windef->rpCommonSyntax->rpDefs != NULL);
 
 	/*
-	 * Validate PATTERN variable count and collect all RPR variable names
-	 * (PATTERN + DEFINE) for use in transformColumnRef.
+	 * Validate PATTERN variable count, reject DEFINE variables not used in
+	 * PATTERN, and collect PATTERN variable names for transformColumnRef.
 	 */
 	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
 							   windef->rpCommonSyntax->rpDefs,
-- 
2.50.1 (Apple Git-155)

From 9a9dcd1b51867185331092b941c467f93549833b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 17:21:56 +0900
Subject: [PATCH 6/9] Add trailing commas to RPR enum definitions

RPRNavKind, RPRNavOffsetKind, RPRPatternNodeType lacked a trailing
comma on the last enumerator, contrary to project coding style.
---
 src/include/nodes/parsenodes.h | 4 ++--
 src/include/nodes/primnodes.h  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 455db2cec61..adefb1d5bad 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -604,7 +604,7 @@ typedef enum RPRNavOffsetKind
 	RPR_NAV_OFFSET_FIXED,		/* resolved constant; use the offset value */
 	RPR_NAV_OFFSET_NEEDS_EVAL,	/* non-constant offset; evaluate at executor
 								 * init */
-	RPR_NAV_OFFSET_RETAIN_ALL	/* cannot determine; retain all rows (no trim) */
+	RPR_NAV_OFFSET_RETAIN_ALL,	/* cannot determine; retain all rows (no trim) */
 } RPRNavOffsetKind;
 
 /*
@@ -615,7 +615,7 @@ typedef enum RPRPatternNodeType
 	RPR_PATTERN_VAR,			/* variable reference */
 	RPR_PATTERN_SEQ,			/* sequence (concatenation) */
 	RPR_PATTERN_ALT,			/* alternation (|) */
-	RPR_PATTERN_GROUP			/* group (parentheses) */
+	RPR_PATTERN_GROUP,			/* group (parentheses) */
 } RPRPatternNodeType;
 
 /*
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 656c552b0a8..d7138f5141b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -684,7 +684,7 @@ typedef enum RPRNavKind
 	RPR_NAV_PREV_FIRST,
 	RPR_NAV_PREV_LAST,
 	RPR_NAV_NEXT_FIRST,
-	RPR_NAV_NEXT_LAST
+	RPR_NAV_NEXT_LAST,
 } RPRNavKind;

From ed480a165f5d54ed6a094c8bbd77f52dc9980f7e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 17:27:51 +0900
Subject: [PATCH 7/9] Remove optional outer parentheses from ereport() calls in
 RPR files

Per project coding style, the outer parentheses wrapping errcode/
errmsg/etc. arguments in ereport() are optional and should be omitted.
Applies to parse_rpr.c, optimizer/plan/rpr.c, and gram.y.
---
 src/backend/optimizer/plan/rpr.c |  16 ++--
 src/backend/parser/gram.y        |  72 ++++++++--------
 src/backend/parser/parse_rpr.c   | 140 +++++++++++++++----------------
 3 files changed, 114 insertions(+), 114 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index a817eb4a63f..ed8b6c3414c 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1027,10 +1027,10 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 	/* Check recursion depth limit before overflow occurs */
 	if (depth >= RPR_DEPTH_MAX)
 		ereport(ERROR,
-				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-				 errmsg("pattern nesting too deep"),
-				 errdetail("Pattern nesting depth %d exceeds maximum %d.",
-						   depth, RPR_DEPTH_MAX - 1)));
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("pattern nesting too deep"),
+				errdetail("Pattern nesting depth %d exceeds maximum %d.",
+						  depth, RPR_DEPTH_MAX - 1));
 
 	/* Track maximum depth */
 	if (depth > *maxDepth)
@@ -1120,10 +1120,10 @@ scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
 
 	if (*numElements > RPR_ELEMIDX_MAX)
 		ereport(ERROR,
-				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-				 errmsg("pattern too complex"),
-				 errdetail("Pattern has %d elements, maximum is %d.",
-						   *numElements, RPR_ELEMIDX_MAX)));
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("pattern too complex"),
+				errdetail("Pattern has %d elements, maximum is %d.",
+						  *numElements, RPR_ELEMIDX_MAX));
 }
 
 /*
index 9ed12c7ec61..c9f9bb3ad31 100644
@@ -95,20 +95,20 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
-				 errhint("Use ROWS instead."),
-				 parser_errposition(pstate,
-									windef->frameLocation >= 0 ?
-									windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+								   windef->frameLocation : windef->location));
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use FRAME option RANGE with row pattern recognition"),
-				 errhint("Use ROWS instead."),
-				 parser_errposition(pstate,
-									windef->frameLocation >= 0 ?
-									windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+								   windef->frameLocation : windef->location));
 
 	/* Frame must start at current row */
 	if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
@@ -130,11 +130,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			   (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING));
 
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
-				 errdetail("Current frame starts with %s.", startBound),
-				 errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
-				 parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
+				errdetail("Current frame starts with %s.", startBound),
+				errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
+				parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location));
 	}
 
 	/* EXCLUDE options are not permitted */
@@ -156,11 +156,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			   (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES));
 
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use EXCLUDE options with row pattern recognition"),
-				 errdetail("Frame definition includes %s.", excludeType),
-				 errhint("Remove the EXCLUDE clause from the window definition."),
-				 parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use EXCLUDE options with row pattern recognition"),
+				errdetail("Frame definition includes %s.", excludeType),
+				errhint("Remove the EXCLUDE clause from the window definition."),
+				parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location));
 	}
 
 	/* Transform AFTER MATCH SKIP TO clause */
@@ -220,11 +220,11 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 					/* Check against RPR_VARID_MAX before adding */
 					if (list_length(*varNames) >= RPR_VARID_MAX)
 						ereport(ERROR,
-								(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-								 errmsg("too many pattern variables"),
-								 errdetail("Maximum is %d.", RPR_VARID_MAX),
-								 parser_errposition(pstate,
-													exprLocation((Node *) node))));
+								errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+								errmsg("too many pattern variables"),
+								errdetail("Maximum is %d.", RPR_VARID_MAX),
+								parser_errposition(pstate,
+												   exprLocation((Node *) node)));
 
 					*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
 				}
@@ -267,10 +267,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			}
 			if (!found)
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("DEFINE variable \"%s\" is not used in PATTERN",
-								rt->name),
-						 parser_errposition(pstate, rt->location)));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+							   rt->name),
+						parser_errposition(pstate, rt->location));
 		}
 	}
 }
@@ -347,10 +347,10 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 			if (!strcmp(n, name))
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("DEFINE variable \"%s\" appears more than once",
-								name),
-						 parser_errposition(pstate, exprLocation((Node *) r))));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("DEFINE variable \"%s\" appears more than once",
+							   name),
+						parser_errposition(pstate, exprLocation((Node *) r)));
 		}
 
 		restargets = lappend(restargets, restarget);
@@ -529,14 +529,14 @@ define_walker(Node *node, void *context)
 	 */
 	if (check_functions_in_node(node, nav_volatile_func_checker, NULL))
 		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("volatile functions are not allowed in DEFINE clause"),
-				 parser_errposition(ctx->pstate, exprLocation(node))));
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("volatile functions are not allowed in DEFINE clause"),
+				parser_errposition(ctx->pstate, exprLocation(node)));
 	if (IsA(node, NextValueExpr))
 		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("sequence operations are not allowed in DEFINE clause"),
-				 parser_errposition(ctx->pstate, exprLocation(node))));
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("sequence operations are not allowed in DEFINE clause"),
+				parser_errposition(ctx->pstate, exprLocation(node)));
 
 	/* Var sighting feeds the column-ref rule for the enclosing nav scope. */
 	if (IsA(node, Var) &&
@@ -600,17 +600,17 @@ define_walker(Node *node, void *context)
 					/* Reject triple-or-deeper nesting */
 					if (ctx->nav_count > 1)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("cannot nest row pattern navigation more than two levels deep"),
-								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-								 parser_errposition(ctx->pstate, nav->location)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("cannot nest row pattern navigation more than two levels deep"),
+								errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								parser_errposition(ctx->pstate, nav->location));
 
 					if (!IsA(nav->arg, RPRNavExpr))
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
-								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-								 parser_errposition(ctx->pstate, nav->location)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+								errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								parser_errposition(ctx->pstate, nav->location));
 
 					inner = (RPRNavExpr *) nav->arg;
 
@@ -630,29 +630,29 @@ define_walker(Node *node, void *context)
 				}
 				else if (!outer_phys && inner_phys)
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 				else if (outer_phys && inner_phys)
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 				else
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 			}
 			else if (!ctx->has_column_ref)
 			{
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("argument of row pattern navigation operation must include at least one column reference"),
-						 parser_errposition(ctx->pstate, nav->location)));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("argument of row pattern navigation operation must include at least one column reference"),
+						parser_errposition(ctx->pstate, nav->location));
 			}
 
 			/*
@@ -673,9 +673,9 @@ define_walker(Node *node, void *context)
 				(void) define_walker((Node *) nav->offset_arg, ctx);
 				if (ctx->has_column_ref)
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("row pattern navigation offset must be a run-time constant"),
-							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg))));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("row pattern navigation offset must be a run-time constant"),
+							parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg)));
 			}
 			if (flattened && nav->compound_offset_arg != NULL)
 			{
@@ -683,9 +683,9 @@ define_walker(Node *node, void *context)
 				(void) define_walker((Node *) nav->compound_offset_arg, ctx);
 				if (ctx->has_column_ref)
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("row pattern navigation offset must be a run-time constant"),
-							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg))));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("row pattern navigation offset must be a run-time constant"),
+							parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg)));
 			}
 
 			*ctx = saved;
-- 
2.50.1 (Apple Git-155)

From 8666c965e3152f5d6c3bb9438e5bb3250e700b53 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 22:31:06 +0900
Subject: [PATCH 8/9] Add high-water mark tracking to NFA visited bitmap reset

Track the min/max bitmapword index touched in nfa_mark_visited so the
per-advance reset memsets only the touched range, not the full
nfaVisitedNWords array.

Each visited mark performs two int16 comparisons.  For single-word
bitmaps this overhead is added with no reset saving; for larger NFAs
the reset walks only the slice the DFS actually touched, which is
where the win comes from.  Applied unconditionally for simplicity.
Semantics unchanged.
---
 src/backend/executor/execRPR.c       | 41 +++++++++++++++++++++++-----
 src/backend/executor/nodeWindowAgg.c |  3 ++
 src/include/nodes/execnodes.h        |  4 +++
 3 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 242ae9c6dcf..1e6196d6960 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -38,6 +38,21 @@
 #define WORDNUM(x)	((x) / BITS_PER_BITMAPWORD)
 #define BITNUM(x)	((x) % BITS_PER_BITMAPWORD)
 
+/*
+ * Set the visited bit for elemIdx and update the high-water marks
+ * (nfaVisitedMin/MaxWord) so that the next reset only has to clear
+ * the touched range instead of the full nfaVisitedNWords array.
+ */
+static inline void
+nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
+{
+	int16		w = WORDNUM(elemIdx);
+
+	winstate->nfaVisitedElems[w] |= ((bitmapword) 1 << BITNUM(elemIdx));
+	winstate->nfaVisitedMinWord = Min(winstate->nfaVisitedMinWord, w);
+	winstate->nfaVisitedMaxWord = Max(winstate->nfaVisitedMaxWord, w);
+}
+
 /* Forward declarations - NFA state management */
 static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
@@ -320,8 +335,7 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	RPRNFAState *tail = NULL;
 
 	/* Mark VAR in visited before duplicate check to prevent DFS loops */
-	winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
-		((bitmapword) 1 << BITNUM(state->elemIdx));
+	nfa_mark_visited(winstate, state->elemIdx);
 
 	/* Check for duplicate and find tail */
 	for (s = ctx->states; s != NULL; s = s->next)
@@ -1341,8 +1355,7 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
 	 * the same VAR in a new iteration.
 	 */
 	if (!RPRElemIsVar(elem))
-		winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
-			((bitmapword) 1 << BITNUM(state->elemIdx));
+		nfa_mark_visited(winstate, state->elemIdx);
 
 	switch (elem->varId)
 	{
@@ -1394,9 +1407,23 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 		CHECK_FOR_INTERRUPTS();
 		savedMatchedState = ctx->matchedState;
 
-		/* Clear visited bitmap before each state's DFS expansion */
-		memset(winstate->nfaVisitedElems, 0,
-			   sizeof(bitmapword) * winstate->nfaVisitedNWords);
+		/*
+		 * Clear visited bitmap before each state's DFS expansion.  Only the
+		 * range touched since the previous reset (tracked via the high-water
+		 * marks updated in nfa_mark_visited) needs to be cleared; for small
+		 * NFAs this is the whole array, but for large NFAs whose DFS only
+		 * reaches a few elements per advance it avoids walking the full
+		 * bitmap.
+		 */
+		if (winstate->nfaVisitedMaxWord >= winstate->nfaVisitedMinWord)
+		{
+			memset(&winstate->nfaVisitedElems[winstate->nfaVisitedMinWord], 0,
+				   sizeof(bitmapword) *
+				   (winstate->nfaVisitedMaxWord -
+					winstate->nfaVisitedMinWord + 1));
+			winstate->nfaVisitedMinWord = INT16_MAX;
+			winstate->nfaVisitedMaxWord = -1;
+		}
 
 		state = states;
 		states = states->next;
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index af2351bccb8..d82ad8d3897 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3054,6 +3054,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 			(node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1;
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
 											winstate->nfaVisitedNWords);
+		/* High-water mark sentinels: no bits set yet. */
+		winstate->nfaVisitedMinWord = INT16_MAX;
+		winstate->nfaVisitedMaxWord = -1;
 	}
 
 	/* Set up row pattern recognition DEFINE clause */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0cb01baa949..1fba14b892e 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2673,6 +2673,10 @@ typedef struct WindowAggState
 									 * detection */
 	int			nfaVisitedNWords;	/* number of bitmapwords in
 									 * nfaVisitedElems */
+	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
+									 * last reset (INT16_MAX = none) */
+	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
+									 * last reset (-1 = none) */
 	int64		nfaLastProcessedRow;	/* last row processed by NFA (-1 =
 										 * none) */
 
-- 
2.50.1 (Apple Git-155)

From a5239a3c0dd9cf79506039368c6b5bcfc4c3a16e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:00:48 +0900
Subject: [PATCH 9/9] Document DEFINE subquery rejection as intentional
 over-rejection
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The SubLink switch in transformSubLink() rejects every subquery in
EXPR_KIND_RPR_DEFINE with no inline rationale.  Add an XXX block
recording that SQL/RPR (ISO/IEC 9075-2 §4.18.4 / 6.17.4; R010 /
R020) actually permits a nested subquery in DEFINE provided it
does not itself perform row pattern recognition and does not
reference an outer pattern variable, and that the blanket
rejection here subsumes both restrictions by making the subquery
itself unreachable.

Implementing the case distinction would mean walking the analyzed
subquery Query tree for nested RPR and walking it for ColumnRef
qualifiers matching any ancestor's p_rpr_pattern_vars; both are
doable with the existing infrastructure and are left as future
work, not blocked on any other feature.  Comment-only change.
---
 src/backend/parser/parse_expr.c | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 9997d6bc4c5..399cf68b3e5 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1944,6 +1944,28 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use subquery in FOR PORTION OF expression");
 			break;
+
+			/*----------
+			 * XXX SQL/RPR (ISO/IEC 9075-2 §4.18.4 / 6.17.4; R010 / R020)
+			 * permits a subquery nested in a DEFINE expression provided
+			 * that:
+			 *   (a) the subquery does not itself perform row pattern
+			 *       recognition, and
+			 *   (b) the subquery does not reference a row pattern variable
+			 *       of the outer query.
+			 *
+			 * We reject all subqueries here for now.  Implementing the
+			 * case distinction would mean walking the analyzed subquery
+			 * Query tree for nested RPR window clauses to enforce (a),
+			 * and walking it for ColumnRef qualifiers matching any
+			 * ancestor's p_rpr_pattern_vars to enforce (b).  Both checks
+			 * are doable with the existing infrastructure -- they are
+			 * left as future work, not blocked on any other feature.
+			 * Until then this blanket rejection is intentional
+			 * over-rejection, not a standard fit; it subsumes both (a)
+			 * and (b) by making the subquery itself unreachable.
+			 *----------
+			 */
 		case EXPR_KIND_RPR_DEFINE:
 			err = _("cannot use subquery in DEFINE expression");
 			break;
-- 
2.50.1 (Apple Git-155)

From 49722e9fd37430733a84c39da688fe1aedb48a10 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:37:51 +0900
Subject: [PATCH 10/10] Remove duplicate #include in nodeWindowAgg.c

#include "common/int.h" was included twice; the first occurrence
was also misplaced between two catalog/* headers, breaking the
alphabetical grouping of system header includes.  Drop the
misplaced first occurrence; the second sits in the correct
alphabetical position between catalog/ and executor/ headers.
---
 src/backend/executor/nodeWindowAgg.c | 1 -
 1 file changed, 1 deletion(-)

index d82ad8d3897..2f87449a0e0 100644
@@ -36,7 +36,6 @@
 #include "common/int.h"

---

From: Henson Choi <[email-masked]>
Date: Sat, 9 May 2026 16:43:18 +0900

Hi Tatsuo,

Eleven incremental patches on top of v47.  Patches 0001-0010
are the same set I posted on 2026-05-07, with cosmetic touches
only.  Patch 0011 is new and tightens the consistency of how
this code base cites the SQL standard.

So far I have been pushing ahead narrowly on the feature
implementation itself.  I am aware I still have much to learn
about the PostgreSQL project's culture and conventions, and
your continued guidance would be very much appreciated.

Patch summary:

  - 0007 Remove optional outer parentheses from ereport() calls
         in RPR files
      Per Jian + your gram.y patch.  19 sites in parse_rpr.c /
      optimizer/plan/rpr.c plus the gram.y sites.

  - 0009 Document DEFINE subquery rejection as intentional
         over-rejection
      Comment-only.  Records that SQL/RPR permits a non-RPR,
      non-pattern-var-correlated subquery in DEFINE (see 0011
      for the normalized citation), and that our blanket
      rejection is deliberate over-rejection.  The case
      distinction (walk subquery Query for nested RPR; match
      ColumnRef qualifiers against ancestor p_rpr_pattern_vars)
      is doable with existing infrastructure and left as future
      work, not blocked on any other feature.

  - 0011 Normalize SQL/RPR standard references across code,
         comments, and tests
      Doc/comment/test-header hygiene.  Standardizes citations
      to ISO/IEC 19075-5 (SQL Technical Report Part 5, "Row
      pattern recognition in SQL"); earlier comments mixed bare
      "19075-5", "9075-2", "SQL standard", and "SQL:2016 STR06"
      forms.  Where Chapter 4 (FROM / R010) and Chapter 6
      (WINDOW / R020) describe parallel material, cites the
      Chapter 6 subclause first since this implementation
      targets R020.  Pins "STR06" to its source (Subclause
      7.2.8) in the rpr_nfa / rpr_explain commentary; replaces
      ad-hoc section / arrow glyphs with ASCII.  Adds a short
      normative-reference paragraph to executor/README.rpr so
      future patches inherit the policy.  Comments / docs /
      test headers only.

Still deferred:
  - B7 Recursive CTE XXX: pending community input on the
    ISO/IEC 19075-5 6.17.5 / 4.18.5 interpretation.

Best regards,
Henson
From aba6c56a6854e8f101846aa03e42204facc02485 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 19:09:34 +0900
Subject: [PATCH 01/11] Add DEFINE non-volatile baseline to rpr_integration B9

(e.g. contain_volatile_functions -> contain_mutable_functions); the

 

From 84cd98072184ec63bb2f79477f03bbe81c659b83 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 21:37:59 +0900
Subject: [PATCH 02/11] Unify RPR DEFINE walkers and reject volatile callees

 
 
 /*

 
 
 /*

 

 
 
-typedef struct
-{
 
 /*
 
 	ctx.winstate = winstate;

 
 
index 52205cc7159..c8ecaeea7cf 100644
 
 /*
 
 /*
 
 /*
- * nav_offset_walker
+ *			 - PREV(v, N), LAST(v, N)              -> N (default 1)
+ *			 - compound PREV_LAST(v, N, M)         -> N + M (overflow -> maxOverflow)
+ *			 - compound NEXT_LAST(v, N, M)         -> max(N - M, 0)
+ *			 - FIRST(v, N)                         -> N (default 0)
+ *			 - compound PREV_FIRST(v, N, M)        -> N - M (may be negative)
+ *			 - compound NEXT_FIRST(v, N, M)        -> N + M
 
-	if (node == NULL)
-		return false;
+	/*

 
-		/*

 
-		/*

-		/*

 
 /*
- * compute_nav_offsets
 
 	ctx.maxOffset = 0;

 
 
-/*

 	/*

 

 #include "miscadmin.h"

 	return result;
 }
+
+/*
+ * nav_traversal_walker
index f56b7db5bc8..87411abcbe2 100644
 
 #include "postgres.h"

 
 /*
  * transformRPR

 
 /*
- * check_rpr_nav_expr
 
+/*
+ * define_walker
+ *		  [2] for each outer RPRNavExpr (PHASE_BODY -> PHASE_NAV_ARG):
 
 	if (node == NULL)
 		return false;
 
-static void

 

-/*
 
+/*

 
 RESET plan_cache_mode;

 

 

--- Volatile (random) is the prohibition target; today still accepted.
+-- Volatile (random) is rejected.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8e889ab5e0f..d23b392800e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -663,7 +663,10 @@ DecodingWorkerShared
 DefElem
 DefElemAction
 DefaultACLInfo
+DefineMetadataContext
+DefinePhase
 DefineStmt
+DefineWalkCtx
 DefnDumperPtr
 DeleteStmt
 DependenciesParseState
@@ -762,8 +765,7 @@ ErrorData
 ErrorSaveContext
 EstimateDSMForeignScan_function
 EstimationInfo
-EvalNavFirstContext
-EvalNavMaxContext
+EvalDefineOffsetsContext
 EventTriggerCacheEntry
 EventTriggerCacheItem
 EventTriggerCacheStateType
@@ -1824,8 +1826,8 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
-NavCheckResult
-NavOffsetContext
+NavTraversal
+NavVisitFn
 NestLoop
 NestLoopParam
 NestLoopState
-- 
2.50.1 (Apple Git-155)

From 56ebb1ad70a152cb22d4adb60069b4625659421e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 14:29:14 +0900
Subject: [PATCH 03/11] Cover RPR empty-match path with EXPLAIN tests; fix
 stale XXX comments

 

 

 

 

From af9f50ed1522bda716d10edf932bc5aeb4b0cb37 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 11:36:53 +0900
Subject: [PATCH 04/11] Reclassify DEFINE qualifier check and reword diagnostic
 to "expression"

index f145342e1fb..69148328719 100644
 
-	/*
+	 *	  pattern variable qualifier (e.g. UP.price): valid per 19075-5 4.16
+	 *	  but not yet implemented -- raise FEATURE_NOT_SUPPORTED.
+	 *	  FROM-clause range variable qualifier: prohibited by 19075-5 6.5
+	 *	  -- raise SYNTAX_ERROR.
 
 	/*----------

 
 
 -- Semantic errors
 
-- 
2.50.1 (Apple Git-155)

From c3ccc037cc128f5a640a7357669db43f1dcdf6f9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 11:44:45 +0900
Subject: [PATCH 05/11] Sync stale comments on DEFINE/PATTERN handling

 
 
    <para>
index 87411abcbe2..2c6fccebd47 100644
 
 	/*
 
 	/*

From 1aba5a8b605023ea2993faf98869b7be2860fc53 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 17:21:56 +0900
Subject: [PATCH 06/11] Add trailing commas to RPR enum definitions

 
 /*
 
 /*

From 37cb8a6e9d36ac98b7cd10bb81a9a4f70720d016 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 17:27:51 +0900
Subject: [PATCH 07/11] Remove optional outer parentheses from ereport() calls
 in RPR files

 

 /*
index 2c6fccebd47..bba887f17ce 100644

 		ereport(ERROR,

 		ereport(ERROR,

 
 			if (!strcmp(n, name))
 				ereport(ERROR,

 

 
 			/*
 
 			*ctx = saved;
-- 
2.50.1 (Apple Git-155)

From 8f87a3952268ff3b4cb7fd8a09aff1df013e1d18 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 22:31:06 +0900
Subject: [PATCH 08/11] Add high-water mark tracking to NFA visited bitmap
 reset

 
+/*

 
 	switch (elem->varId)
 	{

 		state = states;
 		states = states->next;

-- 
2.50.1 (Apple Git-155)

From 26ff8122ea01c9208b8c7c48bca330192c18d08d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:00:48 +0900
Subject: [PATCH 09/11] Document DEFINE subquery rejection as intentional
 over-rejection

recording that SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 /

index 69148328719..58ebd7d24b8 100644
+			 * XXX SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 / R020)

From 0c65fed46bafe16ff228fe460d15f2fbdca45e3f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:37:51 +0900
Subject: [PATCH 10/11] Remove duplicate #include in nodeWindowAgg.c

From bd0f11a6a80004168ff0aef512da58d0f3fe137a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 9 May 2026 13:47:49 +0900
Subject: [PATCH 11/11] Normalize SQL/RPR standard references

Make every reference cite ISO/IEC 19075-5 explicitly across RPR code
and regress tests.  Prefix bare "19075-5" / "SQL standard" forms,
pin STR06 to its source (7.2.8), and where a clause is mirrored in
both Chapter 4 (FROM) and Chapter 6 (WINDOW), cite the Chapter 6
subclause first because this implementation targets Feature R020.
Document the citation policy in README.rpr.
---
 src/backend/executor/README.rpr               |  8 +++++++-
 src/backend/executor/execRPR.c                |  5 +++--
 src/backend/optimizer/plan/rpr.c              |  8 ++++----
 src/backend/parser/parse_expr.c               | 11 ++++++-----
 src/backend/parser/parse_rpr.c                |  2 +-
 src/test/regress/expected/rpr_base.out        |  6 +++---
 src/test/regress/expected/rpr_explain.out     |  2 +-
 src/test/regress/expected/rpr_integration.out |  4 ++--
 src/test/regress/expected/rpr_nfa.out         |  4 ++--
 src/test/regress/sql/rpr_base.sql             |  6 +++---
 src/test/regress/sql/rpr_explain.sql          |  2 +-
 src/test/regress/sql/rpr_integration.sql      |  4 ++--
 src/test/regress/sql/rpr_nfa.sql              |  4 ++--
 13 files changed, 37 insertions(+), 29 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 52bcd77390c..e64efe0c7fc 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -38,6 +38,12 @@ What is a Flat-Array Stream NFA?
 Chapter I  Row Pattern Recognition Overview
 ============================================================================
 
+Normative reference: ISO/IEC 19075-5 (SQL Technical Report, Part 5: Row
+pattern recognition in SQL).  Subclause numbers cited throughout this code
+base refer to that document.  Where Chapters 4 (FROM clause) and 6 (WINDOW
+clause) describe parallel material, this implementation cites the Chapter 6
+subclause first because it targets Feature R020.
+
 Row Pattern Recognition (hereafter RPR) is a feature introduced in SQL:2016
 that matches regex-based patterns against ordered row sets.
 
@@ -1033,7 +1039,7 @@ match:
 
 X-3. INITIAL vs SEEK
 
-  Standard definition (section 6.12):
+  Standard definition (ISO/IEC 19075-5 6.12):
   INITIAL: "is used to look for a match whose first row is R."
   SEEK:    "is used to permit a search for the first match anywhere
            from R through the end of the full window frame."
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1e6196d6960..e1caa7bb528 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -736,8 +736,9 @@ nfa_absorb_contexts(WindowAggState *winstate)
  * once per row by evaluating all DEFINE expressions.  NULL means no DEFINE
  * clauses exist (only possible during early development/testing).
  *
- * Per SQL:2016 R020, pattern variables not listed in DEFINE are implicitly
- * TRUE -- they match every row.  This is checked via varId >= list_length.
+ * Per ISO/IEC 19075-5 Feature R020, pattern variables not listed in DEFINE
+ * are implicitly TRUE -- they match every row.  This is checked via
+ * varId >= list_length.
  */
 static bool
 nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index ed8b6c3414c..c65681463b3 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1050,10 +1050,10 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			}
 
 			/*
-			 * Variable not in DEFINE clause - this is valid per SQL standard.
-			 * Such variables are implicitly TRUE. Add to varNames so they get
-			 * a varId >= defineVariableList length, which executor treats as
-			 * TRUE.
+			 * Variable not in DEFINE clause - this is valid per ISO/IEC
+			 * 19075-5 Feature R020.  Such variables are implicitly TRUE. Add
+			 * to varNames so they get a varId >= defineVariableList length,
+			 * which executor treats as TRUE.
 			 */
 			Assert(*numVars < RPR_VARID_MAX);
 			varNames[(*numVars)++] = node->varName;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 58ebd7d24b8..228d3b063db 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -631,11 +631,12 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	/*----------
 	 * Qualified references in DEFINE need a tri-classification:
 	 *
-	 *	  pattern variable qualifier (e.g. UP.price): valid per 19075-5 4.16
-	 *	  but not yet implemented -- raise FEATURE_NOT_SUPPORTED.
+	 *	  pattern variable qualifier (e.g. UP.price): valid per
+	 *	  ISO/IEC 19075-5 6.15 / 4.16 but not yet implemented --
+	 *	  raise FEATURE_NOT_SUPPORTED.
 	 *
-	 *	  FROM-clause range variable qualifier: prohibited by 19075-5 6.5
-	 *	  -- raise SYNTAX_ERROR.
+	 *	  FROM-clause range variable qualifier: prohibited by
+	 *	  ISO/IEC 19075-5 6.5 -- raise SYNTAX_ERROR.
 	 *
 	 *	  any other qualifier (typo, undefined name): fall through and let
 	 *	  normal column resolution produce a sensible error.
@@ -1946,7 +1947,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			break;
 
 			/*----------
-			 * XXX SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 / R020)
+			 * XXX SQL/RPR (ISO/IEC 19075-5 6.17.4 / 4.18.4; R020 / R010)
 			 * permits a subquery nested in a DEFINE expression provided
 			 * that:
 			 *   (a) the subquery does not itself perform row pattern
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index bba887f17ce..d2ed6c14811 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -467,7 +467,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  *     (RPR's NFA may evaluate the same row's predicate multiple times
  *     during backtracking, so a volatile result would make matching
  *     non-deterministic).
- *   - For each outer RPRNavExpr (per SQL 5.6.4 nesting rules):
+ *   - For each outer RPRNavExpr (per ISO/IEC 19075-5 5.6.4 nesting rules):
  *     * arg must contain at least one column reference
  *     * PREV/NEXT wrapping FIRST/LAST flattens to a compound kind
  *     * Other nestings are rejected (FIRST(PREV()), PREV(PREV()), ...)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 86abb96c177..cfd2645bbed 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1,6 +1,6 @@
 -- ============================================================
 -- RPR Base Tests
--- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5)
 -- ============================================================
 --
 -- Parser Layer:
@@ -3065,7 +3065,7 @@ LINE 6:     DEFINE A AS val > 0
             ^
 -- Expected: Syntax error
 -- Qualified column references (NOT SUPPORTED)
--- Pattern variable qualified name: not supported (valid per SQL standard 4.16, not yet implemented)
+-- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3104,7 +3104,7 @@ ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
 -- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
--- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
+-- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index c4516d3c756..77079d5e8c9 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2185,7 +2185,7 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
 (9 rows)
 
--- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+-- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches
 CREATE VIEW rpr_ev_edge_empty_match_min2 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 3) AS s(v)
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 905bd3538de..7cbeed3347e 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1278,8 +1278,8 @@ ORDER BY o.id, r.id;
 -- PostgreSQL restriction, so this is the natural place to exercise
 -- "RPR under Recursive Union").
 --
--- XXX: Whether this case falls under the ISO/IEC 9075-2 4.18.5 /
--- 6.17.5 prohibition is not something I can judge.  If this case
+-- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
+-- 4.18.5 prohibition is not something I can judge.  If this case
 -- is not prohibited, the open question is whether a query that
 -- does trigger the prohibition can be constructed at all.
 -- Whether to prohibit this case is left to the community.
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 4cff7cfbbd7..fe5bb324df0 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -4083,7 +4083,7 @@ WINDOW w AS (
 
 -- ============================================================
 -- Standard Clause 7: Formal Pattern Matching Rules
--- ISO/IEC 19075-5:2021, Clause 7
+-- ISO/IEC 19075-5, Clause 7
 -- ============================================================
 -- ------------------------------------------------------------
 -- 7.2.2 Alternation: first alternative is preferred
@@ -4453,7 +4453,7 @@ WINDOW w AS (
   3 | {B}   |             |          
 (3 rows)
 
--- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- (A?){2,3}: min=2, nullable inner.  Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE)
 -- is valid: two empty iterations satisfy min=2.
 -- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e8c72706720..fd289d7cf67 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1,6 +1,6 @@
 -- ============================================================
 -- RPR Base Tests
--- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5)
 -- ============================================================
 --
 -- Parser Layer:
@@ -2083,7 +2083,7 @@ WINDOW w AS (

--- Pattern variable qualified name: not supported (valid per SQL standard 4.16, not yet implemented)
+-- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2116,7 +2116,7 @@ WINDOW w AS (
 );
 -- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
--- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
+-- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index d339a80a673..a527615849a 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -1228,7 +1228,7 @@ WINDOW w AS (
     DEFINE A AS FALSE
 );');
 
--- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+-- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches
 CREATE VIEW rpr_ev_edge_empty_match_min2 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 3) AS s(v)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 29b2db2f7bb..f4267c74645 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -792,8 +792,8 @@ ORDER BY o.id, r.id;
 -- PostgreSQL restriction, so this is the natural place to exercise
 -- "RPR under Recursive Union").
 --
--- XXX: Whether this case falls under the ISO/IEC 9075-2 4.18.5 /
--- 6.17.5 prohibition is not something I can judge.  If this case
+-- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
+-- 4.18.5 prohibition is not something I can judge.  If this case
 -- is not prohibited, the open question is whether a query that
 -- does trigger the prohibition can be constructed at all.
 -- Whether to prohibit this case is left to the community.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 29ec4a9dacb..7a5b5c41b24 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2976,7 +2976,7 @@ WINDOW w AS (
 
 -- ============================================================
 -- Standard Clause 7: Formal Pattern Matching Rules
--- ISO/IEC 19075-5:2021, Clause 7
+-- ISO/IEC 19075-5, Clause 7
 -- ============================================================
 
 -- ------------------------------------------------------------
@@ -3280,7 +3280,7 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- (A?){2,3}: min=2, nullable inner.  Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE)
 -- is valid: two empty iterations satisfy min=2.
 -- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
-- 
2.50.1 (Apple Git-155)

---

From: Tatsuo Ishii <[email-masked]>
Date: Sun, 10 May 2026 11:54:37 +0900 (JST)

Hi Henson,

>     ISO/IEC 19075-5 6.17.5 / 4.18.5 interpretation.

ISO/IEC 19075-5 6.17.5 stats that "Row pattern matching is prohibited
in recursive queries. For example, the following is a syntax error:

CREATE RECURSIVE VIEW Problem... SELECT Kolo, Xoro FROM Ticker... WINDOW W AS... MEASURES..."

ISO/IEC 19075-5 4.18.5 stats that "Row pattern matching is prohibited
in recursive queries. For example, the following is a syntax error:

CREATE RECURSIVE VIEW Problem... SELECT Kolo, Xoro FROM t... MATCH_REZOGNIZE..."

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 12 May 2026 15:03:28 +0900 (JST)

Hi Henson,

> - Add src/backend/executor/README.rpr (previously was in ExecRPR.c)

README.rpr is extremely useful for those who want to review the RPR
patches.  I found a room to enhance the document.  Attached is a small
patch tries to enhance README.rpr, on top of v47.

- Make "target audience" and "scope" of the README more descriptive.
- Add References (currently the SQL standards only)
- Add explanation of some abbreviations (NFA, AST)
- Add reference sections for absorption. Readers might not be familiar with "absorption"
- Add more fields to WindowAggState
- Add window framing rules with RPR

Regards,
--
Tatsuo Ishii
SRA OSS K.K.
English: http://www.sraoss.co.jp/index_en/
Japanese:http://www.sraoss.co.jp
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 52bcd77390c..dd98ce97adf 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -2,11 +2,15 @@
   PostgreSQL Row Pattern Recognition: Flat-Array Stream NFA Guide
 ============================================================================
 
-  Target audience: Developers with a basic understanding of the PostgreSQL
-                   executor and planner architecture
+  This README's target audience is developers with a basic
+  understanding of the PostgreSQL executor and planner architecture.
+  Also it would be better for them to understand the specification of
+  the row pattern recognition in the SQL standard [1][2]. If you do
+  not have access to the SQL standard, Oracle's manual or Trino's
+  manual can be alternatives for them.
 
-  Scope: The entire process from PATTERN/DEFINE clause parsing to NFA
-         runtime execution
+  This README's scope is the entire process from PATTERN/DEFINE clause
+  parsing to NFA runtime execution.
 
   Related code:
     - src/backend/parser/parse_rpr.c          (parser phase)
@@ -23,10 +27,11 @@
 
 What is a Flat-Array Stream NFA?
 
-  The NFA in this implementation is not a traditional state-transition graph
-  but a flat array of fixed-size 16-byte elements. At runtime, it processes
-  the row stream in a forward-only manner, expanding epsilon transitions
-  eagerly without backtracking.
+  The NFA (Nondeterministic Finite Automaton) in this implementation
+  is not a traditional state-transition graph but a flat array of
+  fixed-size 16-byte elements. At runtime, it processes the row stream
+  in a forward-only manner, expanding epsilon transitions eagerly
+  without backtracking.
 
   - Flat-Array: Pattern compiled into a flat array,
                 not a graph (Chapter IV)
@@ -126,14 +131,14 @@ following:
 
   (3) DEFINE clause transformation (transformDefineClause)
 
-III-2. PATTERN AST
+III-2. PATTERN AST (Abstract Syntax Tree)
 
 The parser transforms the PATTERN clause into an RPRPatternNode tree.
 Each node has one of the following four types:
 
   RPR_PATTERN_VAR    Variable reference. Name stored in varName field.
   RPR_PATTERN_SEQ    Concatenation. Children node list in children.
-  RPR_PATTERN_ALT    Alternation. Branch node list in children.
+  RPR_PATTERN_ALT    Alternation (or). Branch node list in children.
   RPR_PATTERN_GROUP  Group (parentheses). Body node list in children.
 
 All nodes have min/max fields to express quantifiers:
@@ -264,9 +269,11 @@ Element flags (1 byte, bitmask):
         matches. (IV-4b)
 
   0x04  RPR_ELEM_ABSORBABLE_BRANCH  (VAR, BEGIN, END, ALT)
-        Element lies within an absorbable region.  Used at runtime
-        to track whether the current NFA state is in an absorbable
-        context.
+        Element lies within an absorbable region.  Used at runtime to
+        track whether the current NFA state is in an absorbable
+        context. See "IV-5. Absorbability Analysis" and
+        "VIII-2. Solution: Context Absorption" for more details about
+        absorption.
 
   0x08  RPR_ELEM_ABSORBABLE         (VAR, END)
         Absorption judgment point.  Where to compare consecutive
@@ -508,7 +515,10 @@ V-3. RPR Fields of WindowAggState
   nfaStateFree                  Reuse pool for states
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
+  nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
   nfaStateSize                  Precomputed size of RPRNFAState
+  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
+  nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
 Memory management:
 
@@ -1047,6 +1057,10 @@ X-3. INITIAL vs SEEK
 
 X-4. Bounded Frame Handling
 
+  With RPR, the frame mode is always ROWS and the frame start must be
+  CURRENT ROW. The frame end can be either UNBOUNDED FOLLOWING or n
+  FOLLOWING.
+  
   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
   frameOffset indicating the upper bound.  Before the match phase,
@@ -1573,6 +1587,15 @@ C-7. PATTERN ((A+ B | C*)+ D)  -- Per-branch absorption in ALT
   nullable.
   BEGIN and ALT get ABSORBABLE_BRANCH (on the path to absorbable elements).
 
+
+References:
+
+[1] ISO/IEC 19075-5 Information technology - Guidance for the use of
+    database language SQL - Part 5: Row pattern recognition
+
+[2] ISO/IEC 9075-2 Information technology - Database languages - SQL -
+    Part 2: Foundation (SQL/Foundation)
+
 ============================================================================
   End of document
 ============================================================================

---

From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 16:01:05 +0900

Hi Tatsuo,

Four more incremental patches on top of the 2026-05-09 set
(0001-0011).  0012-0013 close the previously deferred B7
recursive-CTE item, on the strength of your ISO/IEC 9075-2:2016
7.17 citation; 0014-0015 apply and round out the README.rpr
enhancement you sent on 2026-05-12.

Patch summary:

> So I think at least SQL:2016 explicitly prohibits using RPR within
> recursive CTE. I would appreciate if anybody confirms this in
> SQL:2023.

  - 0012 Add rpr_integration B7 cases for RPR in recursive
         query
      Test-only prelude to 0013.  Replaces the prior B7 case
      (RPR in a recursive CTE base leg, asserting it works)
      with two cases that the upcoming prohibition needs to
      cover: WITH RECURSIVE with RPR in the base leg, and
      CREATE RECURSIVE VIEW with an RPR window.  Cites
      ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010), and
      the formal rule in ISO/IEC 9075-2:2016 7.17 Syntax
      Rule 3)e)f) you pointed me to.  Drops the deferred
      XXX comment that left this open to community input.
      Expected output still matches pre-rejection behavior;
      0013 flips both queries to ERROR.

  - 0013 Reject row pattern recognition in recursive queries
      Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every
      <with list element> in a WITH RECURSIVE clause is
      "potentially recursive" and shall not contain a
      <row pattern common syntax>.  ISO/IEC 19075-5 6.17.5
      (R020) and 4.18.5 (R010) restate the prohibition for
      CREATE RECURSIVE VIEW, which makeRecursiveViewSelect()
      already rewrites to WITH RECURSIVE so the same path
      catches both forms.  The rejection runs in
      transformWithClause() against the raw parse tree,
      before per-CTE analysis, and reports the PATTERN keyword
      position via a new RPCommonSyntax.location field
      captured in gram.y.  Flips both B7 cases from result
      rows to the new error.

> - Add window framing rules with RPR

  - 0014 Enhance README.rpr per Tatsuo Ishii's review
      Doc-only.  Applies your off-list enhancement patch
      (2026-05-12) on top of v47 with no modification:
        * Make "target audience" and "scope" more
          descriptive, pointing readers to the SQL standard
          (with Oracle / Trino manuals as alternatives)
        * Spell out NFA and AST on first use
        * Cross-reference "IV-5. Absorbability Analysis" and
          "VIII-2. Solution: Context Absorption" from the
          RPR_ELEM_ABSORBABLE_BRANCH flag description
        * List nfaVisitedNWords, defineMatchStartDependent,
          and nfaLastProcessedRow in V-3
        * State the window framing rules that apply with RPR
        * Add a References section (SQL standards)

  - 0015 Round out README.rpr WindowAggState field coverage
      Doc-only follow-up to 0014.  Completes the V-3 field
      list (adds nfaVisitedMinWord / nfaVisitedMaxWord; notes
      that EXPLAIN ANALYZE instrumentation counters are
      intentionally omitted, with a pointer to execnodes.h)
      and mirrors the new fields in the Appendix B diagram,
      which still reflected the pre-review field list.

Previously deferred items:
  - B7 Recursive CTE XXX: addressed in 0012 (tests) + 0013
    (rejection) per the standards citations you supplied.

Best regards,
Henson
From cda37c3c806c8ee99e7e2060bfab5b4105b728ad Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 10 May 2026 13:51:45 +0900
Subject: [PATCH 12/15] Add rpr_integration B7 cases for RPR in recursive query

Replace the prior B7 test (which asserted that an RPR window works
in the base leg of a recursive CTE) with two cases the recursive-RPR
prohibition needs to cover: WITH RECURSIVE with RPR in the base leg,
and CREATE RECURSIVE VIEW with an RPR window.  Cite ISO/IEC 19075-5
6.17.5 (R020) and 4.18.5 (R010), and the formal rule in ISO/IEC
9075-2:2016 7.17 Syntax Rule 3)e)f), and drop the deferred XXX
comment that left this case open to community input.

Expected output still matches the current (pre-rejection) behavior;
a follow-up patch adds the rejection in parse_cte.c and flips both
queries to ERROR.
---
 src/test/regress/expected/rpr_integration.out | 71 ++++++-------------
 src/test/regress/sql/rpr_integration.sql      | 47 ++++++------
 2 files changed, 43 insertions(+), 75 deletions(-)

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 7cbeed3347e..0b05a826a27 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1269,54 +1269,18 @@ ORDER BY o.id, r.id;
 -- ============================================================
 -- B7. RPR + Recursive CTE
 -- ============================================================
--- Verify that an RPR window can appear inside the non-recursive
--- (base) leg of a recursive CTE.  The plan must show the RPR
--- WindowAgg sitting under the Recursive Union as the base-leg
--- child, with the WorkTable Scan feeding the recursive leg above
--- it.  This confirms that RPR output can seed a recursive CTE
--- (window functions cannot appear in the recursive leg itself, a
--- PostgreSQL restriction, so this is the natural place to exercise
--- "RPR under Recursive Union").
---
--- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
--- 4.18.5 prohibition is not something I can judge.  If this case
--- is not prohibited, the open question is whether a query that
--- does trigger the prohibition can be constructed at all.
--- Whether to prohibit this case is left to the community.
--- Plan: Recursive Union with the RPR WindowAgg on the base leg and
--- the WorkTable Scan on the recursive leg.
-EXPLAIN (COSTS OFF)
-WITH RECURSIVE seq AS (
-    SELECT id, val, count(*) OVER w AS cnt
-    FROM rpr_integ
-    WINDOW w AS (ORDER BY id
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (A B+)
-        DEFINE B AS val > PREV(val))
-    UNION ALL
-    SELECT id + 100, val, cnt FROM seq WHERE id < 3
-)
-SELECT id, val, cnt FROM seq ORDER BY id;
-                                              QUERY PLAN                                               
--------------------------------------------------------------------------------------------------------
- Sort
-   Sort Key: seq.id
-   CTE seq
-     ->  Recursive Union
-           ->  WindowAgg
-                 Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-                 Pattern: a b+
-                 Nav Mark Lookback: 1
-                 ->  Sort
-                       Sort Key: rpr_integ.id
-                       ->  Seq Scan on rpr_integ
-           ->  WorkTable Scan on seq seq_1
-                 Filter: (id < 3)
-   ->  CTE Scan on seq
-(14 rows)
-
--- Result: the base leg contributes the RPR match counts; the
--- recursive leg propagates those counts with shifted ids.
+-- Verify that RPR is rejected inside a recursive query.
+-- ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010) cite CREATE
+-- RECURSIVE VIEW examples and state that "row pattern matching
+-- is prohibited in recursive queries".  The formal rule lives in
+-- ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)f): a potentially
+-- recursive <with list element> shall not contain a <row pattern
+-- measures> or <row pattern common syntax>.  Per 3)e), every
+-- <with list element> under WITH RECURSIVE is "potentially
+-- recursive", so the rejection covers the base (non-recursive)
+-- leg too, not just the self-referencing leg.
+-- WITH RECURSIVE: RPR in the base leg is rejected even though the
+-- base leg never references the recursive CTE name.
 WITH RECURSIVE seq AS (
     SELECT id, val, count(*) OVER w AS cnt
     FROM rpr_integ
@@ -1344,6 +1308,17 @@ SELECT id, val, cnt FROM seq ORDER BY id;
  102 |  20 |   0
 (12 rows)
 
+-- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
+-- into WITH RECURSIVE, so the same rejection applies.  This is
+-- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
+CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
+    SELECT id, val, count(*) OVER w
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val));
+DROP VIEW rpr_recv;
 -- ============================================================
 -- B8. RPR + Incremental sort
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index f4267c74645..bc8f4712bcb 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -783,24 +783,19 @@ ORDER BY o.id, r.id;
 -- ============================================================
 -- B7. RPR + Recursive CTE
 -- ============================================================
--- Verify that an RPR window can appear inside the non-recursive
--- (base) leg of a recursive CTE.  The plan must show the RPR
--- WindowAgg sitting under the Recursive Union as the base-leg
--- child, with the WorkTable Scan feeding the recursive leg above
--- it.  This confirms that RPR output can seed a recursive CTE
--- (window functions cannot appear in the recursive leg itself, a
--- PostgreSQL restriction, so this is the natural place to exercise
--- "RPR under Recursive Union").
---
--- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
--- 4.18.5 prohibition is not something I can judge.  If this case
--- is not prohibited, the open question is whether a query that
--- does trigger the prohibition can be constructed at all.
--- Whether to prohibit this case is left to the community.
-
--- Plan: Recursive Union with the RPR WindowAgg on the base leg and
--- the WorkTable Scan on the recursive leg.
-EXPLAIN (COSTS OFF)
+-- Verify that RPR is rejected inside a recursive query.
+-- ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010) cite CREATE
+-- RECURSIVE VIEW examples and state that "row pattern matching
+-- is prohibited in recursive queries".  The formal rule lives in
+-- ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)f): a potentially
+-- recursive <with list element> shall not contain a <row pattern
+-- measures> or <row pattern common syntax>.  Per 3)e), every
+-- <with list element> under WITH RECURSIVE is "potentially
+-- recursive", so the rejection covers the base (non-recursive)
+-- leg too, not just the self-referencing leg.
+
+-- WITH RECURSIVE: RPR in the base leg is rejected even though the
+-- base leg never references the recursive CTE name.
 WITH RECURSIVE seq AS (
     SELECT id, val, count(*) OVER w AS cnt
     FROM rpr_integ
@@ -813,19 +808,17 @@ WITH RECURSIVE seq AS (
 )
 SELECT id, val, cnt FROM seq ORDER BY id;
 
--- Result: the base leg contributes the RPR match counts; the
--- recursive leg propagates those counts with shifted ids.
-WITH RECURSIVE seq AS (
-    SELECT id, val, count(*) OVER w AS cnt
+-- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
+-- into WITH RECURSIVE, so the same rejection applies.  This is
+-- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
+CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
+    SELECT id, val, count(*) OVER w
     FROM rpr_integ
     WINDOW w AS (ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
-        DEFINE B AS val > PREV(val))
-    UNION ALL
-    SELECT id + 100, val, cnt FROM seq WHERE id < 3
-)
-SELECT id, val, cnt FROM seq ORDER BY id;
+        DEFINE B AS val > PREV(val));
+DROP VIEW rpr_recv;
 
 -- ============================================================
 -- B8. RPR + Incremental sort
-- 
2.50.1 (Apple Git-155)

From e0a2a8420cf00a807784f0aa7dcf74382b66525e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 10 May 2026 14:42:09 +0900
Subject: [PATCH 13/15] Reject row pattern recognition in recursive queries

Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every <with list
element> in a WITH RECURSIVE clause is "potentially recursive" and
shall not contain a <row pattern common syntax>.  ISO/IEC 19075-5
6.17.5 (R020) and 4.18.5 (R010) restate the prohibition for CREATE
RECURSIVE VIEW, which makeRecursiveViewSelect() rewrites to WITH
RECURSIVE so the same path catches both forms.

The rejection runs in transformWithClause() against the raw parse
tree, before per-CTE analysis, and reports the PATTERN keyword
position via a new RPCommonSyntax.location field captured in
gram.y.  Flips both rpr_integration B7 cases (added in the
preceding commit) from result rows to the new error.
---
 src/backend/parser/gram.y                     |  1 +
 src/backend/parser/parse_cte.c                | 58 ++++++++++++++++++-
 src/include/nodes/parsenodes.h                |  1 +
 src/test/regress/expected/rpr_integration.out | 23 ++------
 src/test/regress/sql/rpr_integration.sql      |  1 -
 src/tools/pgindent/typedefs.list              |  1 +
 6 files changed, 64 insertions(+), 21 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index aa587e6aced..a2fafb717cd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17585,6 +17585,7 @@ opt_row_pattern_skip_to opt_row_pattern_initial_or_seek
 				n->initial = $2;
 				n->rpPattern = (RPRPatternNode *) $5;
 				n->rpDefs = $8;
+				n->location = @3;
 				$$ = (Node *) n;
 			}
 			| /*EMPTY*/		{ $$ = NULL; }
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index ccde199319a..3830597bb2b 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -96,6 +96,14 @@ static void checkWellFormedRecursion(CteState *cstate);
 static bool checkWellFormedRecursionWalker(Node *node, CteState *cstate);
 static void checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate);
 
+/* Recursive-WITH RPR rejection */
+typedef struct
+{
+	ParseLoc	location;		/* location of first RPR window, or -1 */
+} ContainRPRContext;
+
+static bool contain_rpr_walker(Node *node, void *context);
+
 
 /*
  * transformWithClause -
@@ -157,13 +165,31 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 	if (withClause->recursive)
 	{
 		/*
-		 * For WITH RECURSIVE, we rearrange the list elements if needed to
-		 * eliminate forward references.  First, build a work array and set up
-		 * the data structure needed by the tree walkers.
+		 * Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every <with list
+		 * element> in a WITH RECURSIVE clause is "potentially recursive" and
+		 * shall not contain a <row pattern common syntax>.  (PostgreSQL does
+		 * not implement <row pattern measures>, so only the common syntax
+		 * needs to be checked.)  ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5
+		 * (R010) restate the prohibition for CREATE RECURSIVE VIEW, which is
+		 * rewritten to WITH RECURSIVE by makeRecursiveViewSelect() and so
+		 * flows through here as well.
 		 */
 		CteState	cstate;
 		int			i;
 
+		foreach(lc, withClause->ctes)
+		{
+			CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
+			ContainRPRContext ctx;
+
+			ctx.location = -1;
+			if (contain_rpr_walker(cte->ctequery, &ctx))
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("cannot use row pattern recognition in a recursive query"),
+						parser_errposition(pstate, ctx.location));
+		}
+
 		cstate.pstate = pstate;
 		cstate.numitems = list_length(withClause->ctes);
 		cstate.items = (CteItem *) palloc0(cstate.numitems * sizeof(CteItem));
@@ -1268,3 +1294,29 @@ 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.
+ */
+static bool
+contain_rpr_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, WindowDef))
+	{
+		WindowDef  *wd = (WindowDef *) node;
+
+		if (wd->rpCommonSyntax != NULL)
+		{
+			((ContainRPRContext *) context)->location = wd->rpCommonSyntax->location;
+			return true;
+		}
+	}
+	return raw_expression_tree_walker(node, contain_rpr_walker, context);
+}
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index adefb1d5bad..5200182aa46 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -646,6 +646,7 @@ typedef struct RPCommonSyntax
 	RPRPatternNode *rpPattern;	/* PATTERN clause AST */
 	List	   *rpDefs;			/* row pattern definitions clause (list of
 								 * ResTarget) */
+	ParseLoc	location;		/* PATTERN keyword location, or -1 */
 } RPCommonSyntax;
 
 /*
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 0b05a826a27..b598ef95776 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1292,22 +1292,9 @@ WITH RECURSIVE seq AS (
     SELECT id + 100, val, cnt FROM seq WHERE id < 3
 )
 SELECT id, val, cnt FROM seq ORDER BY id;
- id  | val | cnt 
------+-----+-----
-   1 |  10 |   2
-   2 |  20 |   0
-   3 |  15 |   2
-   4 |  25 |   0
-   5 |   5 |   3
-   6 |  30 |   0
-   7 |  35 |   0
-   8 |  20 |   3
-   9 |  40 |   0
-  10 |  45 |   0
- 101 |  10 |   2
- 102 |  20 |   0
-(12 rows)
-
+ERROR:  cannot use row pattern recognition in a recursive query
+LINE 6:         PATTERN (A B+)
+                ^
 -- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
 -- into WITH RECURSIVE, so the same rejection applies.  This is
 -- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
@@ -1318,7 +1305,9 @@ CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
         DEFINE B AS val > PREV(val));
-DROP VIEW rpr_recv;
+ERROR:  cannot use row pattern recognition in a recursive query
+LINE 6:         PATTERN (A B+)
+                ^
 -- ============================================================
 -- B8. RPR + Incremental sort
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index bc8f4712bcb..5f3853becba 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -818,7 +818,6 @@ CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
         DEFINE B AS val > PREV(val));
-DROP VIEW rpr_recv;
 
 -- ============================================================
 -- B8. RPR + Incremental sort
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d23b392800e..da10b4ac546 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -534,6 +534,7 @@ Constraint
 ConstraintCategory
 ConstraintInfo
 ConstraintsSetStmt
+ContainRPRContext
 ControlData
 ControlFileData
 ConvInfo
-- 
2.50.1 (Apple Git-155)

From e31cc038ee37bf62d1cc50ae478bd455579a34ee Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 15:38:03 +0900
Subject: [PATCH 14/15] Enhance README.rpr per Tatsuo Ishii's review

Apply Tatsuo Ishii's enhancement patch on top of v47:

- Make "target audience" and "scope" more descriptive,
  pointing readers to the SQL standard (and Oracle/Trino
  manuals as alternatives)
- Spell out NFA and AST on first use
- Cross-reference the absorbability sections from the
  RPR_ELEM_ABSORBABLE_BRANCH flag description
- List additional WindowAggState fields in V-3
  (nfaVisitedNWords, defineMatchStartDependent,
  nfaLastProcessedRow)
- State the window framing rules that apply with RPR
- Add a References section (SQL standards)
---
 src/backend/executor/README.rpr | 49 ++++++++++++++++++++++++---------
 1 file changed, 36 insertions(+), 13 deletions(-)

index e64efe0c7fc..6c2bddab455 100644

 
   Related code:

 
   - Flat-Array: Pattern compiled into a flat array,
                 not a graph (Chapter IV)
@@ -132,14 +137,14 @@ following:

-III-2. PATTERN AST

 
 All nodes have min/max fields to express quantifiers:
@@ -270,9 +275,11 @@ Element flags (1 byte, bitmask):
         matches. (IV-4b)

@@ -514,7 +521,10 @@ V-3. RPR Fields of WindowAggState
 
 Memory management:
 
@@ -1053,6 +1063,10 @@ X-3. INITIAL vs SEEK

@@ -1579,6 +1593,15 @@ C-7. PATTERN ((A+ B | C*)+ D)  -- Per-branch absorption in ALT
 
+
+References:
+

From 7face9e5606c56d22286f4866adc81e39f88bc82 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 15:43:49 +0900
Subject: [PATCH 15/15] Round out README.rpr WindowAggState field coverage

Follow-up to the previous commit applying Tatsuo Ishii's
review.  That commit added three WindowAggState fields to
V-3 but left a few related entries out, and Appendix B's
diagram still showed the pre-review field list.

- Add nfaVisitedMinWord and nfaVisitedMaxWord to V-3
- Note that EXPLAIN ANALYZE instrumentation counters are
  omitted from V-3 (see execnodes.h)
- Mirror the V-3 additions in the Appendix B diagram
---
 src/backend/executor/README.rpr | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6c2bddab455..6ff7f33e62e 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -522,10 +522,15 @@ V-3. RPR Fields of WindowAggState
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
   nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
+  nfaVisitedMinWord             Lowest bitmapword index touched since last reset
+  nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
   defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
   nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
+  EXPLAIN ANALYZE instrumentation counters are omitted here; see
+  execnodes.h for the full list.
+
 Memory management:
 
   States and contexts are managed through their own free lists.
@@ -1480,7 +1485,13 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineVariableList: List<String> (variable names, DEFINE order)
     |--- defineClauseList: List<ExprState>
     |--- nfaVarMatched: bool[] (per-row cache)
+    |--- defineMatchStartDependent: Bitmapset* (match_start-dependent
+    |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
+    |--- nfaVisitedNWords: int (size of nfaVisitedElems)
+    |--- nfaVisitedMinWord / nfaVisitedMaxWord: int16
+    |        (touched-word range for fast reset)
+    |--- nfaLastProcessedRow: int64 (-1 = none)
     |--- nfaStateSize: Size (pre-calculated RPRNFAState allocation size)
     |--- nfaContext <-> nfaContextTail (doubly-linked list)
     |   +--- RPRNFAContext
-- 
2.50.1 (Apple Git-155)

---

From: Tatsuo Ishii <[email-masked]>
Date: Wed, 13 May 2026 12:11:14 +0900 (JST)

Hi Henson,

Thank you for the patches. I looked into 0013.

> -		 * the data structure needed by the tree walkers.

I think removing the exiting comment ("For WITH RECURSIVE, we rearrange
the list elements...") is not appropriate as this explains subsequent
process, which is not changed after the patch.

> +						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),

ERRCODE_FEATURE_NOT_SUPPORTED should be ERRCODE_SYNTAX_ERROR instead?
IMO ERRCODE_FEATURE_NOT_SUPPORTED is used when the feature is defined
by the standard but PostgreSQL just has not implemented yet. In this
case the standard disllow RPR in recursive CTE.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Wed, 13 May 2026 13:15:20 +0900

Hi Tatsuo,

Thanks for the review.  Both points addressed in the attached
0013 v2; 0012/0014/0015 are unchanged from the 2026-05-12 set
and re-attached for convenience.

> process, which is not changed after the patch.

Agreed -- the original comment still describes the work-array
and tree-walker setup that follows, which the patch does not
touch.  Restored it in place and moved the new ISO citation
comment to sit directly above the RPR rejection foreach so
each block is documented next to the code it explains.

> case the standard disllow RPR in recursive CTE.

Right, the standard explicitly prohibits this combination
rather than leaving it unimplemented.  Switched to
ERRCODE_SYNTAX_ERROR.  The .out files reference only the
ERROR message so the regress expected output is unchanged.

Thanks,
Henson

 

From c837d254bf13dcc85e07530a30210fda8b8ea27f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 10 May 2026 14:42:09 +0900
Subject: [PATCH 13/15] Reject row pattern recognition in recursive queries

 src/backend/parser/parse_cte.c                | 57 +++++++++++++++++++
 6 files changed, 66 insertions(+), 18 deletions(-)

index ccde199319a..0974b43d028 100644

 /*
  * transformWithClause -
@@ -164,6 +172,29 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 		CteState	cstate;
 		int			i;
 
+		/*
@@ -1268,3 +1299,29 @@ checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
 
 /*

From fe8edf9e1a79141ad63e731101bb8332d8158d72 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 15:38:03 +0900
Subject: [PATCH 14/15] Enhance README.rpr per Tatsuo Ishii's review

 

   Related code:

 

-III-2. PATTERN AST

 

 
 Memory management:

 
 
+
+References:
+

From ad5f8dc3b587b2aed17954b5bfcdea2a4c94c288 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 15:43:49 +0900
Subject: [PATCH 15/15] Round out README.rpr WindowAggState field coverage

---

From: Tatsuo Ishii <[email-masked]>
Date: Sun, 24 May 2026 17:11:39 +0900 (JST)

Hi Henson,

> Attached is the v47 patches for Row pattern recognition (SQL/RPR).

While looking into v47, I noticed that
raw_expression_tree_walker_impl() lacks tracking RPCommonSyntax and
its children nodes. Probably this does nothing wrong with RPR
functionalities but just for completeness I created a patch on top of
v47.

Regards,
--
Tatsuo Ishii
SRA OSS K.K.
English: http://www.sraoss.co.jp/index_en/
Japanese:http://www.sraoss.co.jp
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 651d049cfa2..c8694726e3c 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4612,6 +4612,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(wd->endOffset))
 					return true;
+				if (WALK(wd->rpCommonSyntax))
+					return true;
 			}
 			break;
 		case T_RangeSubselect:
@@ -4865,6 +4867,24 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_RPCommonSyntax:
+			{
+				RPCommonSyntax *rc = (RPCommonSyntax *) node;
+
+				if (WALK(rc->rpPattern))
+					return true;
+				if (WALK(rc->rpDefs))
+					return true;
+			}
+			break;
+		case T_RPRPatternNode:
+			{
+				RPRPatternNode *rp = (RPRPatternNode *) node;
+
+				if (WALK(rp->children))
+					return true;
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));

---

From: jian he <[email-masked]>
Date: Tue, 26 May 2026 11:20:04 +0800

Hi.

Disclaimer: I had some private off-list discussions with Henson Choi
regarding execRPR.c, and the NFA.
The following are more formal comments regarding execRPR.c and the NFA
algorithm, based on v47-0001 to v47-0009, I haven't applied the
incremental diff yet.
(Since the incremental diffs are scattered across several threads, a
unified v48 would be better).
(I'm still wrapping my head around the NFA in execRPR.c, so take the
comments below with a grain of salt.)

PATTERN (A B C D)
We can short-circuit and exit early if any of the evaluations (A, B,
or C) fail in nfa_match.
This is necessary since the chance of a pattern element evaluation
returning false is not rare, I think.

For src/backend/executor/README.rpr:
We should explicitly explain 'absorbable' and 'absorption' somewhere in
README.rpr, as the text currently just assumes the reader knows what they mean.
Using some example illustrate "absorption" meaning, put it on
README.rpr would be great.
We can also mention that 'DFS' refers to Depth-First Search".

``````
  (4) Call nfa_advance(initialAdvance=true)
``````
In V47, the variable `initialAdvance` does not exist.

In nfa_advance_var, after the first Assert, we can add:
Assert(elem->next <  pattern->numElements);

ExecRPRFinalizeAllContexts seems unnecessary; I commented it out,
rerun the regress tests
(TESTS='test_setup rpr_base rpr_nfa rpr_explain rpr_integration rpr'
meson test -C $BUILD3 --num-processes 20 --suite regress --verbose)
Only two SQL tests in rpr_explain.sql failed.

SELECT first_value(id) OVER w AS match_start FROM stock_ticks
WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED
FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B) {2}) DEFINE A
AS price < 100, B AS price < 100);

The query above invokes the following code. Since the PATTERN above is
not greedy, is the comment below incorrect?
``````
    else
    {
        /* Greedy: enter first, skip second */
        state->elemIdx = elem->next;
        nfa_route_to_elem(winstate, ctx, state,
                          &elements[state->elemIdx], currentPos);
        if (skipState != NULL)
        {
            nfa_route_to_elem(winstate, ctx, skipState,
                              &elements[elem->jump], currentPos);
        }
    }
``````

nfa_advance_var
```
    else if (canExit)
    {
        state->counts[depth] = 0;
        state->elemIdx = elem->next;
        nextElem = &elements[state->elemIdx];
        /*
         * Update isAbsorbable for target element (monotonic: AND preserves
         * false)
         */
        state->isAbsorbable = state->isAbsorbable &&
            RPRElemIsAbsorbableBranch(nextElem);
        /* See comment above: increment outer END count for quantified VARs */
        if (RPRElemIsEnd(nextElem))
        {
            if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
                state->counts[nextElem->depth]++;
        }
        nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
    }
```
The above ELSE IF overrides all RPRNFAState field values except
RPRNFAState->next.
Should we set RPRNFAState->next to NULL?
(If I add ``state->next = NULL;`` in the above ELSE IF branch, all the
regress tests still pass)
Some comments about the situation of (RPRNFAState) state->next would be great.
This also happens in `` if (canLoop && canExit)``, ``if (reluctant)`` branch.

For function nfa_advance_var, I don't understand the meaning of the
variable "count", after the first Assert I have added below:

    if (count > 2 && !state->isAbsorbable && RPRElemIsReluctant(elem))
        elog(INFO, "count is %d and elem is reluctant and isAbsorbable
is false XXXXX", count);
    if (count > 2 && state->isAbsorbable && RPRElemIsReluctant(elem))
        elog(INFO, "count is %d and elem is reluctant and isAbsorbable
is true XXXXX", count);

Rerunning the regress tests shows that count >= 3 occurs very infrequently.
I'm don't understand isAbsorbable, so I am not sure if the second
``elog(INFO`` is actually reachable or not.
Can we add more complex queries (more count >= 3) to check if the
"count" variable is working correctly?

In function nfa_add_state_unique:
    /* Mark VAR in visited before duplicate check to prevent DFS loops */
    winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
        ((bitmapword) 1 << BITNUM(state->elemIdx));

I honestly don't understand the purpose of the code block above. But it doesn't
seem to influence the subsequent FOR LOOP;
It only modifies its own variables without any other side effects within
nfa_add_state_unique. Could we add some comments explaining which external
functions rely on this code and why it belongs in nfa_add_state_unique?

nfa_states_equal
compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
The comment above isn't helpful, IMHO, and I don't understand it.
We should focus on why compareDepth should be ```elem->depth + 1```.

function nfa_add_state_unique return value is not being used?
Do we need to do something with the return value, or is this expected?
(I don't have an opinion on it, I guess it would be better to raise this issue)

In nfa_advance_alt, during the main WHILE loop, I think altElem->depth
must be larger than elem->depth.
Therefore we can do
``````
        if (altElem->depth == elem->depth)
            elog(ERROR, "nfa_advance_alt altElem->depth should not be
the same as elem->depth reached");
        if (altElem->depth < elem->depth)
            break;
``````

---

From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 07:31:02 +0900

Hi Tatsuo, jian,

> functionalities but just for completeness I created a patch on top of

Thanks for catching this. I applied the patch and the RPR regress passes
cleanly. I also re-ran it with debug_raw_expression_coverage_test turned
on (on an assert-enabled build), and the full regress is green as well.

One observation while testing: the GUC catches missing case handlers
once the walker actually reaches a node, but it cannot flag a missing
WALK on its own -- if no caller drives the walker into a subtree, the
omission stays silent. So your inspection was the part that found the
gap; the GUC just confirms the patch closes it. With the patch in, RPR
raw subtrees are on the safety net for any future node-type additions.

I'll include the patch in v48 as nocfbot-0015. My suggestion would
be to defer the fold until the jian-response patches (numbered from
0016 onward, which I'll be sending shortly) have also gone through a
review round, so the whole bundle can land together in one pass.
Patch attached below for convenience.

jian -- thanks for the thorough review. It covers a lot of ground,
and I'm still working through it. Current expectation is that most
items will be accepted; a few smaller ones may end up with a
different conclusion than your suggestion, and those are still under
analysis on my side.

The plan is to turn the responses into a patch series and send them
out for another round of review. More to follow once the batch is in
shape.

Thanks,
Henson
From dbefdd47ab63c099b8cb9e7a7af3907ed2212dcd Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 07:02:38 +0900
Subject: [PATCH] Add raw_expression_tree_walker coverage for RPR raw nodes

WindowDef.rpCommonSyntax was not walked, and there were no case
arms for T_RPCommonSyntax or T_RPRPatternNode.  RPR core was
unaffected -- contain_rpr_walker() in parse_cte.c intercepts
WindowDef before delegating -- but debug_raw_expression_coverage_test
silently skipped these subtrees, leaving any future raw-node
omission on the RPR side undetected.
---
 src/backend/nodes/nodeFuncs.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 734bb0554fe..101c03b6ae8 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4641,6 +4641,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(wd->endOffset))
 					return true;
+				if (WALK(wd->rpCommonSyntax))
+					return true;
 			}
 			break;
 		case T_RangeSubselect:
@@ -4896,6 +4898,24 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_RPCommonSyntax:
+			{
+				RPCommonSyntax *rc = (RPCommonSyntax *) node;
+
+				if (WALK(rc->rpPattern))
+					return true;
+				if (WALK(rc->rpDefs))
+					return true;
+			}
+			break;
+		case T_RPRPatternNode:
+			{
+				RPRPatternNode *rp = (RPRPatternNode *) node;
+
+				if (WALK(rp->children))
+					return true;
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));
-- 
2.50.1 (Apple Git-155)

---

From: Tatsuo Ishii <[email-masked]>
Date: Wed, 27 May 2026 11:49:06 +0900 (JST)

Hi Henson,

> raw subtrees are on the safety net for any future node-type additions.

Thanks for checking.

> Patch attached below for convenience.

Sure. I will wait for your ready-to-go signal before creating the v48
pach set.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 14:49:44 +0900

Hi jian, Tatsuo,

Thanks for the thorough first read of execRPR.c and the NFA. The
depth of understanding you reached on a fairly intricate body of
code in a short window is impressive -- and several of the open
questions in your review pointed straight at documentation and
comment debt on our side that needed exposing. First-reviewer
feedback of that kind is especially valuable, because the gaps it
surfaces are exactly the ones the next reader would otherwise have
to re-discover; closing them now puts everyone reading the code
afterwards on firmer ground. Would be glad to keep working together
on this area.

> comments below with a grain of salt.)

Agreed on the unified v48 -- both the scattered incrementals and
the responses to your review will be part of that series. The
responses below will land alongside their corresponding change
(README, comment, test, or commit) in that series rather than as
a free-standing reply that defers the actual fixes.

> returning false is not rare, I think.

Agreed on the optimization intent. A slightly different shape worth
considering: rather than eager short-circuit in nfa_match, defer
DEFINE predicate evaluation to first use -- varMatched[i] computed
on demand and cached per row, so pruned paths never pay for
predicates they didn't reach. The code change is small; the part
needing care is whether "not evaluating" is safe for every predicate
(navigation state, slot setup, side effects). The current code or
the standard may already enforce constraints that make lazy
evaluation naturally safe, but I'd rather not act on that assumption
without concrete grounding in what the code actually enforces or in
test coverage that pins it down.

Would you be interested in driving the discussion in this thread?
You've been deep in execRPR.c recently, so the trade-offs sit
closest to you, and I think Tatsuo will land a good conclusion once
they're on the table. Happy to support the discussion in whatever
way is useful as it converges.

> We can also mention that 'DFS' refers to Depth-First Search".

Acknowledged, and the request surfaced an underlying problem in the
README's terminology. "Absorption" is currently used for two
distinct things: an AST-level rewrite in Phase 1 that pulls
identical sequences around a group inside it, and the runtime
context-equivalence collapse that drives the O(n^2) -> O(n)
optimization. Sharing the word leaves a reader encountering
"absorbable" early on without an anchor.

Rather than disambiguate by qualifier ("prefix/suffix absorption"
vs "context absorption"), I'd lean toward renaming the AST-level
case so "absorption" stays reserved for the runtime concept. The
README then only needs to explain absorption in one place, in
detail, without the disambig preamble.

For the rename, "prefix/suffix merging" feels like the natural fit
-- the other AST-level optimizations in the same Phase 1 are already
named "consecutive variable / group / ALT merging", so it slots in
cleanly. "Prefix/suffix factoring" is another candidate if a more
descriptive verb is preferred.

Tatsuo, curious what you think of this direction and naming. Happy
to take any name you prefer for the AST-level operation, or to keep
the original "absorption" wording with stronger forward-references
if you'd rather not rename.

For the absorption explanation itself in README.rpr, the diagnosis
I'd offer is that Chapter VIII already carries the necessary content
-- the issue is narrative order. VIII-1 leads with the O(n^2) problem
framing, so a reader meets the cost shape before meeting the
intuition for why absorption is possible, and has to carry the
problem until VIII-2's monotonicity argument finally lands. Beyond
that, VIII-2 stays abstract; there is no row-by-row trace showing
two states being judged equivalent.

Two small additions seem to close most of the gap:

  (A) A 4-5 line intuition summary at the top of Chapter VIII,
      before VIII-1, naming what absorption is (collapsing contexts
      that have converged on identical future behavior) and the
      monotonicity principle that makes it safe. This gives the
      reader an anchor before the problem framing.

  (B) A short worked example at the end of VIII-2: a PATTERN (A+)
      trace over a few rows showing each new context being absorbed
      by Context_1 once its (elemIdx, depth-0 count) is dominated.
      Concrete state/count comparisons make the abstract solution
      land.

Curious if this read of the gap matches what tripped you up, and
whether (A) + (B) feel sufficient. Happy to draft both as part of
the v48 README changes.

For DFS, will expand it to "Depth-First Search (DFS)" at the first
occurrence.

> In V47, the variable `initialAdvance` does not exist.

Leftover from an earlier patch version -- the boolean parameter was
refactored away and the README notation wasn't updated. I'll bring
it in line with the current signature.

> Assert(elem->next <  pattern->numElements);

Agreed. Will add it right after Assert(canLoop || canExit) in
nfa_advance_var, with a >= 0 lower bound tacked on while there
(RPRElemIdx is signed int16, INVALID = -1):

  Assert(elem->next >= 0 && elem->next < pattern->numElements);

> Only two SQL tests in rpr_explain.sql failed.

Reproduced this. You're right on correctness and memory: data rows
are identical with the call removed, and release_partition's
MemoryContextReset reclaims memory anyway.

Finalize isn't really about handling matches. By the time the
partition ends, all genuine FIN reaches have already been recorded
in-flight. Its job is to kill any VAR states still pursuing when
rows run out, so cleanup sees a uniform ctx->states == NULL across
every context. Three shapes survive there:

  - Pure pursuit (no matchedState, e.g., A+ B mid-pattern).
  - Empty-match candidate + pursuit (matchedState set with
    matchEndRow < matchStartRow -- e.g., greedy A* with no
    successful matches yet, while VAR is still chasing a longer
    non-empty one).
  - Real match + pursuit (matchedState set with matchEndRow >=
    matchStartRow -- e.g., greedy A* with some matches recorded
    and still looping for a longer one).

The first two get reclassified as failures by cleanup; without
Finalize they linger without contributing to stats. The third is
stat-neutral -- cleanup skips it either way -- but goes through
the same uniform path so partition-end classification stays
centralized.

The classification surfaces today only via rpr_explain stats, but
becomes user-visible once we extend the R020 surface or move into
R010 -- MEASURES and eventual R010 hooks count matches based on
this classification. Worth locking in now, and an explicit
partition-end stage is structurally cleaner than scattering the
logic.

Plan: keep the call and reframe it as the partition-end
classification policy holder. Strategically, that gives the future
partition-end hooks a single anchor to extend, instead of growing
scattered end-of-partition paths.

>         /* Greedy: enter first, skip second */

The comment is misleading; the code is correct. Two pieces:

First, greedy vs reluctant: '?' plays two distinct roles in our
grammar. As a quantifier on its own (A?, (A B)?) it means "optional"
-- equivalent to {0,1}. As a suffix on another quantifier (A+?,
(A B)*?, {n}?, etc.) it makes that quantifier reluctant. So {n}
without a trailing '?' is greedy; the {n}? form is reluctant.
PATTERN ((A B){2}) is in fact greedy.

Second, why the branch is entered: nfa_advance_begin's else arm
handles two cases at once:

  (a) Greedy with optional group: skipState != NULL, not reluctant.
      "Enter the group; also create the skip path."
  (b) Non-nullable group (min > 0, regardless of greedy/reluctant):
      skipState stays NULL, the outer guard
      "if (skipState != NULL && RPRElemIsReluctant(elem))" falls
      through, and the inner "if (skipState != NULL)" prevents the
      skip-path action from running.

(A B){2} has min = max = 2, so it lands in (b) -- the action that
actually runs is "enter the group", no skip path. The current label
only describes (a), which is why it reads wrong for your test query.
Plan is to rewrite the comment along the lines of
"Greedy-or-non-nullable: route to the first child; for optional
groups (skipState != NULL), additionally create the skip path."

> regress tests still pass)

Good catch on the asymmetry. Tracing it through, state->next is
actually already NULL at every branch you flagged: nfa_advance
resets it just before crossing into nfa_advance_state, and the
intermediate branches don't disturb it. Your experiment passing
with an added "state->next = NULL" is consistent with that -- the
assignment is redundant rather than load-bearing.

The contract that keeps state->next sane lives at two concentrated
points (nfa_advance entry, nfa_add_state_unique linking), and the
branches in between are pass-through. Sprinkling the same reset at
every branch would be defensive noise rather than a real safety
net, so I'd leave the branches alone.

Happy to add a short comment near nfa_advance's reset marking it
as the boundary contract, so the next reader doesn't trip on the
same question.

> "count" variable is working correctly?

The "count" semantics will read more cleanly once the absorption
README work above lands (counts[d] = iteration count at nesting
depth d). For coverage I'll add a nested reluctant quantifier to
rpr_nfa (e.g., PATTERN ((A B){3,5}? C)) to drive count through the
3..5 band repeatedly. (rpr_nfa is the suite that already targets
Quantifier Runtime Behavior and Absorption Optimization.)

> Could we add some comments explaining which external functions rely on
> this code and why it belongs in nfa_add_state_unique?

The code is correct, but the contract is split across two functions
and currently only one side points to the other. The visited marking
scheme is asymmetric on purpose:

  - Non-VAR elements (END/ALT/BEGIN/FIN) are marked on entry to
    nfa_advance_state because epsilon cycles must be prevented
    immediately.
  - VAR elements are marked later, in nfa_add_state_unique, only
    when added to the state list. That delay is intentional: it
    keeps legitimate quantifier loop-back to the same VAR across
    iterations possible.

The paired cycle check sits in nfa_advance_state, and the
asymmetric-marking rationale is documented there. What's missing is
the back-reference from nfa_add_state_unique. I'll add a single line
at the marking site pointing back to nfa_advance_state.

> We should focus on why compareDepth should be ```elem->depth + 1```.

Agree the trailing comment is too terse. Two pieces are missing:

  (a) The +1 arithmetic: to compare counts up to depth N, we need
      slots counts[0..N], which is N+1 entries.
  (b) Why deeper slots are excluded: counts[d > elem->depth] are
      scratch state from deeper groups and get reset on re-entry,
      so they must not participate in equivalence judgment.

Two states sharing elemIdx are equivalent iff all
enclosing-or-current depth counts match. I'll replace the trailing
comment with a small block covering both pieces.

> (I don't have an opinion on it, I guess it would be better to raise this
issue)

Leftover from an earlier design -- the duplicate case is fully
handled inside the function (the state is freed and nfaStatesMerged
is incremented), so callers have nothing to branch on, and indeed
none of them do. Will change the signature from bool to void and
drop the return statements.

>         if (altElem->depth < elem->depth)

I had to push back on this one. Tracing the depth bookkeeping:

  - For an ALT at depth D, branches sit at depth D+1, and each
    branch's first element has .jump pointing to the next branch's
    first (set in fillRPRPatternAlt). So the walk normally
    terminates when the last branch's .jump = INVALID -- the depth
    check doesn't fire at all.
  - But when the last branch is a quantified group, its first
    element is a BEGIN whose .jump = past-END (set by
    fillRPRPatternGroup and not overridden for the last branch).
    The walk then steps to a post-ALT element, and the depth check
    is what stops it from creating a stray state out there.

That post-ALT element has depth <= D:

  * D-1 if the ALT is inside an enclosing group with a non-trivial
    quantifier, e.g., PATTERN ((A | (B C)+){2}) -- post-ALT lands
    on the outer END at depth 0, ALT at depth 1. (A {1,1} outer
    wrap gets removed by single-child unwrap, so it has to be a
    real quantifier.)
  * D if the ALT has a sibling at the same level, e.g.,
    PATTERN (A | (B C)+) at top level -- post-ALT is FIN at depth 0,
    matching the ALT's depth 0.

So "altElem->depth == elem->depth" is a legitimate end-of-walk
signal for the quantified-group-last-branch case, not an invariant
violation. Treating it as an error would misfire on patterns like
A | (B C)+. The current "if (altElem->depth <= elem->depth) break;"
in nfa_advance_alt is intentionally <= and not <, and the looser
comparison is correct. Happy to add a brief comment there noting
the trigger condition, if it would help future readers.

Summary of decisions, in the order above:

  Short-circuit optimization       Separate series -- invite you to drive
  Absorption README narrative      Accept -- Chapter VIII summary + example
  AST-level "absorption" rename    Pending Tatsuo's call -- prefix/suffix
merging?
  DFS expansion                    Accept
  initialAdvance README mismatch   Accept -- align with current signature
  Defensive Assert in advance_var  Accept -- also add lower bound
  Finalize unnecessary?            Keep -- partition-end policy holder
  Greedy comment label             Accept -- rewrite to cover both cases
  state->next reset                Decline -- boundary contract covers it
  count >= 3 test coverage         Accept -- add to rpr_nfa
  visited marking purpose          Accept -- add back-reference comment
  compareDepth comment             Accept -- rewrite with intent
  Unused bool return               Accept -- change to void
  ALT depth invariant Assert       Decline -- end-of-walk signal, not
invariant

That's the full pass. The actual patches (nocfbot-0016 onward) will
follow shortly as a separate submission, for another review round.

Thanks,
Henson

---

From: jian he <[email-masked]>
Date: Thu, 28 May 2026 21:23:59 +0800

Hi.

In src/include/nodes/execnodes.h, we're adding quite a few fields to
WindowAggState that are only used for RPR queries.
Should we consolidate these fields behind a single pointer (named
RPRContext) to keep the WindowAggState size smaller for non-RPR
queries?

In function get_reduced_frame_status, I made the following changes:
``````
bool conditionA;
bool conditionB;
bool conditionC;
bool conditionD;
if (!winstate->rpr_match_valid)
    return RF_NOT_DETERMINED;
conditionA = (pos == start && winstate->rpr_match_matched && length == 0);
conditionB = (pos < start || pos >= start + length);
conditionC = !winstate->rpr_match_matched;
conditionD = (pos == start);
if ((conditionA && conditionB) || (conditionA && conditionC) ||
    (conditionA && conditionD))
{
    if (conditionA)
        elog(INFO, "conditionA is true");
    if (conditionB)
        elog(INFO, "conditionB is true");
    if (conditionC)
        elog(INFO, "conditionC is true");
    if (conditionD)
        elog(INFO, "conditionD is true");
}
if ((conditionB && conditionC) || (conditionB && conditionD) ||
(conditionC && conditionD))
{
    elog(INFO, "more than 2 branch is true, should not be reached");
    if (!(conditionC && conditionD))
        elog(INFO, "not (conditionCD is true)");
    if ((conditionB && conditionD))
        elog(INFO, "conditionB and conditionD is true)");
}
``````
I re-ran the regression tests and noticed it's entirely possible for multiple
conditions to be true at once. Because of this, all the IF blocks in
get_reduced_frame_status (after the initial one) are order-dependent. If I move
one IF statement above another, get_reduced_frame_status may return a
different result.
Is this the expected behavior? It seems like a potential logic flaw.

------------------------------------------------------------------------------------
VIII-3. Absorption Conditions

Planner-time prerequisites (all must hold for absorption to be enabled):

  (a) SKIP PAST LAST ROW.  SKIP TO NEXT ROW creates overlapping
      contexts that cannot be safely absorbed.
  (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
      FOLLOWING).  Limited frames apply differently to each context,
      breaking the monotonicity principle.
  (c) No match_start-dependent navigation in DEFINE.

      Mechanism: each context has a different matchStartRow, so FIRST
      resolves to a different row for each context at the same
      currentpos.  An earlier context's DEFINE result no longer
      subsumes a later one's, making count-dominance comparison
      invalid.  Rather than comparing matchStartRow at runtime
      (which would complicate the absorb path), any match_start
      dependency disables absorption entirely.

      Navigation content              match_start dep.  absorption
      ------------------------------------------------------------
      No navigation                   none              safe
      PREV/NEXT only                  none              safe
      LAST (no offset)                none              safe
      LAST (with offset)              boundary check    unsafe
      FIRST (any)                     direct            unsafe
      Compound (inner FIRST)          direct            unsafe
      Compound (inner LAST, no off.)  none              safe
      Compound (inner LAST, w/off.)   boundary chk      unsafe
------------------------------------------------------------------------------------
"boundary chk" should be "boundary check".
column "match_start dep.", I don't understand the meaning of "direct"
and "boundary check".
"count-dominance" seems like an invented word, but I could not find
the explanation.
"match_start-dependent" occurred many times, it would also deserve an
explanation.
We can also rename it as match_start_dependent.

----------------------------------------------------
Attached is a minor refactoring of ExecRPRProcessRow, no need for
boolean hasLimitedFrame.
``if (hasLimitedFrame && winstate->endOffsetValue != 0)`` seems wrong
to me, endOffsetValue is a Datum,
and you directly compare it with 0.
see also calculate_frame_offsets.
----------------------------------------------------
ExecRPRProcessRow,
nfa_evaluate_row
```
  if (!window_gettupleslot(winobj, pos, slot))
        return false;            /* No row exists */
```
indicates that it's not possible for (currentPos > ctxFrameEnd).
therefore
`````
    if (currentPos >= ctxFrameEnd)
    {
        /* Frame boundary exceeded: force mismatch */
        nfa_match(winstate, ctx, NULL);
        continue;
    }
`````
should change to ```if (currentPos == ctxFrameEnd)```.
With that, the comment wording "exceeded" seems wrong too.

The comment 'Frame boundary exceeded' also needs updating since
it's no longer exceeding the boundary.
----------------------------------------------------
Since I couldn't generate a coverage report, I am using elog(INFO) to test
reachability. Rerun the regress tests, you will find out that the ELSE IF branch
below is not reached by current regress tests.
``````
    else if (targetCtx->states == NULL)
    {
        /* Context already completed - skip to result registration */
        goto register_result;
        elog(INFO, "XXXX reached");
    }
``````

---

From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 10:28:29 +0900

Hi Jian,

Thanks for the careful read of execRPR.c and the NFA -- several of these
landed on real gaps in our comments/docs. Going through your points
inline below, with a summary of decisions at the end.

> RPRContext) to keep the WindowAggState size smaller for non-RPR

The size win is real -- it's roughly 450-500 bytes per WindowAggState
that every non-RPR window query carries today. But it takes a wide
code change to get there: it reshapes every RPR access path, a
sizable (if mostly mechanical) diff across nodeWindowAgg.c,
execRPR.c, and the explain side.

Tatsuo, as co-author -- do you want this in v48? If you do, I'll
prepare the RPRContext consolidation as an incremental patch for you to
fold into v48. It isn't blocking either way.

> [conditionA..D probe elided]
> I re-ran the regression tests and noticed it's entirely possible for
> multiple conditions to be true at once. Because of this, all the IF
> blocks in get_reduced_frame_status (after the initial one) are
> order-dependent. If I move one IF statement above another,
> get_reduced_frame_status may return a different result.

You're right that the conditions aren't mutually exclusive and that
reordering the branches changes the result -- but that's the
cascade-with-early-return idiom, standard in C and used throughout the
backend, not a logic flaw. Each branch is the minimal test given the
negations the earlier returns have already established.

heapam_visibility.c is the canonical example: the HeapTupleSatisfies*
family cascades through ordered early returns, with the later XMAX
branches correct only because the XMIN-committed invariant above holds.

The order is correct, not just idiomatic: update_reduced_frame() only
ever records (matched, length) as (false, 1), (true, 0), or (true, >=1),
and A->B->C->D is the order that classifies exactly those three cases --
reorder it and one of them gets misclassified.

I'll add short "by here, ..." comments between the branches to make the
invariants visible, but keep the structure. If you feel strongly it
should be restructured, I'd defer to Tatsuo on that.

> explanation. We can also rename it as match_start_dependent.

All fair. v48 fixes the typo and adds a terminology block to README.rpr
defining count-dominance (the VIII-3 cover condition, named there),
match_start_dependent (renamed to the underscore spelling, to match the
defineMatchStartDependent identifier), and the "direct" vs "boundary
check" navigation distinction.

> to me, endOffsetValue is a Datum, and you directly compare it with 0.

Good eye on the Datum comparison, and I agree your version reads more
clearly -- gating on the frame-option flag and going through
DatumGetInt64() rather than comparing a Datum to 0, plus the two new
Asserts. (The existing code is correct, since hasLimitedFrame carries
the limited/unbounded distinction separately, but the refactor is the
clearer shape.) I'll take it.

One thing to watch when dropping hasLimitedFrame, though: a zero offset
is still a valid limited frame -- ROWS BETWEEN CURRENT ROW AND CURRENT
ROW, and ... AND 0 FOLLOWING -- so the new check must not treat offset 0
as unbounded, otherwise the boundary check is skipped and a quantified
pattern (A+) silently absorbs the whole partition. The limited/unbounded
distinction lives in the frame-option flag, not the offset value, so the
refactor has to keep that flag in the picture.

We don't currently cover the offset-0 quantified case -- the two
zero-offset frame tests in rpr_base both use a plain PATTERN (A) -- so
I'll add an A+ regression test for it.

> [nfa_evaluate_row's window_gettupleslot returning false]
> therefore [if (currentPos >= ctxFrameEnd) ...]
> should change to ``if (currentPos == ctxFrameEnd)``.

Agreed. `>` is unreachable by the loop invariant -- currentPos advances
by exactly one per row, and once a context is finalized the
states == NULL guard skips it -- so >= and == behave identically here,
and == states the intent better. The `>=` was a defensive guard against
an overshoot that can't actually happen; v48 tightens it to ==, changes
the comment from "exceeded" to "reached", and moves that defense into an
Assert(currentPos <= ctxFrameEnd) so a future change that breaks the
invariant trips immediately instead of silently.

> Since I couldn't generate a coverage report, I am using elog(INFO) to
> test reachability. Rerun the regress tests, you will find out that the
> ELSE IF branch below is not reached by current regress tests.

The elog in your snippet sits *after* the goto, so it can never print
even though the branch runs. Moving it above the goto shows it firing
across the suite. Easy mistake; I've put a probe right after a
goto/return more times than I'd like to admit.

The branch is correct as written: under SKIP TO NEXT ROW an overlapping
context can finish (and be preserved by cleanup, since
matchEndRow >= matchStartRow) before the next update_reduced_frame()
call lands on its start row. So no new test is needed -- I'll just add a
comment above it explaining why the head context can already be complete
there.

  RPRContext consolidation         Tatsuo's call -- non-blocking for v48
  get_reduced_frame_status order   Keep -- cascade idiom; add comments
  README terminology + typo        Accept -- definitions block + rename
  ExecRPRProcessRow refactor       Accept -- but fix frameOffset
  currentPos >= -> ==              Accept -- plus comment + Assert
  states == NULL branch coverage   Already reached (155x) -- add comment

I'll post an incremental patch shortly with the accepted changes and the
comment additions; the RPRContext question is for Tatsuo.

Thanks again,
Henson

---

From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 18:41:52 +0900

Hi Jian, Tatsuo,

Jian, your review keeps paying off beyond the items themselves.  Working
through the ExecRPRProcessRow refactor you proposed -- dropping
hasLimitedFrame and fixing the "endOffsetValue != 0" Datum-vs-0
comparison -- and writing the offset-0 regression test it calls for sent
me back to the standard, where I hit a conformance reading I'd value both
of your judgments on.

The case is the single-row full window frame.  It has two spellings,
semantically identical -- both make the full window frame exactly the
current row:

    ROWS BETWEEN CURRENT ROW AND CURRENT ROW
    ROWS BETWEEN CURRENT ROW AND 0 FOLLOWING

PostgreSQL accepts both today (rpr_base tests the 0 FOLLOWING form with
PATTERN (A)).  But I'm no longer sure either is sanctioned by Subclause
6.10.2, "ROWS BETWEEN CURRENT ROW AND", which reads (paraphrasing
ISO/IEC 19075-5):

  "When performing row pattern recognition in a window, only two options
   are allowed for specifying the window frame extent:
     - ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     - ROWS BETWEEN CURRENT ROW AND offset FOLLOWING ... [the offset]
       shall be a positive integer ..."

Two things put the single-row frame outside a literal reading, and they
hit both spellings equally:

  - CURRENT ROW as the *end* bound is not one of the two listed options;
    and

  - the offset form requires a *positive integer*, and 0 is not positive
    -- so AND 0 FOLLOWING is no better sanctioned than AND CURRENT ROW.

The two spellings therefore stand or fall together: they denote the same
one-row frame, and neither is literally among 6.10.2's options.
(README.rpr X-4 already follows the strict line -- it lists only
"UNBOUNDED FOLLOWING or n FOLLOWING".)

Either way the behavior is unambiguous: a single-row search space, so the
pattern can only ever map the current row.  That makes it a degenerate,
WHERE-like use rather than real row-pattern matching -- which one might
read as benign (so allow) or as pointless (so reject), so it doesn't
settle the question for me.

This is, in effect, a borderline, harmless standard violation: it sits
just outside 6.10.2 as written, yet is well-defined, runs cleanly, and
leaves every conforming frame unchanged.

So, concretely, the choice:

  (a) Allow it -- accept both spellings and document the degenerate
      frame as a deliberate extension; or

  (b) Forbid it -- follow 6.10.2 to the letter ("only two options" and a
      "positive integer" offset) and reject both spellings in
      transformRPR().

Which reading do you take?  Tatsuo, as co-author your call carries it;
Jian, I'd value yours too, since you were just in this code.

This isn't blocking.  The implementation accepts offset 0 today, so for
now my regression test exercises it (via the 0 FOLLOWING spelling, as
rpr_base already does); if the answer is (b), that test and the existing
one become error tests together.

Thanks,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Sat, 30 May 2026 11:47:37 +0900 (JST)

Hi Henson,

> one become error tests together.

I vote for (b).

Reason 1: (a) does not give clear benefit to users.

Reason 2: we haven't implemented some large features yet (MEASURES
etc.)  If we follow (a), we may have to add more code to support it
in the process of implementing MEASURES when they are not necessary if
we had chosen (b).

Reason 3: In the future, we may find an optimization if 0 FOLLOWING is
prohibited.

Regards,

---

From: jian he <[email-masked]>
Date: Sat, 30 May 2026 14:45:25 +0800

Hi.

-- Consecutive VAR merge: A A+ -> a{2,}
-- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
-- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
             PATTERN (A A+) DEFINE A AS val > 0);

-- Consecutive VAR merge: A A+ -> a{2,}
-- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
-- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
             PATTERN (A A+) DEFINE A AS val > 0);

Comments like "Tests line 251" should not appear in SQL, since the
line number would change.
So, we need to rephrase this comment. Please don't delete it, it may
clarify the patch for future readers.

/*
 * nfa_add_state_unique
 *
 * Add a state to ctx->states at the END, only if no duplicate exists.
 * Returns true if state was added, false if duplicate found (state is freed).
 * Earlier states have better lexical order (DFS traversal order), so
existing wins.
 */
static bool
nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
RPRNFAState *state)

The "at the END" confuses me. It may also refers to RPR_VARID_END /
RPRElemIsEnd.
How about something like:
"Add the state to the end of the ctx->states linked list, but only if
a duplicate state is not already present."

I think a bunch of these uppercase ENDs should just be lowercase "end",
capitalizing them may collides visually with RPR_VARID_END and RPRElemIsEnd and
makes the comments harder to read.

In nfa_match:
"Non-VAR elements (ALT, END, FIN) are kept as-is for advance phase."

Here END does mean RPRElemIsEnd, right? That one's probably fine as uppercase
since it's listed alongside other element kinds.

But then:
/*
 * Evaluate VAR elements against current row. For VARs that reach max
 * count with END next, advance through END chain inline so absorb phase
 * can compare states at judgment points.
 */
I'm think END here means the tail of the RPRNFAContext->RPRNFAState linked
list, not the element kind -- which is exactly the ambiguity I'm worried about.

Could we lowercase the list-end ones and keep uppercase only when actually
referring to the END element kind?

The comment under nfa_advance_var is confusing (for me).
/* After a successful match, count >= 1, so at least one must be true */

nfa_advance_var doesn't actually know anything about match status (i
think), it can be reached with count == 0.
I added this elog(INFO) to confirm:
    if (count < 1 && currentPos > -1)
        elog(INFO, "should not reach, count is %d, currentPos=%ld",
count, currentPos);

...and it does fire, so the comment's premise isn't quite right.

------------------------------------------------
please check the attached minor refactoring.
1.  Refactor struct WindowAggState: Remove the nfaStateSize and nfaVisitedNWords
fields because they are constant, and we can easily compute it, seems
not necessary to stay in WindowAggState.

2. Rename nfa_context_alloc() to nfa_context_make(), nfa_state_alloc()
to nfa_state_make(), nfa_state_create() to nfa_state_clone().
Rationale: We have makeNode, changing "alloc" to "make" would be more
intuitive, IMHO.
In tablecmd.c, we have CloneForeignKeyConstraints, which is based on
existing information, creating a new node, here we are doing the same
in
nfa_state_create.

3. Refactor functions: nfa_advance_alt, nfa_advance_begin,
nfa_advance_end, and nfa_advance_var.
Because the (RPRPatternElement *elem) parameter is unnecessary.

---

From: Henson Choi <[email-masked]>
Date: Sat, 30 May 2026 22:45:39 +0900

[mailing-list reply draft -- pgsql-hackers, RPR thread.
 In-reply-to Jian's round-3 review, 2026-05-30 06:45
 (Message-ID CACJufxEsaU8GQ4yeXTWhAO8VjbrZTh5CpvUqz=
[email-masked]).
 Covers R3-1..R3-4 (comments) and the three attached refactors R3-5a/b/c.
 All accepted; fold into the v48 refactor series.]

Hi Jian,

Thanks for the round-3 pass -- all seven are good catches. Going through
them inline below, with a summary of decisions at the end.

> clarify the patch for future readers.

Agreed on both counts -- the intent (this case exercises the
child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars, where a
finite prev meets an infinite child) is worth keeping; only the "line 251"
anchor is fragile. I'll rephrase it to name the branch and the condition
it covers instead of the line, so it survives any later shuffle.

There's a parallel one a few tests down -- "Tests line 325 ... in
mergeConsecutiveGroups" -- with the same problem, so I'll fix both
together on the same principle.

> a duplicate state is not already present."

Your wording is clearer than mine -- I'll take it almost verbatim. "at the
END" there meant the list tail, and capitalizing it was exactly the wrong
choice next to the END element kind.

> [...]

Yes -- that's the right rule, and I'll go through them on it: lowercase
"end" for the linked-list tail, uppercase END only when it means the
element kind (RPRElemIsEnd / RPR_VARID_END). On the two you singled out:

  - nfa_match, "Non-VAR elements (ALT, END, FIN) ..." -- element kind,
    listed with the others, stays uppercase. Agreed.

  - "advance through END chain inline ..." -- this one is actually the
    element kind too: it walks the chain of group-END *elements* up to the
    absorption judgment point, not the state list. So it stays uppercase,
    but since it tripped you up I'll reword it to "chain of END elements"
    to make that unambiguous rather than leaning on the reader to infer it.

> [...] and it does fire, so the comment's premise isn't quite right.

You're right, and thanks for confirming it with the elog. nfa_advance_var
is part of the advance phase, which generates every reachable next state
regardless of what matched -- so it's routinely entered with count == 0,
for a VAR just reached through an ALT branch or a group that hasn't been
evaluated yet. The comment carries match-phase reasoning into a phase that
has no notion of matching. The Assert above still holds, but on the
structure of a VAR's bounds, not on any prior match, so I'll reword the
comment to say that instead.

> 1.  Refactor struct WindowAggState: Remove the nfaStateSize and
>     nfaVisitedNWords fields because they are constant, and we can easily
>     compute it, [...] not necessary to stay in WindowAggState.
>    Rationale: We have makeNode [...]. In tablecmd.c, we have
>    CloneForeignKeyConstraints, which is based on existing information,
>    creating a new node, here we are doing the same in nfa_state_create.
>    nfa_advance_end, and nfa_advance_var. Because the
>    (RPRPatternElement *elem) parameter is unnecessary.

Looked at all three. 0002 and 0003 I'll take; 0001 I'd split.

  1. The two fields differ. nfaVisitedNWords -- yes: in the current tree
     it's read only once, at init, to size the visited bitmap, since the
     per-row reset clears just the high-water [min,max] range. Demoting it
     to a local is clean. (Heads up: the patch is on v47, before that
     high-water-mark change, so its nfa_advance hunk no longer applies and
     drops out on rebase.)

     nfaStateSize I'd keep. The patch recomputes it inside the allocator,
     i.e. on every state allocation -- the hottest path in the engine; a
     pathological (V1|...|Vk)* can allocate hundreds of millions of states.
     It's a loop-invariant fixed at init, so I'd rather keep it precomputed
     than rebuild offsetof + maxDepth*4 each alloc -- the same reasoning I
     gave for keeping the frame offsets out of the per-row loop.

  2. make/clone: agreed, and there's a closer precedent than I'd have
     reached for -- our own regex NFA engine already clones states
     (clonesuccessorstates / cloneouts in regcomp.c), so clone is the right
     word for duplicating an NFA state, not just the CloneForeignKey sense.
     The pair reads well too: make for the blank allocation, clone for
     building a state from an existing one's counts. (create was itself an
     earlier rename of mine from clone "for clarity," so this just lands us
     back on clone, which I now agree is better.) When I apply it I'll also
     tidy two comments the rename left behind -- the nfa_state_make header
     now reads "a new RPRNFAState state," and nfa_state_clone's body comment
     still says "Create."

  3. elem parameter: here I'd keep it, on the same grounds as nfaStateSize.
     The four functions have a single caller, nfa_advance_state, which
     already computes elem = &elements[state->elemIdx] for its own
     mark-visited check and switch, and hands it down. Dropping the
     parameter makes each function rebuild that same address on the
     per-dispatch advance path -- discarding a value the caller already
     holds. And with one trusted caller, the mismatch the change guards
     against can't really arise. The cost either way is tiny; I'd just
     rather keep the value flowing from where it's already computed than
     recompute it, which is the same call I made on nfaStateSize.

  "Tests line N" test comments     Rephrase -- name the branch, not the
line (both)
  "at the END" wording             Accept -- your wording, near-verbatim
  uppercase END cleanup            Accept -- lowercase the list-end ones,
keep the kind
  nfa_advance_var count comment    Reword -- wrong premise; state the
structural reason
  0001 nfaVisitedNWords            Accept -- demote to a local
  0001 nfaStateSize                Keep -- hottest path; leave it
precomputed
  0002 make / clone rename         Accept -- plus tidy two stray comments
  0003 drop the elem parameter     Keep -- single caller already holds it

So the next revision carries the four comment fixes, 0002, and the
nfaVisitedNWords half of 0001; nfaStateSize and the elem parameter I'd keep
as they are. Happy to be talked round on either if you see a cost I'm
missing.

Thanks again,
Henson

---

From: Henson Choi <[email-masked]>
Date: Sat, 30 May 2026 23:08:38 +0900

Hi all,

Resolved since the last post:

  D1. Single-row frame conformance, Subclause 6.10.2 -- Tatsuo's call [6]
      was to reject. Both ROWS BETWEEN CURRENT ROW AND CURRENT ROW and
      AND 0 FOLLOWING are now rejected (nocfbot-0025), which in turn
      unblocks the held ExecRPRProcessRow change (nocfbot-0026).

Open decisions (repeated with context at the end):

  D2. The RPRContext consolidation -- Tatsuo's call as co-author;
      non-blocking either way [4].
  D3. The AST "absorption" rename -- Tatsuo's call [2].

  [1] Jian's review, round 1 (2026-05-26):

https://postgr.es/m/CACJufxH-DZePhbdJM=[email-masked]
  [2] my round-1 reply -- AST "absorption" rename deferred to Tatsuo (D3,
2026-05-27):

https://postgr.es/m/CAAAe_zDephfiDA_A3FN0hCymJRogEr=[email-masked]
  [3] Jian's review, round 2 (2026-05-28):

https://postgr.es/m/[email-masked]
  [4] my round-2 reply -- RPRContext deferred to Tatsuo (D2, 2026-05-29):

https://postgr.es/m/[email-masked]
  [5] single-row frame conformance question (D1, 2026-05-29):

https://postgr.es/m/CAAAe_zCbSU=[email-masked]
  [6] Tatsuo's reply -- reject single-row frames (D1 resolved, 2026-05-30):

https://postgr.es/m/[email-masked]
  [7] Jian's review, round 3 (2026-05-30):

https://postgr.es/m/CACJufxEsaU8GQ4yeXTWhAO8VjbrZTh5CpvUqz=[email-masked]

Attached: the v47 feature series (v47-0001..0009) rebased onto current
master, plus the incremental patch series carried on top of it.

Base:

  9a41b34a287  2026-05-26  doc:  add comma to UPDATE docs, for consistency

Unchanged -- rebase only (already posted; only rebased, no content change).
Titles for reference:

  nocfbot-0001  Add DEFINE non-volatile baseline to rpr_integration B9
  nocfbot-0002  Unify RPR DEFINE walkers and reject volatile callees
  nocfbot-0003  Cover RPR empty-match path with EXPLAIN tests; fix stale
XXX comments
  nocfbot-0004  Reclassify DEFINE qualifier check and reword diagnostic to
"expression"
  nocfbot-0005  Sync stale comments on DEFINE/PATTERN handling
  nocfbot-0006  Add trailing commas to RPR enum definitions
  nocfbot-0007  Remove optional outer parentheses from ereport() calls in
RPR files
  nocfbot-0008  Add high-water mark tracking to NFA visited bitmap reset
  nocfbot-0009  Document DEFINE subquery rejection as intentional
over-rejection
  nocfbot-0010  Remove duplicate #include in nodeWindowAgg.c
  nocfbot-0011  Normalize SQL/RPR standard references
  nocfbot-0012  Add rpr_integration B7 cases for RPR in recursive query
  nocfbot-0013  Reject row pattern recognition in recursive queries
  nocfbot-0014  Enhance README.rpr per Tatsuo Ishii's review
  nocfbot-0015  Round out README.rpr WindowAggState field coverage
  nocfbot-0016  Add raw_expression_tree_walker coverage for RPR raw nodes

  (nocfbot-0016 was sent earlier as 0015; renumbered here so the review
  series runs contiguously from 0017.)

New incremental patches -- nocfbot-0017 onward. These apply Jian He's
review (rounds 1 [1] and 2 [3]) and settle D1. nocfbot-0017..0024 and
0026 are comment / doc / test plus a signature change and a couple of
Assert additions -- no behavior change. nocfbot-0025 is the one
user-visible change: it rejects the single-row frame per D1 [6].

  nocfbot-0017  Enhance README.rpr
      Chapter VIII absorption intro + a worked PATTERN (A+) trace;
      "Depth-First Search" spelled out at first use; the stale
      nfa_advance(initialAdvance=...) reference replaced.

  nocfbot-0018  Clarify execRPR.c comments and tighten an Assert
      Document the NFA invariants (compareDepth slot arithmetic, the
      asymmetric visited-marking scheme, the greedy/non-nullable BEGIN
      label, the ALT depth break, the state->next reset boundary),
      reframe ExecRPRFinalizeAllContexts as the partition-end policy
      holder, and add a defensive Assert in nfa_advance_var.

  nocfbot-0019  nfa_add_state_unique: bool return -> void
      The return value was unused.

  nocfbot-0020  Reluctant bounded mid-band test (rpr_nfa)
      A{3,5}? B, which drives the VAR-level count in nfa_advance_var
      through 3..5.

  nocfbot-0021  Define RPR absorption terminology in README.rpr
      Terminology block for the "match_start dep." column (none /
      direct / boundary check), the "boundary chk" typo fix, the
      count-dominance definition, and the match_start_dependent rename.

  nocfbot-0022  Document the get_reduced_frame_status cascade invariant
      The branches form an order-dependent early-return cascade; the
      running invariant is spelled out so the order reads as intentional.

  nocfbot-0023  Explain the completed-head-context branch in
                update_reduced_frame
      Why a head context can already be complete under SKIP TO NEXT ROW.

  nocfbot-0024  Tighten the frame-boundary check from >= to ==
      The > case is unreachable by the loop invariant; the defense moves
      into an Assert(currentPos <= ctxFrameEnd), and the comment changes
      from "exceeded" to "reached".

  nocfbot-0025  Reject single-row window frame in row pattern recognition
      Per D1 [6], the frame end must be UNBOUNDED FOLLOWING or a positive
      offset FOLLOWING. CURRENT ROW is rejected in transformRPR() at parse
      time; a zero offset -- which need not be a constant -- in
      calculate_frame_offsets() at run time. The two single-row rpr_base
      tests become error cases, with a bind-parameter error test added.

  nocfbot-0026  Remove the redundant zero check on the RPR frame ending
                offset
      With the single-row frame now rejected, a limited frame always
      carries a real offset, so the "endOffsetValue != 0" guard -- which
      compared a Datum directly to zero -- is dropped, leaving a plain
      DatumGetInt64(). This is the offset half of Jian's ExecRPRProcessRow
      cleanup; the structural half (dropping the hasLimitedFrame
      parameter) I've left as-is -- it is loop-invariant and computed once
      outside the per-row loop, so moving it into ExecRPRProcessRow would
      just repeat the work each row.

Coverage -- my decision summaries [2], [4], with the patch each became:

  Round 1 [2]
    Short-circuit optimization       Separate series   -> separate series
    Absorption README narrative      Accept            -> nocfbot-0017
    AST-level "absorption" rename    Pending Tatsuo    -> D3
    DFS expansion                    Accept            -> nocfbot-0017
    initialAdvance README mismatch   Accept            -> nocfbot-0017
    Defensive Assert in advance_var  Accept            -> nocfbot-0018
    Finalize unnecessary?            Keep              -> nocfbot-0018
    Greedy comment label             Accept            -> nocfbot-0018
    state->next reset                Decline           -> nocfbot-0018
    count >= 3 test coverage         Accept            -> nocfbot-0020
    visited marking purpose          Accept            -> nocfbot-0018
    compareDepth comment             Accept            -> nocfbot-0018
    Unused bool return               Accept            -> nocfbot-0019
    ALT depth invariant Assert       Decline           -> nocfbot-0018

  Round 2 [4]
    RPRContext consolidation         Tatsuo's call     -> D2
    get_reduced_frame_status order   Keep              -> nocfbot-0022
    README terminology + typo        Accept            -> nocfbot-0021
    ExecRPRProcessRow refactor       Datum fix only    -> nocfbot-0026
    single-row frame (6.10.2, D1)    Reject (Tatsuo)   -> nocfbot-0025
    currentPos >= -> ==              Accept            -> nocfbot-0024
    states == NULL branch coverage   Already reached   -> nocfbot-0023

Remaining work and decisions

  Decisions (need your input):
    D2  RPRContext consolidation -- Tatsuo.
    D3  AST "absorption" rename -- Tatsuo.

  Held pending a decision:
    - the RPRContext consolidation itself -- done if D2 says go.
    - the AST "absorption" rename itself -- done if D3 says go.

  (D1 settled: the parse-time frame-end check, the offset-0 test as an
   error case, and the Datum-comparison fix are now in nocfbot-0025/0026.)

  From Jian's round-3 review [7] (into the next revision):
    - four comment fixes: the line-number test anchors, the "at the END"
      and uppercase-END wording, and the nfa_advance_var count premise.
    - the make/clone rename of the NFA helpers (one of three attached
      refactors); the other two -- dropping nfaStateSize and the elem
      parameter -- I'd keep, both on hot-path grounds.

  From our in-house review (separate follow-ups):
    - a quantifier-normalization correctness fix (nested unbounded
      quantifiers such as (A{2,})* are currently mis-normalized)
    - a per-tuple memory-context fix in DEFINE evaluation (still verifying)
    - smaller correctness/conformance fixes (an overflow guard, a few
      missing parse-time checks, EXPLAIN output details)
    - documentation gaps (comments, README, SGML)
    - added regression tests (round-trip deparse, edge-case offsets)

  Before the v48 fold (from Jian's off-list comments):
    - INT_MAX -> PG_INT32_MAX (the unbounded-quantifier sentinel; ~24 sites)
    - foreach + lfirst() -> foreach_node (~33 sites)
    - foreach_current_index, dropping the redundant break (3 sites)

  Work, no decision needed:
    - short-circuit (lazy eval) -- stop evaluating a DEFINE predicate
      once its outcome is fixed. A separate series.
      It turns on a standard-interpretation point -- whether skipping is
      sound when a dropped subexpression has side effects.

Thanks again to Jian for the careful reading.

Henson
From b3f8437ddb973537c75374f7c51dccfe30f33d95 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 19:09:34 +0900
Subject: [PATCH 01/26] Add DEFINE non-volatile baseline to rpr_integration B9

 

From ed80669ff1e75f728ae0df74f56bf4d838fbb8da Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 11:36:53 +0900
Subject: [PATCH 04/26] Reclassify DEFINE qualifier check and reword diagnostic
 to "expression"

index 1e9cee36e35..4221643d9c8 100644
 
-	/*
 
 	/*----------

 
 
 -- Semantic errors
 
-- 
2.50.1 (Apple Git-155)

From aadff39d2d9749e6999650c5987e05b4c01bd941 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 14:29:14 +0900
Subject: [PATCH 03/26] Cover RPR empty-match path with EXPLAIN tests; fix
 stale XXX comments

 

 

 

 

From 63e574a07dc0d8b35589dd20954db72bd5f335a0 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 17:21:56 +0900
Subject: [PATCH 06/26] Add trailing commas to RPR enum definitions

 
 /*
 
 /*

From 737ff7a37869fb3b2d1b755fc7c8236238ba98d1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 17:27:51 +0900
Subject: [PATCH 07/26] Remove optional outer parentheses from ereport() calls
 in RPR files

 

 /*

 		ereport(ERROR,

 		ereport(ERROR,

 
 			if (!strcmp(n, name))
 				ereport(ERROR,

 

 
 			/*
 
 			*ctx = saved;
-- 
2.50.1 (Apple Git-155)

From fc00ddd90fce1026366ff3dc9042e3edb28f1ccf Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:00:48 +0900
Subject: [PATCH 09/26] Document DEFINE subquery rejection as intentional
 over-rejection

index 4221643d9c8..10edccb8afb 100644

From 5dcb8f07457f058c707c80c9c793c4fa38b34794 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 11:44:45 +0900
Subject: [PATCH 05/26] Sync stale comments on DEFINE/PATTERN handling

 
 
    <para>
 
 	/*
 
 	/*

From 3206380000abc3c9b06308fd44be809835ff4514 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 21:37:59 +0900
Subject: [PATCH 02/26] Unify RPR DEFINE walkers and reject volatile callees

index f1cd9b66098..200d8a49001 100644
@@ -247,8 +247,7 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);

 /*
  * Not null info bit array consists of 2-bit items
@@ -2580,12 +2579,10 @@ ExecWindowAgg(PlanState *pstate)
 			{
 				int64		firstreach;

@@ -3038,17 +3035,13 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 
@@ -4020,42 +4013,64 @@ eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
 
 /*

 
@@ -4068,168 +4083,123 @@ eval_nav_max_offset_walker(Node *node, void *ctx)

 
 
-typedef struct
-{
 
 /*
 
 	ctx.winstate = winstate;

 

 /*
 
 /*
 
 /*
- * nav_offset_walker
 
-	if (node == NULL)
-		return false;
+	/*

 
-		/*

 
-		/*

-		/*

 
 /*
- * compute_nav_offsets
 
 	ctx.maxOffset = 0;

 
 
-/*

 	/*

 

 #include "miscadmin.h"

 	return result;
 }
+
+/*
+ * nav_traversal_walker
 
 #include "postgres.h"

 
 /*
  * transformRPR

 
 /*
- * check_rpr_nav_expr
 
+/*
+ * define_walker
 
 	if (node == NULL)
 		return false;
 
-static void

 

-/*
 
+/*

 
 RESET plan_cache_mode;

 

 

--- Volatile (random) is the prohibition target; today still accepted.
+-- Volatile (random) is rejected.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ce587068bca..1970ca5da14 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -662,7 +662,10 @@ DecodingWorkerShared
 DefElem
 DefElemAction
 DefaultACLInfo
+DefineMetadataContext
+DefinePhase
 DefineStmt
+DefineWalkCtx
 DefnDumperPtr
 DeleteStmt
 DependenciesParseState
@@ -761,8 +764,7 @@ ErrorData
 ErrorSaveContext
 EstimateDSMForeignScan_function
 EstimationInfo
-EvalNavFirstContext
-EvalNavMaxContext
+EvalDefineOffsetsContext
 EventTriggerCacheEntry
 EventTriggerCacheItem
 EventTriggerCacheStateType
@@ -1833,8 +1835,8 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
-NavCheckResult
-NavOffsetContext
+NavTraversal
+NavVisitFn
 NestLoop
 NestLoopParam
 NestLoopState
-- 
2.50.1 (Apple Git-155)

From fe4b0403012b6521c75ebf3558ddc77cad1d85db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:37:51 +0900
Subject: [PATCH 10/26] Remove duplicate #include in nodeWindowAgg.c

index 188445d779c..99858d22dad 100644

From c92392c482936604a42dfe085a604396e830c8b8 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 22:31:06 +0900
Subject: [PATCH 08/26] Add high-water mark tracking to NFA visited bitmap
 reset

 
+/*

 
 	switch (elem->varId)
 	{

 		state = states;
 		states = states->next;
index 200d8a49001..188445d779c 100644
@@ -3055,6 +3055,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)

-- 
2.50.1 (Apple Git-155)

From 03b98374a3adc253e81e27eb7f7da268b86664bf Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 9 May 2026 13:47:49 +0900
Subject: [PATCH 11/26] Normalize SQL/RPR standard references

 

 X-3. INITIAL vs SEEK

 			/*
index 10edccb8afb..78abdc88f86 100644
 
 			/*----------

 

 

 

From a4e467b40c8fe7640b95bd3576d26c0fbd2a764f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 10 May 2026 13:51:45 +0900
Subject: [PATCH 12/26] Add rpr_integration B7 cases for RPR in recursive query

 

From 7ac04a8f46dcdcb3bfe9db3bb106d88993d3b725 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 15:38:03 +0900
Subject: [PATCH 14/26] Enhance README.rpr per Tatsuo Ishii's review

 

   Related code:

 

-III-2. PATTERN AST

 

 
 Memory management:

 
 
+
+References:
+

From a8458a955c831d181e199358875e07e2ce8ff684 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 10 May 2026 14:42:09 +0900
Subject: [PATCH 13/26] Reject row pattern recognition in recursive queries

 
 
 /*
  * transformWithClause -
 
+		/*
 
 /*
 
 -- ============================================================
 -- B8. RPR + Incremental sort
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1970ca5da14..24cf2eb7860 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -532,6 +532,7 @@ Constraint
 ConstraintCategory
 ConstraintInfo
 ConstraintsSetStmt
+ContainRPRContext
 ControlData
 ControlFileData
 ConvInfo
-- 
2.50.1 (Apple Git-155)

From 1f82e001031d9b0716433614e94f3cc69adc33df Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 15:43:49 +0900
Subject: [PATCH 15/26] Round out README.rpr WindowAggState field coverage

 

From 993f4423e5c4176d3a8ccb819a57192d226d9cef Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 07:02:38 +0900
Subject: [PATCH 16/26] Add raw_expression_tree_walker coverage for RPR raw
 nodes

From 74cd8fd045529f8f299ad91ea9f4ca38b239a57a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 13:34:23 +0900
Subject: [PATCH 17/26] Enhance README.rpr per Jian He's review

- Add an intuition summary at the top of Chapter VIII naming
  what context absorption is and the monotonicity principle
  that makes it safe, so the reader meets the core idea before
  the O(N^2) problem framing.
- Add a worked PATTERN (A+) trace at the end of VIII-2 to make
  the state/count dominance comparison concrete.
- Expand "DFS" to "Depth-First Search (DFS)" at first occurrence.
- Replace the stale "nfa_advance(initialAdvance=true)" reference
  in VI-2 with the current signature.
---
 src/backend/executor/README.rpr | 34 +++++++++++++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6ff7f33e62e..6d40bd70faa 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -354,7 +354,8 @@ The flag is set on all elements that carry the quantifier:
   Simple VAR (A+?):     RPR_ELEM_RELUCTANT on the VAR element
   Group ((...)+?):      RPR_ELEM_RELUCTANT on BEGIN and END elements
 
-At runtime (nfa_advance), the flag controls DFS exploration order:
+At runtime (nfa_advance), the flag controls Depth-First Search
+(DFS) exploration order:
 
   VAR with quantifier:
     Greedy:    primary path = next (continue), clone = jump (skip)
@@ -582,7 +583,8 @@ Creates a new context and performs the initial advance.
   (2) Set matchStartRow = pos
   (3) Create initial state: elemIdx=0 (first pattern element),
       counts=all zero
-  (4) Call nfa_advance(initialAdvance=true)
+  (4) Call nfa_advance() with currentPos = pos - 1 (no row consumed
+      yet)
 
 The initial advance expands epsilon transitions at the beginning of
 the pattern. For example, the initial advance for PATTERN ((A | B) C):
@@ -737,6 +739,15 @@ Immediate advance for simple VARs:
 Chapter VIII  Phase 2: Absorb (Context Absorption)
 ============================================================================
 
+Absorption is the runtime optimization that collapses contexts which
+have converged on identical future behavior.  Two contexts are
+treated as equivalent when one's bookkeeping (elemIdx and per-depth
+iteration counts) is dominated by another's; the younger one is then
+discarded.  The optimization is safe because pattern matching is
+monotonic -- an earlier context's reachable matches always contain a
+later context's.  This is what reduces the naive O(N^2) state count
+to O(N).
+
 VIII-1. Problem
 
 In the current implementation, a new context is started for each row
@@ -762,6 +773,25 @@ is already contained within Context 1.
 
 Therefore Context 2 can be "absorbed" into Context 1.
 
+Worked example for PATTERN (A+) over 3 rows (each matches A):
+
+  After row 1:
+    Ctx_1 (started row 1): state at A with counts[0] = 1
+
+  After row 2:
+    Ctx_1: state at A with counts[0] = 2
+    Ctx_2 (started row 2): state at A with counts[0] = 1
+    -> Same elemIdx; Ctx_1.count (2) dominates Ctx_2.count (1).
+    -> Ctx_2 absorbed.
+
+  After row 3:
+    Ctx_1: state at A with counts[0] = 3
+    Ctx_3 (started row 3): state at A with counts[0] = 1
+    -> Ctx_1.count (3) dominates Ctx_3.count (1).
+    -> Ctx_3 absorbed.
+
+Total active contexts stays at O(1) instead of growing with N.
+
 VIII-3. Absorption Conditions

From 0e2913edd72ac20d40f18aed2c9b891f780cfa9d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 14:02:19 +0900
Subject: [PATCH 19/26] Change nfa_add_state_unique signature from bool to void

The return value is leftover from an earlier design.  All four
callers ignore it, and the duplicate-found case is fully handled
inside the function (the new state is freed and nfaStatesMerged is
incremented).  Drop the return value and update the doc comment.
---
 src/backend/executor/execRPR.c | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 261e1209744..88c59cf3276 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -61,7 +61,7 @@ static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
 									 int32 *counts, bool sourceAbsorbable);
 static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
 							 RPRNFAState *s2);
-static bool nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
+static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
 								 RPRNFAState *state);
 static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
@@ -335,10 +335,10 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
  * nfa_add_state_unique
  *
  * Add a state to ctx->states at the END, only if no duplicate exists.
- * Returns true if state was added, false if duplicate found (state is freed).
- * Earlier states have better lexical order (DFS traversal order), so existing wins.
+ * Earlier states have better lexical order (DFS traversal order), so existing
+ * wins; the new state is freed when a duplicate is found.
  */
-static bool
+static void
 nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *state)
 {
 	RPRNFAState *s;
@@ -365,7 +365,7 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 			 */
 			nfa_state_free(winstate, state);
 			winstate->nfaStatesMerged++;
-			return false;
+			return;
 		}
 		tail = s;
 	}
@@ -376,8 +376,6 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 		ctx->states = state;
 	else
 		tail->next = state;
-
-	return true;
 }
 
 /*
-- 
2.50.1 (Apple Git-155)

From 71ee26f3954ebebb9c22aa2d279544edaa0a4d4f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 13:44:55 +0900
Subject: [PATCH 18/26] Clarify execRPR.c comments and tighten an Assert per
 Jian He's review

- nfa_advance_var: add Assert that elem->next is within bounds, as
  any reachable VAR's next pointer must be a valid index.
- nfa_advance: document the state->next reset point as the boundary
  contract for the epsilon-expansion DFS, naming the other linking
  site (nfa_add_state_unique) as the pair.
- nfa_add_state_unique: back-reference the asymmetric visited-marking
  scheme, whose primary explanation sits in nfa_advance_state.
- nfa_states_equal: rewrite the compareDepth comment to explain both
  the +1 slot arithmetic and why deeper slots are excluded.
- nfa_advance_begin: replace the misleading "Greedy: enter first,
  skip second" label with one that covers both greedy-optional and
  non-nullable cases.
- nfa_advance_alt: explain when the depth break actually fires
  (quantified-group last branch) and why <= is the correct relation.
- ExecRPRFinalizeAllContexts: reframe as the partition-end
  classification policy holder, enumerating the three context shapes
  that survive into Finalize and how each is handled.
---
 src/backend/executor/execRPR.c | 75 ++++++++++++++++++++++++++++++----
 1 file changed, 68 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index e1caa7bb528..261e1209744 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -311,9 +311,19 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 	if (s1->elemIdx != s2->elemIdx)
 		return false;
 
-	/* Compare counts up to current element's depth */
+	/*
+	 * Compare counts up to current element's depth.  Two states sharing
+	 * elemIdx are equivalent iff every enclosing-or-current depth count
+	 * matches.
+	 *
+	 * The +1 is the slot arithmetic: comparing through depth N requires
+	 * counts[0..N], i.e., N+1 entries.  Deeper slots (counts[d] with d >
+	 * elem->depth) are excluded because they hold scratch state from inner
+	 * groups that gets zeroed on re-entry (see END loop-back in
+	 * nfa_advance_end), and so must not participate in equivalence judgment.
+	 */
 	elem = &pattern->elements[s1->elemIdx];
-	compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
+	compareDepth = elem->depth + 1;
 
 	if (memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
 		return false;
@@ -334,7 +344,12 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	RPRNFAState *s;
 	RPRNFAState *tail = NULL;
 
-	/* Mark VAR in visited before duplicate check to prevent DFS loops */
+	/*
+	 * Mark VAR in visited before duplicate check to prevent DFS loops. This
+	 * is the deferred half of the asymmetric visited-marking scheme; see
+	 * nfa_advance_state for the non-VAR (END/ALT/BEGIN/FIN) half and the
+	 * rationale for the asymmetry.
+	 */
 	nfa_mark_visited(winstate, state->elemIdx);
 
 	/* Check for duplicate and find tail */
@@ -950,7 +965,16 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 		RPRPatternElement *altElem = &elements[altIdx];
 		RPRNFAState *newState;
 
-		/* Stop if element is outside ALT scope (not a branch) */
+		/*
+		 * Stop if element is outside ALT scope (not a branch).  The check
+		 * fires when the last branch is a quantified group whose BEGIN.jump
+		 * (set by fillRPRPatternGroup) is preserved -- not overridden by
+		 * fillRPRPatternAlt, which only links non-last branch heads -- and
+		 * leads to a post-ALT element.  Other branch shapes terminate the
+		 * walk earlier via altIdx = RPR_ELEMIDX_INVALID.  Use <=, not <: the
+		 * post-ALT element may sit at the same depth as the ALT when the ALT
+		 * has a sibling at that level.
+		 */
 		if (altElem->depth <= elem->depth)
 			break;
 
@@ -1016,7 +1040,12 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	}
 	else
 	{
-		/* Greedy: enter first, skip second */
+		/*
+		 * Greedy-or-non-nullable: route to the first child.  For optional
+		 * groups (skipState != NULL, greedy min=0) additionally create the
+		 * skip path; for non-nullable groups (skipState == NULL, min>0) the
+		 * skip-path action is suppressed by the guard below.
+		 */
 		state->elemIdx = elem->next;
 		nfa_route_to_elem(winstate, ctx, state,
 						  &elements[state->elemIdx], currentPos);
@@ -1205,6 +1234,9 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* After a successful match, count >= 1, so at least one must be true */
 	Assert(canLoop || canExit);
 
+	/* elem->next must be a valid index for any reachable VAR */
+	Assert(elem->next >= 0 && elem->next < pattern->numElements);
+
 	if (canLoop && canExit)
 	{
 		/*
@@ -1428,6 +1460,15 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 
 		state = states;
 		states = states->next;
+
+		/*
+		 * Boundary contract: state->next is reset to NULL here, before
+		 * crossing into nfa_advance_state's epsilon-expansion DFS.  The inner
+		 * branches (nfa_advance_var, nfa_advance_begin/end/alt) treat
+		 * state->next as already-NULL and don't reset it themselves; the
+		 * other linking site is nfa_add_state_unique, which sets it when
+		 * appending to ctx->states.
+		 */
 		state->next = NULL;
 
 		nfa_advance_state(winstate, ctx, state, currentPos);
@@ -1779,8 +1820,28 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 /*
  * ExecRPRFinalizeAllContexts
  *
- * Finalize all active contexts when partition ends.
- * Match with NULL to force mismatch, then advance to process epsilon transitions.
+ * Partition-end classification policy: kill any VAR states still pursuing
+ * when rows run out, so cleanup sees a uniform ctx->states == NULL across
+ * every context.  By the time this runs, all genuine FIN reaches have
+ * already been recorded in-flight; three shapes survive here:
+ *
+ *   - Pure pursuit (matchedState == NULL): VAR states waiting for input
+ *     that never arrives (e.g., A+ B mid-pattern at partition end).
+ *   - Empty-match candidate + pursuit (matchedState != NULL,
+ *     matchEndRow < matchStartRow): initial-advance FIN-via-skip recorded
+ *     an empty match while VAR states are still chasing a longer one
+ *     (e.g., greedy A*).
+ *   - Real match + pursuit (matchedState != NULL,
+ *     matchEndRow >= matchStartRow): a match has been recorded and VAR
+ *     states are still looping for a longer one.
+ *
+ * Killing the VAR reclassifies the first two as failures in cleanup
+ * (otherwise they linger without contributing to stats).  The third is
+ * stat-neutral -- cleanup skips it either way -- but goes through the
+ * same uniform path so partition-end classification stays centralized.
+ *
+ * Implementation: nfa_match with NULL forces VAR mismatch; nfa_advance
+ * then drains any remaining epsilon transitions.
  */
 void
 ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos)
-- 
2.50.1 (Apple Git-155)

From 2e062833fb363874896bc2ba2d931d6f522a55c4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 14:15:09 +0900
Subject: [PATCH 20/26] Add reluctant bounded mid-band test to rpr_nfa

PATTERN (A{3,5}? B) drives the VAR-level count in nfa_advance_var
through 3, 4, 5 within a single match attempt -- a band the
existing A{1,3}? B test does not exercise, because A and B match
the same row there and advance's early-termination path frees the
loop state before nfa_advance_var sees count > 2.

This explicitly exercises the count > 2 && reluctant &&
!isAbsorbable path that absorbability analysis structurally
constrains (reluctant quantifiers are excluded, so isAbsorbable
stays false).
---
 src/test/regress/expected/rpr_nfa.out | 38 +++++++++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 29 ++++++++++++++++++++
 2 files changed, 67 insertions(+)

diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index fe5bb324df0..1f494d2db34 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -2237,6 +2237,44 @@ WINDOW w AS (
   4 | {B,_} |             |          
 (4 rows)
 
+-- A{3,5}? B (reluctant bounded mid-band): the VAR-level count in
+-- nfa_advance_var cycles through 3, 4, 5 within a single match
+-- attempt.  Exercises the count > 2 && reluctant && !isAbsorbable
+-- branch (absorbability analysis excludes reluctant quantifiers, so
+-- isAbsorbable stays false for A).
+WITH test_reluctant_mid_band AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_mid_band
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{3,5}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         6
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {A}   |             |          
+  5 | {A}   |             |          
+  6 | {B}   |             |          
+(6 rows)
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 7a5b5c41b24..76dfc4d88bc 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -1554,6 +1554,35 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- A{3,5}? B (reluctant bounded mid-band): the VAR-level count in
+-- nfa_advance_var cycles through 3, 4, 5 within a single match
+-- attempt.  Exercises the count > 2 && reluctant && !isAbsorbable
+-- branch (absorbability analysis excludes reluctant quantifiers, so
+-- isAbsorbable stays false for A).
+WITH test_reluctant_mid_band AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_mid_band
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{3,5}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
-- 
2.50.1 (Apple Git-155)

From 20f719ff24df09bd47f7bbbeec0aacabba4f4969 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 17:45:13 +0900
Subject: [PATCH 21/26] Define RPR absorption terminology in README.rpr per
 Jian He's review

- Define the "match_start dep." column values (none, direct,
  boundary check) in VIII-3; the table listed them without saying
  what "direct" versus "boundary check" actually mean.
- Fix the "boundary chk" typo in that table to "boundary check".
- Name the cover condition "count-dominance" and spell out the
  comparison, linking back to the count-dominance reference in
  VIII-3(c).
- Rename the prose term "match_start-dependent" to
  "match_start_dependent" to match the defineMatchStartDependent
  identifier.
---
 src/backend/executor/README.rpr | 37 ++++++++++++++++++++++++++-------
 1 file changed, 30 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6d40bd70faa..1d211245a5b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -526,7 +526,7 @@ V-3. RPR Fields of WindowAggState
   nfaVisitedMinWord             Lowest bitmapword index touched since last reset
   nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
-  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
+  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start_dependent)
   nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
   EXPLAIN ANALYZE instrumentation counters are omitted here; see
@@ -631,7 +631,7 @@ the same row.
 
 The varMatched array is referenced later in Phase 1 (Match).
 
-VI-4. Per-Context Re-evaluation (match_start-dependent variables)
+VI-4. Per-Context Re-evaluation (match_start_dependent variables)
 
 DEFINE variables that depend on match_start (those containing FIRST,
 LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST)
@@ -801,7 +801,7 @@ Planner-time prerequisites (all must hold for absorption to be enabled):
   (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
       FOLLOWING).  Limited frames apply differently to each context,
       breaking the monotonicity principle.
-  (c) No match_start-dependent navigation in DEFINE.
+  (c) No match_start_dependent navigation in DEFINE.
 
       Mechanism: each context has a different matchStartRow, so FIRST
       resolves to a different row for each context at the same
@@ -820,7 +820,27 @@ Planner-time prerequisites (all must hold for absorption to be enabled):
       FIRST (any)                     direct            unsafe
       Compound (inner FIRST)          direct            unsafe
       Compound (inner LAST, no off.)  none              safe
-      Compound (inner LAST, w/off.)   boundary chk      unsafe
+      Compound (inner LAST, w/off.)   boundary check    unsafe
+
+  The "match_start dep." column classifies how the navigation ties a
+  DEFINE result to the context's matchStartRow:
+
+    none            Independent of matchStartRow.  The result depends
+                    only on currentpos (or a fixed offset from it), so
+                    every context evaluates it identically.
+    direct          Computed from matchStartRow itself -- FIRST counts
+                    forward from match start -- so the resolved row,
+                    and thus the result, differs per context.
+    boundary check  The resolved row is currentpos-relative (LAST with
+                    a backward offset, or a compound whose inner LAST
+                    carries an offset), but its in-range test is taken
+                    against the match range [matchStartRow, currentpos].
+                    The range bound differs per context, so the result
+                    can too.
+
+  Only "none" is safe for absorption; "direct" and "boundary check"
+  both make an earlier context's result stop subsuming a later one's
+  (see (c) above).
 
 Runtime conditions (evaluated per context pair):
 
@@ -828,10 +848,13 @@ Runtime conditions (evaluated per context pair):
   (2) allStatesAbsorbable of the target context is true
   (3) An earlier context "covers" all states of the target
 
-Cover condition (nfa_states_covered):
+Cover condition (nfa_states_covered) -- "count-dominance":
 
   A state with the same elemIdx exists in the earlier context,
-  and the count at that depth is greater than or equal -- then it is covered.
+  and the count at that depth is greater than or equal -- then it is
+  covered.  The earlier context's per-depth iteration count thus
+  dominates the later one's; this is the count-dominance comparison
+  referenced in VIII-3(c).
 
 VIII-4. Dual-Flag Design
 
@@ -1515,7 +1538,7 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineVariableList: List<String> (variable names, DEFINE order)
     |--- defineClauseList: List<ExprState>
     |--- nfaVarMatched: bool[] (per-row cache)
-    |--- defineMatchStartDependent: Bitmapset* (match_start-dependent
+    |--- defineMatchStartDependent: Bitmapset* (match_start_dependent
     |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
     |--- nfaVisitedNWords: int (size of nfaVisitedElems)
-- 
2.50.1 (Apple Git-155)

From 381e30a5b89e088da66a7a60dde3239455b508d4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 19:07:40 +0900
Subject: [PATCH 24/26] Tighten the RPR frame-boundary check from >= to == per
 Jian He's review

currentPos advances by exactly one row per call, and a finalized context
is skipped by the states == NULL guard, so it can only ever reach
ctxFrameEnd, never overshoot it; >= and == behave identically here, and
== states the intent.  The >= was a defensive guard against an overshoot
that cannot happen -- move that defense into Assert(currentPos <=
ctxFrameEnd) so a future change that breaks the invariant fails
immediately instead of silently slipping past the boundary, and change
the comment from "exceeded" to "reached".  No behavior change.
---
 src/backend/executor/execRPR.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 88c59cf3276..4463cfe0a5c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1706,7 +1706,7 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		if (ctx->states == NULL)
 			continue;
 
-		/* Check frame boundary - finalize if exceeded */
+		/* Check frame boundary - finalize the context when it is reached */
 		if (hasLimitedFrame)
 		{
 			int64		ctxFrameEnd;
@@ -1716,9 +1716,18 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 									&ctxFrameEnd))
 				ctxFrameEnd = PG_INT64_MAX;
 
-			if (currentPos >= ctxFrameEnd)
+			/*
+			 * currentPos advances by exactly one per call, and a finalized
+			 * context is skipped by the states == NULL guard above, so it can
+			 * only ever reach ctxFrameEnd, never overshoot it.  The Assert
+			 * turns a future change that broke that invariant into an
+			 * immediate failure rather than a silent slip past the boundary.
+			 */
+			Assert(currentPos <= ctxFrameEnd);
+
+			if (currentPos == ctxFrameEnd)
 			{
-				/* Frame boundary exceeded: force mismatch */
+				/* Frame boundary reached: force mismatch */
 				nfa_match(winstate, ctx, NULL);
 				continue;
 			}
-- 
2.50.1 (Apple Git-155)

From 35fd6edcd5769a45a3071d5da6e75117631ede0b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 17:51:37 +0900
Subject: [PATCH 22/26] Document the get_reduced_frame_status cascade invariant
 per Jian He's review

The RF_* classifier is an early-return cascade whose branches are not
mutually exclusive, so reordering them changes the result.  This is the
standard cascade idiom -- each branch is a minimal test premised on the
negations the preceding returns have established -- not a logic flaw, but
the structure left the contract implicit.

update_reduced_frame() records the match as exactly one of three
(rpr_match_matched, rpr_match_length) shapes: (false, 1), (true, 0), or
(true, >= 1).  Spell that out in the header, add the missing RF_EMPTY_MATCH
return value, and annotate each branch with a "by here" note stating the
running invariant -- notably why the empty match must be classified before
the range test.  No behavior change.
---
 src/backend/executor/nodeWindowAgg.c | 28 ++++++++++++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 99858d22dad..4cf1a9ac67b 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4306,6 +4306,16 @@ clear_reduced_frame(WindowAggState *winstate)
  *   RF_FRAME_HEAD      pos is the start of the current match
  *   RF_SKIPPED         pos is inside the current match but not the start
  *   RF_UNMATCHED       pos is processed but not part of any match
+ *   RF_EMPTY_MATCH     pos is the start of an empty (zero-length) match
+ *
+ * update_reduced_frame() records the current match as exactly one of three
+ * (rpr_match_matched, rpr_match_length) shapes: (false, 1) for unmatched,
+ * (true, 0) for an empty match, and (true, >= 1) for a real match.  The
+ * tests below form a cascade with early returns: each is a minimal check
+ * that relies on the negations the preceding returns have already
+ * established, so their order is significant.  The "by here" notes spell
+ * out the running invariant; reordering a test would misclassify one of
+ * the three shapes.
  */
 static int
 get_reduced_frame_status(WindowAggState *winstate, int64 pos)
@@ -4316,17 +4326,31 @@ get_reduced_frame_status(WindowAggState *winstate, int64 pos)
 	if (!winstate->rpr_match_valid)
 		return RF_NOT_DETERMINED;
 
-	/* Empty match: covers only the start position */
+	/*
+	 * By here the record is valid and holds one of the three shapes above.
+	 *
+	 * The empty match (true, 0) must be classified first: it has length 0, so
+	 * the range test below would compute start + length == start and reject
+	 * its own start position as out of range.
+	 */
 	if (pos == start && winstate->rpr_match_matched && length == 0)
 		return RF_EMPTY_MATCH;
 
-	/* Outside the result range */
+	/*
+	 * By here length >= 1 -- the only zero-length record, the empty match,
+	 * has been handled -- so [start, start + length) is a well-formed range.
+	 */
 	if (pos < start || pos >= start + length)
 		return RF_NOT_DETERMINED;
 
+	/*
+	 * By here pos lies within [start, start + length).  An unmatched record
+	 * is (false, 1), so this returns for its single in-range position.
+	 */
 	if (!winstate->rpr_match_matched)
 		return RF_UNMATCHED;
 
+	/* By here the match is real (true, >= 1) and pos is one of its rows. */
 	if (pos == start)
 		return RF_FRAME_HEAD;
 
-- 
2.50.1 (Apple Git-155)

From 3a64a070fc113d9c90f8ec5b17121203b32658a3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 17:56:02 +0900
Subject: [PATCH 23/26] Explain the completed-head-context branch in
 update_reduced_frame per Jian He's review

ExecRPRGetHeadContext() can return a context whose state list is already
drained, and the branch handling it looked unreachable.  It fires under
SKIP TO NEXT ROW: overlapping contexts let one reach FIN, and record its
result, during an earlier call -- before the call asking about its own
start row arrives.  Comment only; no behavior change.
---
 src/backend/executor/nodeWindowAgg.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4cf1a9ac67b..f16d01e9743 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4434,7 +4434,12 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	}
 	else if (targetCtx->states == NULL)
 	{
-		/* Context already completed - skip to result registration */
+		/*
+		 * The head context already completed in an earlier call.  Reachable
+		 * under SKIP TO NEXT ROW, where overlapping contexts let one reach
+		 * FIN -- recording its result -- before the call for its own start
+		 * row arrives.  Register that result.
+		 */
 		goto register_result;
 	}
 
-- 
2.50.1 (Apple Git-155)

From 58fab018ae949e0e96083921125d19e841776ef4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 30 May 2026 18:57:56 +0900
Subject: [PATCH 25/26] Reject single-row window frame in row pattern
 recognition

The standard allows only UNBOUNDED FOLLOWING or a positive offset
FOLLOWING as the frame end for row pattern recognition.  A CURRENT ROW
end, or a zero offset, reduces the frame to the single current row, which
is not a valid search space for pattern matching.

Reject the CURRENT ROW spelling in transformRPR() at parse time, and a
zero offset in calculate_frame_offsets() at run time, since the offset
need not be a constant -- it may be a parameter, expression, or subquery.
---
 src/backend/executor/README.rpr        |  5 ++--
 src/backend/executor/nodeWindowAgg.c   | 10 +++++++
 src/backend/parser/parse_rpr.c         | 14 +++++++++
 src/test/regress/expected/rpr_base.out | 41 ++++++++++++++++++--------
 src/test/regress/sql/rpr_base.sql      | 26 ++++++++++++++--
 5 files changed, 80 insertions(+), 16 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 1d211245a5b..467cc03ecff 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -1122,8 +1122,9 @@ X-3. INITIAL vs SEEK
 X-4. Bounded Frame Handling
 
   With RPR, the frame mode is always ROWS and the frame start must be
-  CURRENT ROW. The frame end can be either UNBOUNDED FOLLOWING or n
-  FOLLOWING.
+  CURRENT ROW. The frame end must be UNBOUNDED FOLLOWING or a positive
+  offset (n >= 1) FOLLOWING; a CURRENT ROW end or a zero offset is
+  rejected, since it would reduce the frame to the single current row.
 
   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index f16d01e9743..770ea2e5e1a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2369,6 +2369,16 @@ calculate_frame_offsets(PlanState *pstate)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_PRECEDING_OR_FOLLOWING_SIZE),
 						 errmsg("frame ending offset must not be negative")));
+
+			/*
+			 * Row pattern recognition forbids a zero-length frame end;
+			 * checked here so a non-constant offset (e.g. a bind parameter)
+			 * is caught, not just a literal 0.
+			 */
+			if (winstate->rpPattern != NULL && offset == 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_WINDOWING_ERROR),
+						 errmsg("frame ending offset must be positive with row pattern recognition")));
 		}
 	}
 	winstate->all_first = false;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index d2ed6c14811..fa8c375f48b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -163,6 +163,20 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 				parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location));
 	}
 
+	/*
+	 * The standard allows only UNBOUNDED FOLLOWING or a positive offset
+	 * FOLLOWING as the frame end.  The equivalent 0 FOLLOWING spelling is
+	 * caught at runtime in calculate_frame_offsets().
+	 */
+	if (wc->frameOptions & FRAMEOPTION_END_CURRENT_ROW)
+		ereport(ERROR,
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use CURRENT ROW as frame end with row pattern recognition"),
+				errhint("Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
+
 	/* Transform AFTER MATCH SKIP TO clause */
 	wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
 
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index cfd2645bbed..d8f805c89aa 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -542,7 +542,8 @@ ERROR:  frame end cannot be UNBOUNDED PRECEDING
 LINE 5:     ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
                                          ^
 -- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
--- Single row frame: CURRENT ROW AND CURRENT ROW
+-- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
+-- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -553,17 +554,13 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   1
-  2 |  10 |   1
-  3 |  10 |   1
-  4 |  20 |   1
-  5 |  20 |   1
-  6 |  30 |   1
-(6 rows)
-
--- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+ERROR:  cannot use CURRENT ROW as frame end with row pattern recognition
+LINE 5:     ROWS BETWEEN CURRENT ROW AND CURRENT ROW
+            ^
+HINT:  Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING.
+-- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
+-- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -574,6 +571,22 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+ERROR:  frame ending offset must be positive with row pattern recognition
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+-- A non-constant frame end offset is allowed; a zero value is still rejected,
+-- this time at execution time (a literal cannot exercise that path).
+PREPARE rpr_end_offset(int8) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND $1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+EXECUTE rpr_end_offset(2);
  id | val | cnt 
 ----+-----+-----
   1 |  10 |   1
@@ -584,6 +597,10 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
+EXECUTE rpr_end_offset(0);
+ERROR:  frame ending offset must be positive with row pattern recognition
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+DEALLOCATE rpr_end_offset;
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index fd289d7cf67..6c2365a2d20 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -445,7 +445,8 @@ WINDOW w AS (
 );
 -- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 
--- Single row frame: CURRENT ROW AND CURRENT ROW
+-- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
+-- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -456,8 +457,10 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+-- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 
--- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
+-- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -468,6 +471,25 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+
+-- A non-constant frame end offset is allowed; a zero value is still rejected,
+-- this time at execution time (a literal cannot exercise that path).
+PREPARE rpr_end_offset(int8) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND $1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+EXECUTE rpr_end_offset(2);
+EXECUTE rpr_end_offset(0);
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+DEALLOCATE rpr_end_offset;
 
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
-- 
2.50.1 (Apple Git-155)

From 3bbf9ceef9aee65cab32ec0d4d668842bb89d0f0 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 30 May 2026 19:37:11 +0900
Subject: [PATCH 26/26] Remove the redundant zero check on the RPR frame ending
 offset

update_reduced_frame() guarded the frame offset with "endOffsetValue
!= 0", comparing a Datum directly to zero.  Now that a single-row frame
is rejected, a limited frame always carries a real offset, so the guard
is unnecessary; dropping it leaves a plain DatumGetInt64() and removes
the type-confused Datum-vs-zero comparison.

Per Jian He's review.
---

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 770ea2e5e1a..667d7b30cc9 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4399,7 +4399,7 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	 */
 	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
 		!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
-	if (hasLimitedFrame && winstate->endOffsetValue != 0)
+	if (hasLimitedFrame)
 		frameOffset = DatumGetInt64(winstate->endOffsetValue);
 
 	/*
-- 
2.50.1 (Apple Git-155)

---

From: Tatsuo Ishii <[email-masked]>
Date: Sun, 31 May 2026 09:11:19 +0900 (JST)

Hi Henson, Jian,

> fold into v48. It isn't blocking either way.

Basically I think Jian's idea is good. In addition to the size reason
above, we would have less code changes when we adapt existing R020
codes to R010.

However it will need a wide code change as Henson said. I would like
to focus on stabilizing our code for now. Therefore I would not want
the refactoring in v48.

Regards,

---

From: Tatsuo Ishii <[email-masked]>
Date: Sun, 31 May 2026 10:32:58 +0900 (JST)

Hi Henson,

> if you'd rather not rename.

Although I don't have any particular strong preferences, keeping
"absorption" for the runtime concept sounds good to me.

For AST level name changing, "prefix/suffix merging" seems to be
already used in other areas according to Google: LLM, Linker, and
string manipulation in DNA. In the normal expression engine area, it
looks like "flattening nested quantifiers" or "quantifiers reduction"
are used for the case. So, for example, "prefix/suffix quantifiers
reduction" seems to be more appropriate?  (If you don't mind it's too
long) In any case, I would like to respect your opinion.

Regards,

---

From: Tatsuo Ishii <[email-masked]>
Date: Sun, 31 May 2026 21:33:13 +0900 (JST)

Hi Henson,

> Attached is the v47 patches for Row pattern recognition (SQL/RPR).

I accidentaly noticed that v47-0002 changes findTargetlistEntrySQL99
from static to extern.

-static TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
-											 List **tlist, ParseExprKind exprKind);

I think this is not necessary anymore since findTargetlistEntrySQL99
is not used outside parse_clause.c.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 09:40:40 +0900

Hi Tatsuo, Jian,

Thanks, Tatsuo -- your two notes settle both open questions. Answering both
inline below, with a note at the end on the follow-up work.

> to focus on stabilizing our code for now. Therefore I would not want

Agreed -- out of v48, stabilize first. On R010, I'd treat it as the design
lens, not the schedule: it's far out, so rather than hold RPRContext back
until then, I'd do the consolidation at a sensible point after v48 but shape
it against R010 (what is shared versus per-context, how the fields group),
so
one engine core can later back both the R020 window path and R010 without a
second reshape. Not v48 -- on its own schedule once we're stable.

I'll admit the R010 connection had been nagging at me for a while without a
clean answer, and Jian's consolidation suggestion turns out to land right on
it: framed as a size win, it's really the structural move that opens the
R010
path -- that reframing is his. Which is why I'd rather shape it
deliberately,
as the shared engine core, than fold it in as churn.

> "absorption" for the runtime concept sounds good to me.

Good -- "absorption" stays reserved for the runtime context-equivalence
collapse, and the README explains it in one place.

> long) In any case, I would like to respect your opinion.

Thanks -- you're right that "merging" is well-worn elsewhere, and I'll be
honest that "prefix/suffix merging" isn't a term I'd defend on the merits.
Keeping it for v48 is really a stopgap to contain the ripple: the sibling
Phase-1 rewrites are already named "consecutive variable / group / ALT
merging", so switching to the "flattening / reduction" family would force
renaming those too for consistency. So I'd treat the term itself as
genuinely
open -- your "flattening / reduction" neighborhood is the right one -- and
converge on the established academic naming as the paper I'm preparing on
the
algorithm, together with a university research group, takes shape. For now
I'd
ship "prefix/suffix merging" only to keep the README internally consistent,
and fold the settled term into the glossary pass once the paper lands on
it. A
doc-level name is cheap to revise later.

Both of these -- the RPRContext reshape and the naming/terminology -- are
"after v48, by discussion" items, and they aren't the only ones. Rather than
pin down the scope of that follow-up now, I'd suggest we pick it up together
once v48 is stable.

Thanks again,
Henson

---

From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 10:16:54 +0900

Hi Tatsuo,

> is not used outside parse_clause.c.

Good catch -- you're right, and I'll revert it to static in v48. I
double-checked the current tree: the only callers left are inside
parse_clause.c (the findTargetlistEntry wrapper, plus the GROUP BY and ORDER
BY paths), so the extern prototype in parse_clause.h is now unused. The
revert is just dropping that prototype and restoring the static qualifier
with the in-file forward declaration it used to have.

It's worth saying why it went extern in the first place, since the reason is
no longer visible in the tree:

The DEFINE clause needs its referenced columns present in the plan's
targetlist to be evaluable at run time. The original implementation did
that from the RPR side, in parse_rpr.c, by calling
findTargetlistEntrySQL99()
with resjunk = true to add the missing entry -- and since that function was
static in parse_clause.c, reaching it across files is what required exposing
it as extern.

That approach added the whole DEFINE expression to the targetlist, and that
turned out to be the source of a SIGSEGV: when an RPR window and a plain
window coexist, the non-RPR WindowAgg inherited targetlist entries carrying
RPRNavExpr nodes it has no way to evaluate. The fix was to add only the Vars
a DEFINE references (with a guard in allpaths.c to keep those columns from
being pruned), and that is what removed the cross-file call. So the extern
has simply outlived its caller -- exactly as you spotted.

The one loose end there is an optimization, not a correctness issue: the
allpaths.c guard is deliberately coarse, so it also blocks removing a
WindowAgg whose RPR WindowFuncs are all unused. Doing that precisely means
restructuring remove_unused_subquery_outputs(), which runs for every
subquery and not just RPR -- broad enough that I'm treating it as a
longer-term item rather than part of this work. It doesn't bring the
external call back -- the Var-only path stays -- so the revert to static is
safe independently of it.

I'll fold the static revert into v48.

Thanks,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Mon, 01 Jun 2026 11:11:19 +0900 (JST)

Hi Henson,

> I'll fold the static revert into v48.

Thank for eplaining the history.

BTW, in v47-0002 patch,
there are some non ASCII characters ("§").

+	/*

Regards,

---

From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 11:29:21 +0900

Hi Tatsuo,

> there are some non ASCII characters ("§").

Good catch -- that one's already handled on top of v47. nocfbot-0004
("Reclassify DEFINE qualifier check and reword diagnostic to 'expression'")
reworded that comment, replacing the section signs with a spelled-out
reference: "prohibited by §6.5" became "prohibited by ISO/IEC 19075-5 6.5",
and likewise for §4.16. The signs you're looking at are an artifact of the
base v47-0002 patch, before that incremental.

To be sure it wasn't lurking elsewhere, I re-swept the whole RPR footprint
-- the sources, the headers, README.rpr, and the regression tests -- and
there are no non-ASCII characters left in the current tree. The fix folds
into v48.

Thanks,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Mon, 01 Jun 2026 11:47:03 +0900 (JST)

Hi Henson,

> there are no non-ASCII characters left in the current tree. The fix folds

Oh, I see. Sorry for noise.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 14:35:24 +0900

Hi Tatsuo, Jian,

While tidying RPR comments I found a small inconsistency in the varId
bounds.
The comment/README side I'm already fixing in the in-progress series;
whether
to also change the bounds is a separate follow-up.  As lead author that one
is
ultimately your call, Tatsuo, but I'd welcome Jian's and the list's input on
it first.

The current state, in src/include/optimizer/rpr.h:

  #define RPR_VARID_MAX   251
  #define RPR_VARID_BEGIN 252   /* control codes 252..255 */
  ... END 253, ALT 254, FIN 255

  RPRElemIsVar(e)  ==  ((e)->varId <= RPR_VARID_MAX)   /* 0..251 */

and the limit enforced in parse_rpr.c:

  if (list_length(*varNames) >= RPR_VARID_MAX)   /* reject the 252nd */
      ereport(ERROR, "too many pattern variables", "Maximum is 251");

So 251 variables are accepted as varId 0..250, leaving 251 a hole: never
assigned, yet the macro still classifies it as a variable -- one wider than
the comment's own "0 to RPR_VARID_MAX - 1".

RPRVarId is a uint8, kept small on purpose: varId is the likely per-row
match-history key, and since a match can run arbitrarily long the history
grows with it -- so one byte per row, not two, is what keeps that footprint
in check.

The catch of staying in uint8: the four control codes already fill 252..255,
so 251 is the only free slot for any future sentinel (anchor ^/$, exclusion
{- -}) short of widening to uint16.  So the hole is really the last reserve.

Three ways, by what the gap is spent on:

(1) Leave it -- just the doc alignment already underway: 251 stays a
documented
    reserve, macro unchanged.  No follow-up commit.  The one free slot is
then
    on hand for a single future control code, should one ever be needed.

(2) Fill it as a 252nd variable (0..251).  Compatible and doable anytime; a
few
    lines in parse_rpr.c / rpr.h plus the boundary test.  But it spends the
    last free slot, so a future control code would then force either a
    compatibility-breaking narrow of RPR_VARID_MAX or a widen to two bytes
    (doubling history).  Maximal variables now, the control question
deferred.

(3) Reserve 16 control codes now (4 used + 12 spare) at the 0xF0 boundary:
    vars 0..239, control 240..255, existing sentinels unmoved, macro becomes
    (varId & 0xF0) != 0xF0.  Buys 12-code headroom inside the byte, so
history
    stays 1 byte and (2)'s fork never arises.  Same edit shape as (2); costs
    only the nominal drop to 240 variables -- but it is a narrowing, so free
    only pre-release.

The asymmetry: (3) is the only one with a deadline -- a narrowing is
compatible
only before release, while (1)/(2) stay open forever.  So the question is
whether to spend this one free moment to lock in 1-byte control headroom
(3),
or stay minimal now (1)/(2) and take the narrow-or-widen later if it is ever
needed.  My own lean is toward (3): 240 variables is already far more than
any
real pattern will use, so the capacity we give up is nominal, while the
12-code
buffer closes the narrow-or-widen fork for good and keeps match history at
one
byte -- and it is the one choice that is free only now.  That said, I'd like
the decision to rest on everyone's input -- Jian's and the list's as much as
mine -- with you, Tatsuo, weighing it all and making the final call.

Either way, once the feature matures and the final control-code count is
known,
the space can be repacked gap-free -- so none of these is the last word.

Which would you prefer?

Thanks,
Henson

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 02 Jun 2026 11:50:39 +0900 (JST)

Hi Henson, Jian,

> the space can be repacked gap-free -- so none of these is the last word.

I'd prefer (3). Yes, I agree that 240 pattern variables is enough.

Regards,

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 02 Jun 2026 12:03:05 +0900 (JST)

Hi Henson, Jian,

I'd like to suggest making a slight change to v48 patches
organization. Currently README.rpr is included in 0005 executor
patch. Since README.rpr is a documentation, I think it will be more
natural to move it into 0006 docs patch. This also will make it easier
to judge the size of the patches by category: codes, docs and tests if
README.rpr is correctly categorized.

Thoughts?

---

From: Henson Choi <[email-masked]>
Date: Tue, 2 Jun 2026 12:05:41 +0900

> README.rpr is correctly categorized.

+1

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 02 Jun 2026 14:20:09 +0900 (JST)

Hi Henson,

>   [2] my round-1 reply -- AST "absorption" rename deferred to Tatsuo (D3,

While looking into 0002, I noticed some minor ereport calling style
issues.

Recently we start to use the style:

		ereport(ERROR,
				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),

Rather than (old style):

		ereport(ERROR,

Grepping patches shows following results that hire the old style.

$ grep -n '(errcode' *|grep '+'
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1243:+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1248:+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1421:+								(errcode(ERRCODE_SYNTAX_ERROR),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1428:+								(errcode(ERRCODE_SYNTAX_ERROR),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1451:+							(errcode(ERRCODE_SYNTAX_ERROR),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1457:+							(errcode(ERRCODE_SYNTAX_ERROR),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1463:+							(errcode(ERRCODE_SYNTAX_ERROR),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1471:+						(errcode(ERRCODE_SYNTAX_ERROR),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1494:+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1504:+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
nocfbot-0025-Reject-single-row-window-frame-in-row-pattern-rec.txt:55:+						(errcode(ERRCODE_WINDOWING_ERROR),

Regards,

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 02 Jun 2026 14:23:56 +0900 (JST)

Oops.

> nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt:1504:+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),

These have been fixed in 0007. Sorry for noise.

Maybe this is the only one worth to look into?

Regards,

---

From: jian he <[email-masked]>
Date: Tue, 2 Jun 2026 13:46:22 +0800

On Mon, Jun 1, 2026 at 1:35 PM Henson Choi <[email-masked]> wrote:
>
> Hi Tatsuo, Jian,
>
> While tidying RPR comments I found a small inconsistency in the varId bounds.
> The comment/README side I'm already fixing in the in-progress series; whether
> to also change the bounds is a separate follow-up.  As lead author that one is
> (1) Leave it -- just the doc alignment already underway: 251 stays a documented
>     reserve, macro unchanged.  No follow-up commit.  The one free slot is then
> (2) Fill it as a 252nd variable (0..251).  Compatible and doable anytime; a few
>     (doubling history).  Maximal variables now, the control question deferred.
>
>     (varId & 0xF0) != 0xF0.  Buys 12-code headroom inside the byte, so history

3.

240 variables is enough, as each variable supports multiple complex AND/OR
conditions. Additionally, since PostgreSQL regular expressions use 14 special
characters, reserving the remaining ones in advance is a future-proof approach.

----------------------------------------------------
src/backend/executor/README.rpr

XII-4. Memory Pool Management
  Choice: Custom free list

  Rationale:
  - NFA states are created and destroyed in large numbers per row
  - Avoids palloc/pfree overhead
  - State size is variable (counts[] array), but within a single query
    maxDepth is fixed, so all states have the same size

It would be better simply to mention that:
RPRNFAState and RPRNFAContext are allocated in a partition-lifespan
memory context; they will be destroyed in release_partition.
--------------------------
in ExecRPRFreeContext:
{
    if (ctx->states != NULL)
        nfa_state_free_list(winstate, ctx->states);
    if (ctx->matchedState != NULL)
        nfa_state_free(winstate, ctx->matchedState);
}

If ctx->matchedState points to one of the states already in ctx->states, will
nfa_state_free() be called on the same RPRNFAState twice? Is this double-free
permitted, or do we have a mechanism in place to guard against it?
--------------------------
In ExecRPRProcessRow

            if (currentPos == ctxFrameEnd)
            {
                /* Frame boundary reached: force mismatch */
                nfa_match(winstate, ctx, NULL);
                continue;
            }

If I comment out the CONTINUE, the entire regression still succeeds.

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 02 Jun 2026 19:19:12 +0900 (JST)

Hi Henson,

In execRPR.c:

static void
nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
							  int64 currentPos)

This function does not have a forward declaration. Moreover, it is put
in the "API exposed to nodeWindowAgg.c" section. If there's no
particular reason for this, I suggest to add a forward declaration for
it and move it before "API exposed to nodeWindowAgg.c" section.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Tue, 2 Jun 2026 23:11:05 +0900

Hi Tatsuo,

Catching up on today's notes -- all straightforward, so I'm folding them
into one reply.

> I'd prefer (3). Yes, I agree that 240 pattern variables is enough.

Then (3) it is -- and with Jian on the same choice, that closes it. I'll
go ahead with it. Since it's a narrowing it's only free pre-release, so
it goes in the v48 core rather than a later follow-up.

> in the "API exposed to nodeWindowAgg.c" section. [...] I suggest to add
> a forward declaration for it and move it before "API exposed to
> nodeWindowAgg.c" section.

Agreed -- that was just an oversight. I'll add the forward declaration
and move it out of the API-exposed section.

> nocfbot-0025-...:55:+   (errcode(ERRCODE_WINDOWING_ERROR),

Right -- I'll update 0025 in the next posting.

Thanks,
Henson

---

From: Henson Choi <[email-masked]>
Date: Wed, 3 Jun 2026 07:14:21 +0900

Hi Jian,

> 3.
> 240 variables is enough, as each variable supports multiple complex
> AND/OR conditions. [...] reserving the remaining ones in advance is a
> future-proof approach.

Thanks -- that's (3), and Tatsuo landed on the same choice, so it's
settled. I'll make the change for v48.

>   Choice: Custom free list [...]

Agreed -- that's clearer than the rationale list. I'll replace XII-4 with
that wording.

> If ctx->matchedState points to one of the states already in ctx->states,
> will nfa_state_free() be called on the same RPRNFAState twice? Is this
> double-free permitted, or do we have a mechanism in place to guard
> against it?

No -- they're disjoint by construction, so the two frees never touch the
same state. Step by step:

  - nfa_advance() detaches ctx->states up front (sets it to NULL) and
    rebuilds the list from scratch.
  - Each old state is pulled off and advanced; the one that reaches FIN is
    moved into matchedState and never re-added to ctx->states.
  - So at any moment a state is either on ctx->states or it is
    matchedState, never both.
  - ExecRPRFreeContext frees the list and frees matchedState -- disjoint
    sets, no overlap, so no guard is needed.
  - (When a new FIN replaces matchedState, the previous one is freed right
    there.)

> if (currentPos == ctxFrameEnd) {

It isn't dead -- it's the N-FOLLOWING boundary handler. nfa_match(ctx,
NULL) forces a mismatch that finalizes the context on its matchedState,
and the continue skips the rest since it's done for this row.

Without the continue, that finalized context is matched a second time in
the same row -- a context-matched-twice error. It only looks harmless
because the forced mismatch already emptied ctx->states, so the second
nfa_match iterates nothing (and it would also re-run
nfa_reevaluate_dependent_vars(), overwriting the shared nfaVarMatched the
other contexts read). So I'd keep it: a finalized context must not be
matched twice in the same row.

Thanks,
Henson

---

From: Henson Choi <[email-masked]>
Date: Wed, 3 Jun 2026 08:50:28 +0900

Hi all,

While going over the row pattern grammar I want to put on record why the
empty pattern -- PATTERN (()) and the like -- is left unsupported, and why
I think that is the right call for this series rather than an oversight.

Today it is simply a syntax error.  row_pattern_primary is either a
variable (ColId) or a parenthesized group '(' row_pattern ')', and
row_pattern has no empty production, so '()' never parses.  The standard's
pattern syntax does allow an empty row pattern -- it matches the empty
sequence, i.e. it produces an empty match -- so the question is whether we
should grow the grammar, plus an NFA "empty" element, to accept it.

My claim is that we do not need to: every way an empty pattern can appear
reduces to something we already handle, so a dedicated empty element in
the executor would be dead weight.  There are two cases.

1. The empty pattern is the whole pattern: PATTERN (())

This pattern has zero pattern variables.  But DEFINE is mandatory (per
ISO/IEC 19075-5, Table 18), an empty DEFINE list is rejected, and every
DEFINE variable must appear in PATTERN -- otherwise we already error with
"DEFINE variable \"%s\" is not used in PATTERN".  A pattern with no
variables cannot satisfy any of that: there is nothing for DEFINE to
define, yet DEFINE can be neither omitted nor left empty.  So an all-empty
pattern is rejected by the existing rules, with no new check needed.

(It is degenerate in any case.  An empty match at every row maps to "no
reduced frame" -- row_is_in_reduced_frame() returns -1 for RF_EMPTY_MATCH
exactly as it does for RF_UNMATCHED -- so every row would simply be
unmatched.)

2. The empty pattern is embedded: A () B

Here '()' consumes no rows; it is the identity element under
concatenation, so A () B is equivalent to A B.  This stays entirely on the
parser/optimizer side: the empty-pattern production would carry an empty
AST node, and an identity fold in the SEQ simplification (next to the
prefix/suffix and consecutive merges it already does) drops the '()'
before the executor ever sees it.  The runtime is untouched -- which is
the whole point.  And if the fold removes everything, the pattern has
collapsed to case 1 and is rejected there.

The AST rewrites it would add:

  Concatenation (identity element):
    A () B          ->  A B
    () A            ->  A
    A ()            ->  A
    A () () B       ->  A B

  Group unwrap (single non-empty child):
    (A ())          ->  (A)  ->  A
    (() A)          ->  (A)  ->  A

  Quantified empty (empty repeated is still empty):
    ()*  ()+  ()?  (){3}    ->  (removed)
    A ()* B         ->  A B

  Collapses to case 1, then rejected:
    (())    (() ())    ((()))    ->  ()  -> rejected as case 1

The one form that is not pure deletion is an empty alternative.  Here the
empty branch is optionality, not nothing, but it is exactly the '?'
quantifier applied to the rest -- X | () is (X)?:

    X | ()          ->  (X)?          (and  () | X  ->  (X)??, reluctant)
      A | ()        ->  A?            (single var: group is redundant)
      A{2} | ()     ->  (A{2})?       (the group is required here -- A{2}?
                                       would parse as a reluctant A{2}, not
                                       an optional one)
      A B | ()      ->  (A B)?         (X may be a sequence)
      A | B | ()    ->  (A | B)?       (... or an alternation)
    (A | ()) B      ->  A? B

The branch order maps to greediness ('X | ()' greedy, '() | X' reluctant).
Either way it rewrites to a group plus '?', both of which we already
support, so it still needs no new machinery.

The rewrites also compose across nesting.  A nested empty alternation
applies the rule at each level, producing a nested nullable quantifier:

    (A | ()) | ()
        ->  A? | ()      (inner   A | ()  ->  A?)
        ->  (A?)?        (outer   X | ()  ->  (X)?,  X = A?)

and (A?)? = A?, (A+)? = A*, (A*)? = A* are all ordinary nullable
constructs, never a new shape:

    A? | ()   ->  (A?)?    (= A?)
    A+ | ()   ->  (A+)?    (= A*)
    A* | ()   ->  (A*)?    (= A*)

Whether the optimizer collapses these to a single quantifier or leaves the
group as-is doesn't matter here -- both are nullable groups the runtime
already handles, so still no executor-level element.  (The optimizer
leaves these unfolded: a non-exact outer over a nullable child falls
outside tryMultiplyQuantifiers' conservative guard, which skips the whole
class that can have gaps (e.g. (A{2}){2,3} = {4,6}).  That is an
over-rejection, not a correctness defect -- folding the nullable cases
would be a safe optional extension; see the aside below.)

(Duplicate empty branches would be merged first by the alternation dedup,
so A | () | () reduces through A | () to A? just the same.)

Putting it together: across the board an empty pattern is either

  (a) rejected by the existing DEFINE rules (case 1),
  (b) the concatenation identity, removable by the AST simplification
      (case 2), or
  (c) an existing nullable construct ('?', the empty alternative),

and none of these would ever need an executor-level empty element.

Given that, and that the feature has little practical use while the
current series is already sizable, I am leaving the empty pattern out of
scope: it stays a syntax error.  Concretely, accepting it would take a
grammar production, an empty AST node, the concatenation-identity deletion
in the SEQ pass, and (optionally) an extension of the quantifier
composition to fold the nullable cases -- all new work, none of it in this
series.  We can revisit it as that small follow-up if anyone actually
wants it.

Thanks,
Henson

------------------------------------------------------------------------
Aside (separable from the post above): quantifier-composition gaps,
unrelated to the empty pattern
------------------------------------------------------------------------

Independent of the empty pattern, the AST-level quantifier-composition
pass (tryMultiplyQuantifiers, rpr.c) over-rejects: it collapses a nested
quantifier (X{p,q}){m,n} into a single X{...} only when the outer is exact
(m == n), the child is {1,1}, or both are unbounded.  Those are the
always-safe sufficient conditions.  But the full foldable class is larger:
(X{p,q}){m,n} is foldable to X{m*p, n*q} whenever the reachable repetition
counts -- the union over k in [m,n] of [k*p, k*q] -- are contiguous (no
gaps).  That holds for every nullable child (p == 0) and, more generally,
whenever the child span q-p is wide enough to bridge the jump between k and
k+1 iterations.  The guard thus rejects a strict superset of the genuinely
unsafe (gap-producing) patterns: a class of provably-safe folds is left
undone, and those patterns reach the executor as nested groups -- handled
correctly, just not normalized.  This is an optimization over-rejection,
not a correctness defect: the output is identical either way.

The two rules side by side (outer {m,n} over child {p,q}, INF = unbounded):

  current guard -- folds iff:
      m = n                                       (outer exact)
    OR (p = 1 AND q = 1)                          (child {1,1})
    OR (q = INF AND n = INF AND NOT(m = 0 AND p >= 2))   (both unbounded)

  true safe set -- foldable iff the reachable counts are contiguous:
      m = n                                       (single interval)
    OR p = 0                                      (nullable child)
    OR ( p <= max(m,1)*(q - p) + 1                (child span bridges the
         AND (m >= 1 OR p <= 1) )                  k -> k+1 jump; child
{1,1}
                                                   is the p=q=1 instance)

  over-rejected = safe AND NOT current:
    the non-exact-outer (m != n) folds over a gap-free child -- p = 0
    (nullable) or a span-bridging child.  e.g. (A?)?, (A*)?, (A+)?,
    (A?){2,3}, (A+){0,2}, (A{1,3}){2,4} (all in the table below).
    The boundaries (A{2}){2,3} and (A{2,})* are NOT in the safe set
    (p = 2 fails p <= max(m,1)*(q-p)+1), so both rules reject them --
    correctly; widening the guard must keep doing so.

Verified on the branch -- EXPLAIN's "Pattern:" line shows the optimized
form (a nested group means it was not folded; a flat quantifier means it
was):

  folds today (correct):
    (A){2,3}        -> a{2,3}        (child {1,1})
    (A{2,3}){2}     -> a{4,6}        (outer exact)
    (A*)*           -> a*            (both unbounded)

  gap-free but NOT folded (stays nested; "want" is the safe normal form):
    (A?)?           -> (a?)?         want a?
    (A*)?           -> (a*)?         want a*
    (A+)?           -> (a+)?         want a*
    (A?){2,3}       -> (a?){2,3}     want a{0,3}
    (A+){0,2}       -> (a+){0,2}     want a*
    (A{1,3}){2,4}   -> (a{1,3}){2,4} want a{2,12}

  correctly NOT foldable (real gap -- guard is right here):
    (A{2}){2,3}     -> (a{2}){2,3}   reaches {4,6}, not 4..6

The first three "not folded" rows are the nullable forms the empty pattern
would lean on; the next three show the gap is broader (a range outer over
a non-{1,1} child can still be gap-free).  The win is only normalization /
a smaller NFA, so this is low priority.  And this exact pass has a history
of subtle gap bugs (the (A{2,})* over-flatten and the INF-sum merge, both
recently fixed), so widening the guard toward the safe set above should be
its own patch, with boundary tests covering the foldable cases and the two
gaps -- (A{2}){2,3} and (A{2,})* -- that must stay un-folded.

---

From: Tatsuo Ishii <[email-masked]>
Date: Thu, 04 Jun 2026 13:21:08 +0900 (JST)

Hi Henson,

Just a trivial comment to 1.

> pattern is rejected by the existing rules, with no new check needed.

I think current behavior is correct from the standard's point of
view. As you explained, there's no way to accept "PATTERN (())" or
"PATTERN ()" according to the R020 syntax rule.

Note, however, "PATTERN ()" is possible in R010 because it allows to
omit the DEFINE clause itself.

Regards,

---

From: jian he <[email-masked]>
Date: Thu, 4 Jun 2026 17:20:07 +0800

Hi.

Please check the attached regression test refactoring and gram.y changes

------------------atatched patch commit
message-----------------------------------------------
v47 rpr reformat gram.y and improve regression tests

Reformat the first three alternatives of row_pattern_quantifier_opt
in gram.y (empty, '*', '+') from inline single-line actions to
multi-line blocks, making them consistent with the surrounding Op
and '{...}' alternatives.

Update regression tests to follow the convention established in
commit ecb2508aaf [1]: do not repeat exact error messages as comments
in regress test files, since such comments silently go stale when
error wording changes.  Replace the removed "Expected: ERROR: ..."
comments with short descriptive comments that state what the test
is checking rather than what output it expects.

Also remove unnecessary newline/comments in src/test/regress/sql/rpr_base.sql.

Add test coverage for:
1. invalid token combinations after a quantifier (A+ !, A+ ?+,
  A* ?+, A? ??), exercising the split-token error paths in
  row_pattern_quantifier_opt
2. set-returning function (generate_series) in a DEFINE clause

[1]: https://git.postgresql.org/cgit/postgresql.git/commit/?id=ecb2508aaf9b978871734ea2fdf701ab7d593d0a
----------------------------end of commit
message-------------------------------------------------

It's based on latest commit in https://github.com/assam258-5892/postgres.git
It will have a small conflict (around 100 lines difference) with v47.

In src/test/regress/sql/rpr_base.sql, wording such as ``Jacob's
Patterns`` should be removed?

```
--   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
```
I am not sure what this refers to.

In gram.y:
errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),

Will these cause consistency issues?

---

From: Henson Choi <[email-masked]>
Date: Thu, 4 Jun 2026 21:15:40 +0900

> Please check the attached regression test refactoring and gram.y changes

Thanks for the patch -- the substance all looks right: the gram.y reformat
is a fair consistency fix, the ecb2508aaf convention (no hardcoded error
text in regress comments) is one we should follow, and the added coverage
(split-token quantifier errors, SRF in DEFINE) fills real gaps.

I'll work these in together with the patches I'm lining up right before the
v48 merge, rather than mid-stream -- the large "Expected: ERROR" comment
cleanup would otherwise churn against the other test edits still in flight.
They'll all land in that v48 round.

> Patterns`` should be removed?

Those came in from Jacob's branch, but agreed they're better changed:
"Jacob's Patterns" / "from jacob branch" are dev-time provenance notes, not
something to keep in committed tests. I'll give the section a descriptive
header (the tests themselves stay) and do it together with the v48 round
above.

> I am not sure what this refers to.

Those are views (with RPR in their definition) left undropped on purpose,
so the pg_dump/pg_upgrade tests pick them up: dumping and restoring the
regression DB deparses the RPR window clause and re-parses it, exercising
the serialization round trip. rpr_explain.sql keeps one for the same
reason. I'll reword the comment to say that.

> Will these cause consistency issues?

No, it's intentional. INT_MAX is the unbounded sentinel (RPR_QUANTITY_INF),
so an explicit bound is capped at INT_MAX - 1 to avoid colliding with it;
the "0 vs 1" split just matches each form ({n}/{,m} need >= 1, {n,} allows
0).

Regards,
Henson

---

From: Andres Freund <[email-masked]>
Date: Thu, 4 Jun 2026 16:38:36 -0400

Hi,

cfbot is currently failing, with crashes inside JIT, on this patch. However,
the patch cfbot is submitting is rather old, because all of the versions
posted since have been marked nocfbot.

Greetings,

Andres Freund

---

From: Tatsuo Ishii <[email-masked]>
Date: Fri, 05 Jun 2026 07:49:27 +0900 (JST)

> cfbot is currently failing, with crashes inside JIT, on this patch.

Thanks for letting know us. If my memory serves, the old cfbot did not
fail. So maybe the failure is due to something changed recently on the
master branch regarding JIT but the patch has not followed it yet?

Henson,
Do yo have any idea?

> However,

Yes, it's intended. They are all incremental diffes against old (v47)
patches to save the patch size.

Regards,

---

From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 08:24:05 +0900

> Do you have any idea?

Let me be honest about this one.

I think the root issue is that none of us working on the patch has
enough confidence -- enough real control -- over the tuple slot and JIT
side of it. I wrote the code, it ran fine for me, and I assumed that
meant it was fine; but I have never felt that I fully understand and
control that area. So I cannot point to a specific cause with confidence
yet, and I would rather say that plainly than guess.

One practical note: there have been no tuple slot or JIT changes in the
patch since v47, and none are planned. So that part has been stable, and
reviewing the tuple slot / JIT code against v47 is perfectly viable --
there is no need to wait for a newer revision to look at that area.

What I think would genuinely harden the patch is for someone who knows
tuple slots and JIT well to review -- or rewrite -- that part. I would
very much welcome that, and I am happy to provide the design direction
behind the current code, or any other information a reviewer would need.

To be fair, the same caveat applies to the existing planner code the
patch builds on. I feel I have a reasonable grip on the rest of the
patch, but not on those two areas.

I will keep digging on my side (trying to reproduce the JIT crash with
JIT forced on against the current versions), and report back with
anything concrete I find.

Regards,
Henson

---

From: Henson Choi <[email-masked]>
Date: Tue, 9 Jun 2026 13:31:57 +0900

Hi hackers,

This is the v48 fold previewed in my 2026-05-30 post: the incremental
series carried on top of v47 has been folded back, Jian He's round-3
review [1] is applied (my point-by-point reply [2]), and an in-house
static-analysis pass plus the cosmetic clean-up are now included.

  D3. AST "absorption" rename -- Tatsuo's call.  Renamed to "merging"
      (nocfbot-0031).

  0025 ereport style (Tatsuo, 2026-06-02) [3]: the single-row frame
  rejection used the old (errcode(...)) wrapping; updated in place to the
  new errcode/errmsg style.  (The remaining RPR ereports are tidied up
  tree-wide in nocfbot-0045.)

Deferred (unchanged):

  D2. RPRContext consolidation (Jian's round-2 #1) -- folded into the
      future R010 infrastructure, where a single context can back both the
      R020 window implementation and R010.  We agreed to defer it at the v47
      stage (Tatsuo, 2026-05-31); it is not part of v48.

Attached: the v47 feature series (v47-0001..0009) rebased onto current
master, plus the full incremental series (nocfbot-0001..0068) carried
on top of it.

Base:

  89eafad297a  2026-06-06  Fix tuple deforming with virtual generated
columns

Already posted (nocfbot-0001..0026): 0001..0016 went out in earlier
rounds -- 0001..0011 [4], 0012..0015 [5], and 0016 (sent at the time as
0015) [6]; the 2026-05-30 post [7] renumbered them to run contiguously and
added 0017..0026.  All are rebase only and unchanged, except nocfbot-0025,
whose single-row-frame ereports were updated to the new errcode/errmsg
style (per Tatsuo; see above).  Titles for reference:

  nocfbot-0017  Enhance README.rpr per Jian He's review
  nocfbot-0019  Change nfa_add_state_unique signature from bool to void
  nocfbot-0020  Add reluctant bounded mid-band test to rpr_nfa
  nocfbot-0024  Tighten the RPR frame-boundary check from >= to ==

New since the 2026-05-30 post [7] -- nocfbot-0027..0068, in series order.
These apply Jian He's round-3 review [1], an in-house static-analysis
pass, and the v48 cosmetic fold (smallest-first, A..F at the tail).
Patches that change user-visible behavior are tagged [behavior change],
except nocfbot-0042, whose narrow variable-limit change (251 -> 240) is
noted inline.

  nocfbot-0027  Restore findTargetlistEntrySQL99 to static (per Tatsuo)
  nocfbot-0028  Clarify RPR comments
  nocfbot-0029  Rename RPR NFA constructors to make/clone
  nocfbot-0030  Demote RPR nfaVisitedNWords to a local
  nocfbot-0031  Rename the AST-level prefix/suffix rewrite from
                "absorption" to "merging"  (D3)
  nocfbot-0032  Drop non-standard per-group banner labels from RPR
                forward declarations
  nocfbot-0033  Clarify that ExecRPRCleanupDeadContexts always frees the
                failed context
  nocfbot-0034  Correct stale RPR comments and document a defensive
                window check

  nocfbot-0035  Fix unsafe (A{n,})* quantifier flattening
                [behavior change]
      (A{2,})* reaches {0} UNION [2,INF); flattening to A* wrongly admits
      a single A.  Skip multiplication when the outer is skippable and the
      child min >= 2.

  nocfbot-0036  Avoid INF-valued quantifier bound in consecutive-merge
                [behavior change]
      The merge overflow guard used <=, letting a merged min reach the
      RPR_QUANTITY_INF sentinel; tightened to <.

  nocfbot-0037  Fix count slot leak in row pattern recognition absorption
                [behavior change]
      An absorbable leaf VAR exiting inline at max count left its shared
      depth count slot dirty for the sibling group; cleared on exit, with
      the count-clear policy made explicit via Asserts.

  nocfbot-0038  Demote dead runtime checks in the RPR executor to
                assertions

  nocfbot-0039  Fix memory leak in row pattern recognition DEFINE
                evaluation  [behavior change]
      Per-tuple DEFINE evaluation leaked into the wrong context.

  nocfbot-0040  Honor reluctant quantifier for non-leading optional RPR
                variables  [behavior change]
      Reluctant handling existed for leading VARs and groups but not for
      a non-leading optional VAR routed through nfa_route_to_elem; e.g.
      (B A?? C) matched A greedily.  Same skip-first pattern applied.

  nocfbot-0041  Reject column-less compound navigation  [behavior change]
      A compound nav with no column reference is now rejected at parse
      time, instead of being silently accepted with a wrong result.

  nocfbot-0042  Reserve the high varId nibble for RPR control elements
                (the maximum pattern-variable count drops from 251 to 240)

  nocfbot-0043  Generalize quantifier multiplication  [behavior change]
      Replaces the hand-rolled special cases for (child{p,q}){m,n} with a
      general interval-contiguity test, folding cases such as
      (A{2,3}){2,3} -> A{4,9} and (A+){3} -> A{3,} while leaving genuine
      gaps unflattened.

  nocfbot-0044  Tidy up row pattern recognition pattern compilation
  nocfbot-0045  Drop redundant parentheses from row pattern recognition
                ereports
  nocfbot-0046  Tidy up forward declarations and helper placement
  nocfbot-0047  Update the varId documentation
  nocfbot-0048  Reformat the design-decisions chapter in README.rpr
  nocfbot-0049  Point to the absorption-analysis docs from the RPR flag
                definitions

  nocfbot-0050  Fix signed-integer overflow in frame-end clamp
                [behavior change]
      The frame-end clamp computed frameOffset + 1 (int64) before the
      overflow check, so a FOLLOWING offset near PG_INT64_MAX overflowed;
      now added in two checked steps that clamp to PG_INT64_MAX.

  nocfbot-0051  Rework row pattern EXPLAIN deparser to fix grouped
                alternation branches  [EXPLAIN output]
      The bytecode deparser collapsed alternations whose branches were
      quantified groups (e.g. (C | (A B)+ | D)).  Rewritten as recursive
      descent over depth/jump-anchored windows; display-only, execution
      was already correct.

  nocfbot-0052  Handle row pattern navigation nodes in exprTypmod and
                isSimpleNode
  nocfbot-0053  Restore the error cursor for too many row pattern
                variables
  nocfbot-0054  Test deparse of an inline row pattern window
  nocfbot-0055  Fix a mislabeled INITIAL test

  nocfbot-0056  Reject invalid column references in row pattern DEFINE
                clauses  [behavior change]
      Outer-query correlated references and 3-part qualified names in
      DEFINE were reaching an internal "Upper-level Var found where not
      expected" (XX000); both now raise a clean, classified error at
      transformColumnRef.

  nocfbot-0057  Fix shortest match for reluctant nullable quantifiers
                [behavior change]
      An outer reluctant quantifier over a nullable reluctant body, e.g.
      (A??)+?, consumed rows instead of taking the empty match.  The
      count<min branch in nfa_advance_end now mirrors its sibling and
      routes the fast-forward exit first for reluctant elements.

  nocfbot-0058  Compare varno when preserving DEFINE-referenced columns
                [behavior change]
      The allpaths DEFINE-column retention loop compared varattno only;
      it now matches varno/varlevelsup too, dropping over-retention.

  nocfbot-0059  Allow a row pattern quantifier with no space before the
                alternation operator  [behavior change]
      The lexer glues "*|" etc. into one token, so (A*|B) failed.  The
      grammar now recognizes the glued forms via a transient trailing_alt
      flag and splitRPRTrailingAlt() re-splits so "A*|B" == "A* | B".

  nocfbot-0060  Fix outdated function and file references in RPR docs
  nocfbot-0061  Assert that row pattern nesting depth never aliases the
                RPR_DEPTH_NONE sentinel

  nocfbot-0062  Invalidate the row pattern nav slot cache when a window
                partition changes  [behavior change]
      release_partition() now resets nav_slot_pos so a stale cached slot
      position cannot survive a partition boundary.

  nocfbot-0063  Tidy up formatting in row pattern recognition code   (A)
  nocfbot-0064  Modernize idioms in row pattern recognition code     (B)
  nocfbot-0065  Use width-explicit integer limit macros              (C)
                (INT_MAX -> PG_INT32_MAX, the unbounded sentinel)
  nocfbot-0066  Tidy up row pattern recognition regression test comments (D)
  nocfbot-0067  Add row pattern recognition negative and coverage tests (E)
  nocfbot-0068  Use foreach_node and friends in RPR code             (F)
                (foreach + lfirst() -> foreach_node;
                 foreach_current_index, dropping redundant breaks)

Issue-to-patch map (each raised issue -> decision -> landing), by source.
(Earlier incremental cleanup 0001..0016: see [4][5][6].)

Jian He, round 1 (2026-05-26):
  Short-circuit optimization        invited to drive   -> separate series
  Absorption README narrative       accept             -> nocfbot-0017
  DFS expansion                     accept             -> nocfbot-0017
  initialAdvance README mismatch    accept             -> nocfbot-0017
  "absorption" -> "merging" rename  accept (Tatsuo)    -> nocfbot-0031
  Defensive Assert in advance_var   accept             -> nocfbot-0018
  Finalize unnecessary?             keep               -> nocfbot-0018
  Greedy comment label              accept             -> nocfbot-0018
  state->next reset                 decline            -> nocfbot-0018
  visited marking purpose           accept             -> nocfbot-0018
  compareDepth comment              accept             -> nocfbot-0018
  ALT depth invariant Assert        decline            -> nocfbot-0018
  Unused bool return                accept             -> nocfbot-0019
  count >= 3 test coverage          accept             -> nocfbot-0020

Jian He, round 2 (2026-05-28):
  RPRContext consolidation          defer (Tatsuo)     -> D2
  README terminology + typo         accept             -> nocfbot-0021
  get_reduced_frame_status order    keep               -> nocfbot-0022
  states == NULL branch coverage    already reached    -> nocfbot-0023
  currentPos >= -> ==               accept             -> nocfbot-0024
  single-row frame (6.10.2, D1)     reject (Tatsuo)    -> nocfbot-0025
  ExecRPRProcessRow refactor        Datum fix only     -> nocfbot-0026

Jian He, round 3 (2026-05-30) [1] (reply [2]):
  "Tests line N" comments           rephrase           -> nocfbot-0028
  "at the END" wording              accept             -> nocfbot-0028
  uppercase END cleanup             accept             -> nocfbot-0028
  nfa_advance_var count comment     reword             -> nocfbot-0028
  make / clone rename               accept             -> nocfbot-0029
  nfaVisitedNWords field            accept (-> local)  -> nocfbot-0030
  nfaStateSize field                keep               -> (no change)
  drop the elem parameter           keep               -> (no change)

Jian He, off-list (2026-05-15..06-04):
  Per-group banner labels           drop               -> nocfbot-0032
  Dead-context cleanup comment      clarify            -> nocfbot-0033
  rpr.c pg_unreachable refactor     accept             -> nocfbot-0044
  ABSORBABLE doc pointer            accept             -> nocfbot-0049
  has_column_ref necessity          defend             -> (no change)
  CFI dedup                         decline            -> (no change)
  fractional FOLLOWING tests        decline            -> (no change)
  initial/SEEK rename               defer              -> separate series

Tatsuo Ishii + Jian round 4 (2026-05-31..06-02):
  Tatsuo README review              accept             -> nocfbot-0014
  findTargetlistEntrySQL99 static   accept             -> nocfbot-0027
  varId high nibble ("3")           accept             -> nocfbot-0042
  ereport redundant parens          accept             -> nocfbot-0045
  forward-decl / helper placement   accept             -> nocfbot-0046
  varId documentation sync          accept             -> nocfbot-0047
  XII chapter prose reformat        reformat           -> nocfbot-0048
  README 0005 -> 0006 reorder       at submit (Ishii)  -> committer
  double-free question              cannot occur       -> (no change)
  redundant continue                keep               -> (no change)
  non-ASCII section sign            already ASCII      -> (no change)
  empty pattern PATTERN ()          R020 reject right  -> (no change)

in-house static analysis:
  stale comments + window check     fix                -> nocfbot-0034
  unsafe (A{n,})* flattening        fix                -> nocfbot-0035
  INF-valued merge bound            fix                -> nocfbot-0036
  count slot leak                   fix                -> nocfbot-0037
  dead runtime checks -> asserts    fix                -> nocfbot-0038
  DEFINE per-tuple memory leak      fix                -> nocfbot-0039
  reluctant non-leading optional    fix                -> nocfbot-0040
  column-less compound navigation   fix                -> nocfbot-0041
  incomplete quantifier fold (EP-1) generalize         -> nocfbot-0043
  frame-end signed overflow         fix                -> nocfbot-0050
  EXPLAIN deparser grouped-alt      fix                -> nocfbot-0051
  exprTypmod / isSimpleNode nav     fix                -> nocfbot-0052
  errpos for too-many-vars          fix                -> nocfbot-0053
  inline-OVER deparse coverage      fix                -> nocfbot-0054
  INITIAL test mislabel             fix                -> nocfbot-0055
  DEFINE outer / 3-part column ref  fix                -> nocfbot-0056
  reluctant-nullable shortest match fix                -> nocfbot-0057
  allpaths varno comparison         fix                -> nocfbot-0058
  glued quantifier + alternation    fix                -> nocfbot-0059
  nfa_advance / README references   doc                -> nocfbot-0060
  invariant asserts / notes         assert             -> nocfbot-0061
  nav_slot / LAST(x,0) edge cases   defense            -> nocfbot-0062

v48 final fold (off-list sweep + Jian round 5 v47 patch):
  pure formatting                   fold               -> nocfbot-0063
  idioms (palloc_array, Max, ...)   fold               -> nocfbot-0064
  INT_MAX -> PG_INT32_MAX sentinel  fold               -> nocfbot-0065
  regress comment + doc hygiene     fold               -> nocfbot-0066
  new negative / coverage tests     fold               -> nocfbot-0067
  foreach_node + lfirst sweep       fold               -> nocfbot-0068

Please let me know if any of the slicing or the grouping looks off, or
if you would prefer the correctness fixes split out ahead of the
cosmetic fold.

Best regards,
Henson

[1] Jian's review, round 3 (2026-05-30):

[2] My point-by-point reply to the round-3 review (2026-05-30):

https://postgr.es/m/[email-masked]

[3] Tatsuo's note on the 0025 ereport wrapping (2026-06-02):

https://postgr.es/m/[email-masked]

[4] Earlier incremental post -- nocfbot-0001..0011 (2026-05-09):

https://postgr.es/m/CAAAe_zCL9UtiYthrSaXCmhFMK6Q3YQ6BQGgae7C9en2k=[email-masked]

[5] Earlier incremental post -- nocfbot-0012..0015 (2026-05-12):

https://postgr.es/m/[email-masked]

[6] nocfbot-0016, sent at the time as 0015 (2026-05-26):

https://postgr.es/m/CAAAe_zD7vCLCb+vxpO3P-NsDUZ=[email-masked]

[7] The 2026-05-30 post -- renumbered 0001..0016 and added 0017..0026:

https://postgr.es/m/[email-masked]
From c215fec2b4a9575973314b8b046552cb5870645e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 11:36:53 +0900
Subject: [PATCH 04/68] Reclassify DEFINE qualifier check and reword diagnostic
 to "expression"

 
-	/*
 
 	/*----------

 
 
 -- Semantic errors
 
-- 
2.50.1 (Apple Git-155)

From 251fbbdeb1fa438337cb4399205246c93dc7aa45 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 21:37:59 +0900
Subject: [PATCH 02/68] Unify RPR DEFINE walkers and reject volatile callees

 
 
 /*

 
 
 /*

 

 
 
-typedef struct
-{
 
 /*
 
 	ctx.winstate = winstate;

 

 /*
 
 /*
 
 /*
- * nav_offset_walker
 
-	if (node == NULL)
-		return false;
+	/*

 
-		/*

 
-		/*

-		/*

 
 /*
- * compute_nav_offsets
 
 	ctx.maxOffset = 0;

 
 
-/*

 	/*

 

 #include "miscadmin.h"

 	return result;
 }
+
+/*
+ * nav_traversal_walker
 
 #include "postgres.h"

 
 /*
  * transformRPR

 
 /*
- * check_rpr_nav_expr
 
+/*
+ * define_walker
 
 	if (node == NULL)
 		return false;
 
-static void

 

-/*
 
+/*

 
 RESET plan_cache_mode;

 

 

From d9fc4c58303b6d474168f3168451c2c2e973a848 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 11:44:45 +0900
Subject: [PATCH 05/68] Sync stale comments on DEFINE/PATTERN handling

 
 
    <para>
 
 	/*
 
 	/*

From 31b21e064270f94a8ec27305ea47740ad5b62382 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 14:29:14 +0900
Subject: [PATCH 03/68] Cover RPR empty-match path with EXPLAIN tests; fix
 stale XXX comments

 

 

 

 

From 75ca808a06e22c08442b2fde2bf5ae18987c1ce9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 19:09:34 +0900
Subject: [PATCH 01/68] Add DEFINE non-volatile baseline to rpr_integration B9

 

From 0532539a2509adba38989fc3bf5b1a1281f251ab Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 17:27:51 +0900
Subject: [PATCH 07/68] Remove optional outer parentheses from ereport() calls
 in RPR files

 

 /*

 		ereport(ERROR,

 		ereport(ERROR,

 
 			if (!strcmp(n, name))
 				ereport(ERROR,

 

 
 			/*
 
 			*ctx = saved;
-- 
2.50.1 (Apple Git-155)

From 921232e90b5b9685b87d2b70504077929428563e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 5 May 2026 17:21:56 +0900
Subject: [PATCH 06/68] Add trailing commas to RPR enum definitions

 
 /*
 
 /*

From 029c2bf753599736f0469a308d8fe050e4e2b91f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 4 May 2026 22:31:06 +0900
Subject: [PATCH 08/68] Add high-water mark tracking to NFA visited bitmap
 reset

 
+/*

 
 	switch (elem->varId)
 	{

 		state = states;
 		states = states->next;

-- 
2.50.1 (Apple Git-155)

From 2aaed5f6126ac08b1c11593568bdcbc5f6f58d91 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:00:48 +0900
Subject: [PATCH 09/68] Document DEFINE subquery rejection as intentional
 over-rejection

From a6df6a0af2b523eeff49909cb9e4215b3a435162 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 7 May 2026 21:37:51 +0900
Subject: [PATCH 10/68] Remove duplicate #include in nodeWindowAgg.c

From 5ac8a338e63200e8ab85317cf9cd095a03596fba Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 9 May 2026 13:47:49 +0900
Subject: [PATCH 11/68] Normalize SQL/RPR standard references

 

 X-3. INITIAL vs SEEK

 			/*
 
 			/*----------

 

 

 

From e79f86bb2dddc28457ae0aeb658581457122cf42 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 10 May 2026 13:51:45 +0900
Subject: [PATCH 12/68] Add rpr_integration B7 cases for RPR in recursive query

 

From 0de2f3fbe345717b9f91aad79e9ace2398e75c3c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 15:38:03 +0900
Subject: [PATCH 14/68] Enhance README.rpr per Tatsuo Ishii's review

 

   Related code:

 

-III-2. PATTERN AST

 

 
 Memory management:

 
 
+
+References:
+

From e989f94078b49439052bcaeda38afbd74a2fc057 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 12 May 2026 15:43:49 +0900
Subject: [PATCH 15/68] Round out README.rpr WindowAggState field coverage

 

From 476a8f0a9aa2a5fca573ff608d16d3a4d35b360e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 10 May 2026 14:42:09 +0900
Subject: [PATCH 13/68] Reject row pattern recognition in recursive queries

 
 
 /*
  * transformWithClause -
 
+		/*
 
 /*

From ddf9bbf4d9a03b0377ef06556cf0dbba8f1427bb Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 07:02:38 +0900
Subject: [PATCH 16/68] Add raw_expression_tree_walker coverage for RPR raw
 nodes

From e0de323a26a95fb235c053c1a1bbb4d8b809e3f7 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 13:34:23 +0900
Subject: [PATCH 17/68] Enhance README.rpr per Jian He's review

 
 
   VAR with quantifier:

 

 

From fe90e09297dbb9e16123ffbdd200ca7c4b6b95d4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 14:02:19 +0900
Subject: [PATCH 19/68] Change nfa_add_state_unique signature from bool to void

 
 /*
-- 
2.50.1 (Apple Git-155)

From a0a9d1581e728a564bbd481f2854a4a28209a874 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 14:15:09 +0900
Subject: [PATCH 20/68] Add reluctant bounded mid-band test to rpr_nfa

 

From 1a5893d5d23d3787065192fb63bdeec1719169ba Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 27 May 2026 13:44:55 +0900
Subject: [PATCH 18/68] Clarify execRPR.c comments and tighten an Assert per
 Jian He's review

 

 

 
 
 		state = states;
 		states = states->next;
+
+		/*

From d74ee1d2a2ad85dcb3198f13352d4dcf231e3c35 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 17:45:13 +0900
Subject: [PATCH 21/68] Define RPR absorption terminology in README.rpr per
 Jian He's review

 

 

 

 
 VIII-4. Dual-Flag Design

From 75355c0e115a1874f8833520ae89bcc165867263 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 17:51:37 +0900
Subject: [PATCH 22/68] Document the get_reduced_frame_status cascade invariant
 per Jian He's review

 

+	/*

-- 
2.50.1 (Apple Git-155)

From 6bf5f93a5c5ce41e15e920c09d702713dd15f7f7 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 17:56:02 +0900
Subject: [PATCH 23/68] Explain the completed-head-context branch in
 update_reduced_frame per Jian He's review

 
-- 
2.50.1 (Apple Git-155)

From a0dc841399f9eb79ae4d77374382dc21f4183f31 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 29 May 2026 19:07:40 +0900
Subject: [PATCH 24/68] Tighten the RPR frame-boundary check from >= to == per
 Jian He's review

 

From 36da77822da7f6ead31f0a434a245096cac2aa94 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 30 May 2026 18:57:56 +0900
Subject: [PATCH 25/68] Reject single-row window frame in row pattern
 recognition

 
 
index f16d01e9743..0dab18254e0 100644
+						errmsg("frame ending offset must be positive with row pattern recognition"));
 
+	/*

 

From 4df572a905bbb1ce97e425db45e9d9152d53deac Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 30 May 2026 19:37:11 +0900
Subject: [PATCH 26/68] Remove the redundant zero check on the RPR frame ending
 offset

Per Jian He's review.
---

index 0dab18254e0..bcb3614e96e 100644
 
 	/*
-- 
2.50.1 (Apple Git-155)

From d070b97113883c1f6efbe2aaaf05dd5640cf3b1a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 10:14:33 +0900
Subject: [PATCH 27/68] Restore findTargetlistEntrySQL99 to static per Tatsuo
 Ishii's review

The RPR DEFINE handling once called findTargetlistEntrySQL99 from
parse_rpr.c, which required exposing it as extern.  The Var-only targetlist
fix removed that cross-file call, so revert the function to its original
static linkage and drop the prototype from parse_clause.h.
---
 src/backend/parser/parse_clause.c | 4 +++-
 src/include/parser/parse_clause.h | 3 ---
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 3201a22d278..550ea4eb9c0 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -89,6 +89,8 @@ static void checkExprIsVarFree(ParseState *pstate, Node *n,
 							   const char *constructName);
 static TargetEntry *findTargetlistEntrySQL92(ParseState *pstate, Node *node,
 											 List **tlist, ParseExprKind exprKind);
+static TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
+											 List **tlist, ParseExprKind exprKind);
 static int	get_matching_location(int sortgroupref,
 								  List *sortgrouprefs, List *exprs);
 static List *resolve_unique_index_expr(ParseState *pstate, InferClause *infer,
@@ -2310,7 +2312,7 @@ findTargetlistEntrySQL92(ParseState *pstate, Node *node, List **tlist,
  * tlist	the target list (passed by reference so we can append to it)
  * exprKind identifies clause type being processed
  */
-TargetEntry *
+static TargetEntry *
 findTargetlistEntrySQL99(ParseState *pstate, Node *node, List **tlist,
 						 ParseExprKind exprKind)
 {
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index 8aaac881f2b..fe234611007 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -52,9 +52,6 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle,
 extern Index assignSortGroupRef(TargetEntry *tle, List *tlist);
 extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList);
 
-extern TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
-											 List **tlist, ParseExprKind exprKind);
-
 /* functions in parse_jsontable.c */
 extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt);
 
-- 
2.50.1 (Apple Git-155)

From 8a00c552f5f3ecd6c889b53b8062304d43b06252 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 11:16:49 +0900
Subject: [PATCH 29/68] Rename RPR NFA constructors to make/clone per Jian He's
 round-3 review

Rename nfa_context_alloc and nfa_state_alloc to _make (a blank
allocation), and nfa_state_create to nfa_state_clone (build a state from
an existing one's counts).  This follows the makeNode precedent and the
regex engine's clonesuccessorstates/cloneouts.  Also reword the
nfa_state_clone header comment from "Create" to "Clone".
---
 src/backend/executor/README.rpr       |  2 +-
 src/backend/executor/execRPR.c        | 58 +++++++++++++--------------
 src/test/regress/expected/rpr_nfa.out |  2 +-
 src/test/regress/sql/rpr_nfa.sql      |  2 +-
 4 files changed, 32 insertions(+), 32 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 467cc03ecff..9396a569fbd 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -579,7 +579,7 @@ VI-2. Context Creation: ExecRPRStartContext()
 
 Creates a new context and performs the initial advance.
 
-  (1) Allocate context via nfa_context_alloc()
+  (1) Allocate context via nfa_context_make()
   (2) Set matchStartRow = pos
   (3) Create initial state: elemIdx=0 (first pattern element),
       counts=all zero
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 9bad36239f8..4022ca66e84 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -54,11 +54,11 @@ nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
 }
 
 /* Forward declarations - NFA state management */
-static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
+static RPRNFAState *nfa_state_make(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
 static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
-static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
-									 int32 *counts, bool sourceAbsorbable);
+static RPRNFAState *nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
+									int32 *counts, bool sourceAbsorbable);
 static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
 							 RPRNFAState *s2);
 static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
@@ -67,7 +67,7 @@ static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
 
 /* Forward declarations - NFA context management (internal) */
-static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate);
+static RPRNFAContext *nfa_context_make(WindowAggState *winstate);
 static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
 
 /* Forward declarations - NFA statistics */
@@ -199,14 +199,14 @@ static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
  */
 
 /*
- * nfa_state_alloc
+ * nfa_state_make
  *
  * Allocate an NFA state, reusing from freeList if available.
  * freeList is stored in WindowAggState for reuse across match attempts.
  * Uses flexible array member for counts[].
  */
 static RPRNFAState *
-nfa_state_alloc(WindowAggState *winstate)
+nfa_state_make(WindowAggState *winstate)
 {
 	RPRNFAState *state;
 
@@ -265,21 +265,21 @@ nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list)
 }
 
 /*
- * nfa_state_create
+ * nfa_state_clone
  *
- * Create a new state with given elemIdx and counts.
+ * Clone a state from the given elemIdx and counts.
  * isAbsorbable is computed immediately: inherited AND new element's flag.
  * Monotonic property: once false, stays false through all transitions.
  *
  * Caller is responsible for linking the returned state.
  */
 static RPRNFAState *
-nfa_state_create(WindowAggState *winstate, int16 elemIdx,
-				 int32 *counts, bool sourceAbsorbable)
+nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
+				int32 *counts, bool sourceAbsorbable)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	int			maxDepth = pattern->maxDepth;
-	RPRNFAState *state = nfa_state_alloc(winstate);
+	RPRNFAState *state = nfa_state_make(winstate);
 	RPRPatternElement *elem = &pattern->elements[elemIdx];
 
 	state->elemIdx = elemIdx;
@@ -418,12 +418,12 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 }
 
 /*
- * nfa_context_alloc
+ * nfa_context_make
  *
  * Allocate an NFA context, reusing from free list if available.
  */
 static RPRNFAContext *
-nfa_context_alloc(WindowAggState *winstate)
+nfa_context_make(WindowAggState *winstate)
 {
 	RPRNFAContext *ctx;
 
@@ -931,8 +931,8 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 
 		/* Create skip state before add_unique, which may free state */
 		if (RPRElemCanSkip(nextElem))
-			skipState = nfa_state_create(winstate, nextElem->next,
-										 state->counts, state->isAbsorbable);
+			skipState = nfa_state_clone(winstate, nextElem->next,
+										state->counts, state->isAbsorbable);

@@ -978,8 +978,8 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 			break;
 
 		/* Create independent state for each branch */
-		newState = nfa_state_create(winstate, altIdx,
-									state->counts, state->isAbsorbable);
+		newState = nfa_state_clone(winstate, altIdx,
+								   state->counts, state->isAbsorbable);
 
 		/* Recursively process this branch before next */
 		nfa_advance_state(winstate, ctx, newState, currentPos);
@@ -1011,8 +1011,8 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Optional group: create skip path (but don't route yet) */
 	if (elem->min == 0)
 	{
-		skipState = nfa_state_create(winstate, elem->jump,
-									 state->counts, state->isAbsorbable);
+		skipState = nfa_state_clone(winstate, elem->jump,
+									state->counts, state->isAbsorbable);
 	}
 
 	if (skipState != NULL && RPRElemIsReluctant(elem))
@@ -1094,8 +1094,8 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 *----------
 		 */
 		if (RPRElemCanEmptyLoop(elem))
-			ffState = nfa_state_create(winstate, state->elemIdx,
-									   state->counts, state->isAbsorbable);
+			ffState = nfa_state_clone(winstate, state->elemIdx,
+									  state->counts, state->isAbsorbable);
 
 		/* Primary path: loop back for real matches */
 		for (int d = depth + 1; d < pattern->maxDepth; d++)
@@ -1163,8 +1163,8 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 * Create exit state first (need original counts before modifying
 		 * state)
 		 */
-		exitState = nfa_state_create(winstate, elem->next,
-									 state->counts, state->isAbsorbable);
+		exitState = nfa_state_clone(winstate, elem->next,
+									state->counts, state->isAbsorbable);
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
@@ -1255,8 +1255,8 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			RPRNFAState *savedMatch = ctx->matchedState;
 
 			/* Clone for exit, original stays for loop */
-			cloneState = nfa_state_create(winstate, elem->next,
-										  state->counts, state->isAbsorbable);
+			cloneState = nfa_state_clone(winstate, elem->next,
+										 state->counts, state->isAbsorbable);
 			cloneState->counts[depth] = 0;
 			nextElem = &elements[cloneState->elemIdx];
 
@@ -1287,8 +1287,8 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		else
 		{
 			/* Clone for loop, original used for exit */
-			cloneState = nfa_state_create(winstate, state->elemIdx,
-										  state->counts, state->isAbsorbable);
+			cloneState = nfa_state_clone(winstate, state->elemIdx,
+										 state->counts, state->isAbsorbable);
 
 			/* Loop first (preferred for greedy) */
 			nfa_add_state_unique(winstate, ctx, cloneState);
@@ -1505,9 +1505,9 @@ ExecRPRStartContext(WindowAggState *winstate, int64 startPos)
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elem;
 
-	ctx = nfa_context_alloc(winstate);
+	ctx = nfa_context_make(winstate);
 	ctx->matchStartRow = startPos;
-	ctx->states = nfa_state_alloc(winstate);	/* initial state at elem 0 */
+	ctx->states = nfa_state_make(winstate); /* initial state at elem 0 */
 
 	elem = &pattern->elements[0];
 
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 1f494d2db34..72dbf080a37 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -3844,7 +3844,7 @@ WINDOW w AS (
 
 -- Non-absorbable context during absorption
 -- Pattern (A B)+ C: A,B in absorbable group, C is not.
--- When END exits to C via nfa_state_create, isAbsorbable becomes false.
+-- When END exits to C via nfa_state_clone, isAbsorbable becomes false.
 WITH test_non_absorbable AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 76dfc4d88bc..128476aa1d1 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2790,7 +2790,7 @@ WINDOW w AS (
 
 -- Non-absorbable context during absorption
 -- Pattern (A B)+ C: A,B in absorbable group, C is not.
--- When END exits to C via nfa_state_create, isAbsorbable becomes false.
+-- When END exits to C via nfa_state_clone, isAbsorbable becomes false.
 WITH test_non_absorbable AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
-- 
2.50.1 (Apple Git-155)

From 686c593a23d5604e48a5f08a84cf525cbda8a072 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 11:03:57 +0900
Subject: [PATCH 28/68] Clarify RPR comments per Jian He's round-3 review

- Replace fragile "Tests line NNN" comments with the branch and condition
  they exercise, so they survive later code shuffles
- Reword nfa_add_state_unique to describe appending to the list tail
- Disambiguate "END chain" as the chain of END elements
- Restate the nfa_advance_var Assert comment as a structural invariant
  (min <= max), not a post-match claim
---
 src/backend/executor/execRPR.c         | 11 ++++++-----
 src/test/regress/expected/rpr_base.out |  8 ++++----
 src/test/regress/sql/rpr_base.sql      |  8 ++++----
 3 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 4463cfe0a5c..9bad36239f8 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -334,7 +334,8 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 /*
  * nfa_add_state_unique
  *
- * Add a state to ctx->states at the END, only if no duplicate exists.
+ * Add the state to the end of the ctx->states linked list, but only if a
+ * duplicate state is not already present.
  * Earlier states have better lexical order (DFS traversal order), so existing
  * wins; the new state is freed when a duplicate is found.
  */
@@ -779,7 +780,7 @@ 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 END chain to reach absorption judgment point
+ *   - Advance through the END-element chain to the absorption judgment point
  *   - Only deterministic exits (count >= max, max != INF) are handled
  *   - Chains through END elements while count >= max (must-exit path)
  *
@@ -796,8 +797,8 @@ 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 END chain inline so absorb phase
-	 * can compare states at judgment points.
+	 * count with END next, advance through the chain of END elements inline
+	 * so absorb phase can compare states at judgment points.
 	 */
 	for (state = ctx->states; state != NULL; state = nextState)
 	{
@@ -1229,7 +1230,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 	bool		canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max);
 	bool		canExit = (count >= elem->min);
 
-	/* After a successful match, count >= 1, so at least one must be true */
+	/* min <= max, so !canExit (count < min) implies canLoop (count < max) */
 	Assert(canLoop || canExit);
 
 	/* elem->next must be a valid index for any reachable VAR */
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index d8f805c89aa..c96a1216cc3 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3442,8 +3442,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 (7 rows)
 
 -- Consecutive VAR merge: A A+ -> a{2,}
--- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
--- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars,
+-- where a finite prev (A{1,1}) meets an infinite child (A+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -3492,8 +3492,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 (7 rows)
 
 -- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
--- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
--- prev: (A B){2,2} (finite), child: (A B)+ (infinite) triggers line 325 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups,
+-- where a finite prev ((A B){2,2}) meets an infinite child ((A B)+).
 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 6c2365a2d20..d7b63cfc690 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2379,8 +2379,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN (A+ A*) DEFINE A AS val > 0);
 
 -- Consecutive VAR merge: A A+ -> a{2,}
--- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
--- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars,
+-- where a finite prev (A{1,1}) meets an infinite child (A+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -2399,8 +2399,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A B)+ (A B)+) DEFINE A AS val <= 50, B AS val > 50);
 
 -- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
--- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
--- prev: (A B){2,2} (finite), child: (A B)+ (infinite) triggers line 325 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups,
+-- where a finite prev ((A B){2,2}) meets an infinite child ((A B)+).
 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 66a754b84526730b31edf03462163ae42aec5f9e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 11:38:09 +0900
Subject: [PATCH 30/68] Demote RPR nfaVisitedNWords to a local per Jian He's
 round-3 review

nfaVisitedNWords is read only once, at init, to size the visited bitmap
(the per-row reset clears just the high-water range), so it need not live
in WindowAggState.  nfaStateSize is kept: it is recomputed per state
allocation, the engine's hottest path.
---
 src/backend/executor/README.rpr      | 2 --
 src/backend/executor/execRPR.c       | 2 +-
 src/backend/executor/nodeWindowAgg.c | 6 ++++--
 src/include/nodes/execnodes.h        | 2 --
 4 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 9396a569fbd..26c1a1ea236 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -522,7 +522,6 @@ V-3. RPR Fields of WindowAggState
   nfaStateFree                  Reuse pool for states
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
-  nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
   nfaVisitedMinWord             Lowest bitmapword index touched since last reset
   nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
@@ -1542,7 +1541,6 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineMatchStartDependent: Bitmapset* (match_start_dependent
     |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
-    |--- nfaVisitedNWords: int (size of nfaVisitedElems)
     |--- nfaVisitedMinWord / nfaVisitedMaxWord: int16
     |        (touched-word range for fast reset)
     |--- nfaLastProcessedRow: int64 (-1 = none)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 4022ca66e84..dca1e45be57 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -41,7 +41,7 @@
 /*
  * Set the visited bit for elemIdx and update the high-water marks
  * (nfaVisitedMin/MaxWord) so that the next reset only has to clear
- * the touched range instead of the full nfaVisitedNWords array.
+ * the touched range instead of the full nfaVisitedElems bitmap.
  */
 static inline void
 nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index bcb3614e96e..0b0196e7e40 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3058,12 +3058,14 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	/* Calculate NFA state size and allocate cycle detection bitmap */
 	if (node->rpPattern != NULL)
 	{
+		int			nfaVisitedNWords;
+
 		winstate->nfaStateSize = offsetof(RPRNFAState, counts) +
 			sizeof(int32) * node->rpPattern->maxDepth;
-		winstate->nfaVisitedNWords =
+		nfaVisitedNWords =
 			(node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1;
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
-											winstate->nfaVisitedNWords);
+											nfaVisitedNWords);
 		/* High-water mark sentinels: no bits set yet. */
 		winstate->nfaVisitedMinWord = INT16_MAX;
 		winstate->nfaVisitedMaxWord = -1;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 1fba14b892e..4641ed36cee 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2671,8 +2671,6 @@ typedef struct WindowAggState
 											 * (match_start-dependent) */
 	bitmapword *nfaVisitedElems;	/* elemIdx visited bitmap for cycle
 									 * detection */
-	int			nfaVisitedNWords;	/* number of bitmapwords in
-									 * nfaVisitedElems */
 	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
 									 * last reset (INT16_MAX = none) */
 	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
-- 
2.50.1 (Apple Git-155)

From 4817be41a812df933f0a461609c6d38f35179aa4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 12:08:50 +0900
Subject: [PATCH 31/68] Rename the AST-level prefix/suffix rewrite from
 "absorption" to "merging"

"absorption" now refers only to the runtime context-equivalence collapse.
The Phase-1 AST rewrite in mergeGroupPrefixSuffix is renamed "prefix/suffix
merging" to match the sibling "consecutive variable / group / ALT merging"
rewrites.  Per the naming discussion with Tatsuo Ishii.
---
 src/backend/executor/README.rpr  |  2 +-
 src/backend/optimizer/plan/rpr.c | 17 ++++++++---------
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 26c1a1ea236..449bf051153 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -221,7 +221,7 @@ applied:
   (d) Consecutive ALT merging: Merge repeated identical ALT nodes
       (A | B) (A | B) (A | B) -> (A | B){3}
 
-  (e) Prefix/suffix absorption: Absorb identical sequences before/after
+  (e) Prefix/suffix merging: Merge identical sequences before/after
       a group
       A B (A B)+ -> (A B){2,INF}
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index c65681463b3..2a1d665c7ee 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -480,7 +480,7 @@ mergeConsecutiveAlts(List *children)
  *		Merge sequence prefix/suffix into GROUP with matching children.
  *
  * When a GROUP's children appear as a prefix before and/or suffix after
- * the GROUP in a SEQ, absorb them by incrementing the GROUP's quantifier.
+ * the GROUP in a SEQ, merge them by incrementing the GROUP's quantifier.
  * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
  *
  * Algorithm:
@@ -503,15 +503,15 @@ mergeGroupPrefixSuffix(List *children)
 	List	   *result = NIL;
 	int			numChildren = list_length(children);
 	int			i;
-	int			skipUntil = -1; /* skip suffix elements already absorbed */
+	int			skipUntil = -1; /* skip suffix elements already merged */
 
 	for (i = 0; i < numChildren; i++)
 	{
 		RPRPatternNode *child = (RPRPatternNode *) list_nth(children, i);
 
 		/*
-		 * The suffix absorption logic below adjusts i to skip absorbed
-		 * elements, ensuring we never revisit them. Verify this invariant.
+		 * The suffix merge logic below adjusts i to skip merged elements,
+		 * ensuring we never revisit them. Verify this invariant.
 		 */
 		Assert(i >= skipUntil);
 
@@ -543,7 +543,7 @@ mergeGroupPrefixSuffix(List *children)
 			groupChildCount = list_length(groupContent);
 
 			/*
-			 * PREFIX MERGE: Check if preceding elements match. Keep absorbing
+			 * PREFIX MERGE: Check if preceding elements match. Keep merging
 			 * as long as we have matching prefixes.
 			 */
 			while (prefixLen >= groupChildCount && groupChildCount > 0)
@@ -592,7 +592,7 @@ mergeGroupPrefixSuffix(List *children)
 			}
 
 			/*
-			 * SUFFIX MERGE: Check if following elements match. Keep absorbing
+			 * SUFFIX MERGE: Check if following elements match. Keep merging
 			 * as long as we have matching suffixes.
 			 */
 			while (i + groupChildCount < numChildren && groupChildCount > 0)
@@ -623,7 +623,7 @@ mergeGroupPrefixSuffix(List *children)
 					 child->max < RPR_QUANTITY_INF - 1))
 				{
 					/*
-					 * Match! Absorb suffix by incrementing quantifier and
+					 * Match! Merge suffix by incrementing quantifier and
 					 * skipping.
 					 */
 					child->min += 1;
@@ -632,8 +632,7 @@ mergeGroupPrefixSuffix(List *children)
 					skipUntil = suffixStart + groupChildCount;
 
 					/*
-					 * Update i to continue suffix check after absorbed
-					 * elements
+					 * Update i to continue suffix check after merged elements
 					 */
 					i = skipUntil - 1;
 				}
-- 
2.50.1 (Apple Git-155)

From b5a8065aa1d4e5f4679508c8dbafd6465ef2d6ac Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 12:49:16 +0900
Subject: [PATCH 32/68] Drop non-standard per-group banner labels from RPR
 forward declarations

The per-group "Forward declarations - ..." labels in execRPR.c are local
to the RPR files; the tree convention is a single banner over the block,
or none.  Consolidate to one "/* Forward declarations */" banner.  Per
Jian He's off-list review.
---
 src/backend/executor/execRPR.c | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index dca1e45be57..34638409f66 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -53,7 +53,7 @@ nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
 	winstate->nfaVisitedMaxWord = Max(winstate->nfaVisitedMaxWord, w);
 }
 
-/* Forward declarations - NFA state management */
+/* Forward declarations */
 static RPRNFAState *nfa_state_make(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
 static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
@@ -66,23 +66,19 @@ static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
 static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
 
-/* Forward declarations - NFA context management (internal) */
 static RPRNFAContext *nfa_context_make(WindowAggState *winstate);
 static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
 
-/* Forward declarations - NFA statistics */
 static void nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen);
 static void nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen);
 static void nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen);
 
-/* Forward declarations - NFA absorption */
 static void nfa_update_absorption_flags(RPRNFAContext *ctx);
 static bool nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older,
 							   RPRNFAContext *newer);
 static void nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx);
 static void nfa_absorb_contexts(WindowAggState *winstate);
 
-/* Forward declarations - NFA match and advance */
 static bool nfa_eval_var_match(WindowAggState *winstate,
 							   RPRPatternElement *elem, bool *varMatched);
 static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
-- 
2.50.1 (Apple Git-155)

From 86949cfa045558fc9754f61211f93483a144872e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 12:52:45 +0900
Subject: [PATCH 33/68] Clarify that ExecRPRCleanupDeadContexts always frees
 the failed context

The failure statistic is recorded conditionally, but the context is freed
unconditionally.  A stray "else:" comment before the free made it read as
the conditional branch; reword the comment and drop the "else:".  No code
change.  Per Jian He's off-list review.
---
 src/backend/executor/execRPR.c | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 34638409f66..16a0f4ae375 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1805,9 +1805,9 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 			continue;
 
 		/*
-		 * This is a failed context - count and remove it. Only count if it
-		 * actually processed its start row. Contexts created for
-		 * beyond-partition rows are silently removed.
+		 * Failed context: always removed below.  Only record the failure
+		 * statistic if it actually processed its start row; contexts created
+		 * for beyond-partition rows are removed without being counted.
 		 */
 		if (ctx->lastProcessedRow >= ctx->matchStartRow)
 		{
@@ -1815,7 +1815,6 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 
 			ExecRPRRecordContextFailure(winstate, failedLen);
 		}
-		/* else: context was never processed (beyond-partition), just remove */
 
 		ExecRPRFreeContext(winstate, ctx);
 	}
-- 
2.50.1 (Apple Git-155)

From 57989dcb91d0c0e37d07c4d4b79004c45b2815aa Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 13:45:48 +0900
Subject: [PATCH 34/68] Correct stale RPR comments and document a defensive
 window check

Static analysis surfaced several comments that no longer match the code:

- plannodes.h: rewrite the RPRPattern.isAbsorbable comment to match the
  implemented cases (isUnboundedStart / computeAbsorbability) and the
  normalization that enables absorption; the old text wrongly called the
  prefix/suffix merge unimplemented
- README.rpr: an unmatched row's reduced frame is empty, not "the row
  itself"; the varId range is 0-250, not 0-251
- advanced.sgml: unmatched rows show NULL window functions and
  initial-value aggregates (count() = 0), not all-NULL
- planner.c: note that the RPR fields in optimize_window_clauses' duplicate
  check are reached but defensive -- RPR clauses are separated by their
  frame options first -- kept for parity with transformWindowFuncCall
---
 doc/src/sgml/advanced.sgml           |  5 +++--
 src/backend/executor/README.rpr      |  6 +++---
 src/backend/optimizer/plan/planner.c |  5 ++++-
 src/include/nodes/plannodes.h        | 21 ++++++++++++++-------
 4 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 11c2416df51..1410a443609 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -600,8 +600,9 @@ DEFINE
     subsequent rows all window functions are shown as NULL. Aggregates on
     non-starting rows return their initial value: for example,
     <function>count()</function> returns 0 and <function>sum()</function>
-    returns NULL. For rows that do not match the PATTERN, columns are shown
-    as NULL too. Example of a <literal>SELECT</literal> using
+    returns NULL. For rows that do not match the PATTERN, window functions
+    are likewise shown as NULL and aggregates return their initial value.
+    Example of a <literal>SELECT</literal> using
     the <literal>DEFINE</literal> and <literal>PATTERN</literal> clause is as
     follows.
 
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 449bf051153..08418588114 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -245,7 +245,7 @@ RPRPatternElement struct (16 bytes):
 
   Field      Size     Description
   ---------------------------------------------------------
-  varId      1B      Variable ID (0-251) or control code (252-255)
+  varId      1B      Variable ID (0-250) or control code (252-255)
   depth      1B      Group nesting depth
   flags      1B      Bit flags (see below)
   reserved   1B      Padding
@@ -1303,11 +1303,11 @@ XI-4. Execution Trace
 
 XI-5. Final Result
 
-  Row 0: unmatched     -> frame = the row itself
+  Row 0: unmatched     -> reduced frame empty (window funcs NULL, count() 0)
   Row 1: match head    -> frame = rows 1 through 3
   Row 2: inside match  -> skipped
   Row 3: inside match  -> skipped
-  Row 4: unmatched     -> frame = the row itself
+  Row 4: unmatched     -> reduced frame empty (window funcs NULL, count() 0)
 
 Chapter XII  Summary of Key Design Decisions
 ============================================================================
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index c6fc868cdca..f43cc0edb37 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -6254,7 +6254,10 @@ optimize_window_clauses(PlannerInfo *root, WindowFuncLists *wflists)
 
 				/*
 				 * Perform the same duplicate check that is done in
-				 * transformWindowFuncCall.
+				 * transformWindowFuncCall. wc is never an RPR clause here
+				 * (those are skipped above), and an RPR existing_wc differs
+				 * in its frame options anyway, so the RPR-related comparisons
+				 * are a defensive backstop for parity.
 				 */
 				if (equal(wc->partitionClause, existing_wc->partitionClause) &&
 					equal(wc->orderClause, existing_wc->orderClause) &&
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fd4bdf2cb31..d45d93d79a2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1296,15 +1296,22 @@ typedef struct RPRPattern
 	 * Context absorption optimization.
 	 *
 	 * Absorption is only safe when later matches are guaranteed to be
-	 * suffixes of earlier matches. This requires simple pattern structure:
+	 * suffixes of earlier matches, which requires the pattern to start with
+	 * an unbounded greedy element.  Phase-1 normalization (consecutive
+	 * variable / group / ALT merging and prefix/suffix merging) rewrites the
+	 * pattern toward that form first -- so e.g. A B (A B)+ is merged to
+	 * (A B){2,} and then judged absorbable.
 	 *
-	 * Case 1: No ALT, single unbounded element (A+, (A B)+)
-	 * Case 2: Top-level ALT with each branch being single unbounded (A+ | B+)
+	 * computeAbsorbability() marks the absorbable cases (see isUnboundedStart):
+	 *   - simple unbounded VAR at the start:                    A+ B C
+	 *   - unbounded GROUP with fixed-length children:           (A B)+, (A B{2})+
+	 *   - top-level ALT with independently absorbable branches: A+ | B+
+	 *     (handled in computeAbsorbabilityRecursive)
 	 *
-	 * Complex patterns like A B (A B)+ could theoretically be transformed to
-	 * (A B){2,} for absorption, but this changes lexical order and is not
-	 * implemented. Similarly, (A|B)+ cannot be absorbed because different
-	 * start positions produce different match contents (not suffix relation).
+	 * Not absorbable: an unbounded element not at the start (A B+), a
+	 * reluctant quantifier (A+?), or an ALT inside a group ((A|B)+) -- there
+	 * different start positions yield different match contents, so later
+	 * matches are not suffixes of earlier ones.
 	 */
 	bool		isAbsorbable;	/* true if pattern supports context absorption */
 } RPRPattern;
-- 
2.50.1 (Apple Git-155)

From 6bfd39fc7cf8b758a3dd910d89c0893d57425407 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 16:35:45 +0900
Subject: [PATCH 35/68] Fix unsafe (A{n,})* quantifier flattening in row
 pattern recognition

The Phase-1 pattern optimizer's tryMultiplyQuantifiers() collapsed a
nested quantifier (A{k,})* to A* whenever both the inner and outer
bounds were unbounded.  That is only correct when the inner minimum is
0 or 1.  For k >= 2 the reachable repetition counts are {0} together
with [k, INF), so the counts 1..k-1 are unreachable; A* admits them.
As a result an isolated single A matched as length 1 where the pattern
must instead produce an empty match (length 0), widening the matching
language.

Skip the multiplication when the outer quantifier is skippable
(minimum 0) and the inner minimum is at least 2.  The existing safe
cases are unaffected: an inner minimum of 0 or 1 stays contiguous, and
a non-skippable outer such as (A{2,})+ still folds to A{2,}.

Per a report from a static analysis tool.
---
 src/backend/executor/README.rpr           |  1 +
 src/backend/optimizer/plan/rpr.c          | 18 +++++-
 src/test/regress/expected/rpr_explain.out | 41 +++++++++++++
 src/test/regress/expected/rpr_nfa.out     | 72 +++++++++++++++++++++++
 src/test/regress/sql/rpr_explain.sql      | 23 ++++++++
 src/test/regress/sql/rpr_nfa.sql          | 53 +++++++++++++++++
 6 files changed, 207 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 08418588114..3a215f2566b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -232,6 +232,7 @@ applied:
   (g) Quantifier multiplication: Collapse nested quantifiers when safe
       (A+)+ -> A+
       (A{2,3}){5} -> A{10,15}
+      (A{2,})* stays as-is (count 1 unreachable; A* would be wrong)
 
   (h) Single-child unwrap
       SEQ(A) -> A,  (A){1,1} -> A
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2a1d665c7ee..2a98977b288 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -781,7 +781,8 @@ optimizeAltPattern(RPRPatternNode *pattern)
  *		Try to multiply quantifiers.
  *
  * Multiplication is SAFE when:
- *   1. Both unbounded: (A*)* -> A*, (A+)+ -> A+
+ *   1. Both unbounded, with skipless outer or child->min <= 1:
+ *      (A*)* -> A*, (A+)+ -> A+, (A+)* -> A*, (A{2,})+ -> A{2,}
  *   2. Outer exact: (A{m,n}){k} -> A{m*k, n*k}
  *   3. Outer range + child {1,1}: (A){2,} -> A{2,}
  *
@@ -789,6 +790,9 @@ optimizeAltPattern(RPRPatternNode *pattern)
  *   - Only child unbounded: (A+){3} has different semantics
  *   - Outer range + child not {1,1}: gaps possible
  *     e.g., (A{2}){2,3} yields 4,6 only (not 4,5,6)
+ *   - Skippable outer (min 0) + child->min >= 2: (A{2,})* reaches
+ *     {0} UNION [child->min, INF), so 1..child->min-1 are unreachable
+ *     and A* would wrongly admit them
  *
  * Returns the child node with multiplied quantifiers if successful,
  * otherwise returns the original pattern unchanged.
@@ -816,6 +820,18 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
 	if (child->max == RPR_QUANTITY_INF && pattern->max == RPR_QUANTITY_INF)
 	{
+		/*
+		 * A skippable outer (min 0) over a child with min >= 2 reaches
+		 * repetition counts {0} UNION [child->min, INF): the counts
+		 * 1..child->min-1 are unreachable, and no single quantifier can
+		 * express that gap.  Flattening to A{0,INF} = A* would wrongly admit
+		 * them, e.g. (A{2,})* would match a single A.  Multiplication is safe
+		 * here only when child->min <= 1 (the reachable set is then
+		 * contiguous from 0); otherwise leave the pattern unflattened.
+		 */
+		if (pattern->min == 0 && child->min >= 2)
+			return pattern;
+
 		new_min_64 = (int64) child->min * pattern->min;
 		if (new_min_64 >= RPR_QUANTITY_INF)
 			return pattern;		/* overflow, skip optimization */
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 77079d5e8c9..9ba302b11ae 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -782,6 +782,47 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (10 rows)
 
+-- (A{2,})* must NOT flatten to a* (H-1): counts {0} UNION [2, INF) leave 1
+-- unreachable.  The planner keeps it as (a{2,})*, not a*.
+CREATE VIEW rpr_ev_nested_quant_no_flatten AS
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nested_quant_no_flatten'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN ((a{2,})*) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=6.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{2,}")*
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 18 total, 0 merged
+   NFA Contexts: 3 peak, 7 total, 2 pruned
+   NFA: 2 matched (len 2/2/2.0), 0 mismatched
+   NFA: 2 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=6.00 loops=1)
+(10 rows)
+
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 72dbf080a37..59b91ff9aa4 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -2275,6 +2275,78 @@ WINDOW w AS (
   6 | {B}   |             |          
 (6 rows)
 
+-- Nested quantifier flattening must not widen the matching language (H-1).
+-- (A{k,})* with k >= 2 reaches repetition counts {0} UNION [k, INF); the gap
+-- 1..k-1 is unreachable, so it must NOT collapse to A*.  An isolated single A
+-- must yield an EMPTY match (count 0), not a length-1 match.
+WITH test_nested_quant_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated A: (A{2,})* matches empty here, not 1
+        (2, ARRAY['_']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),  -- run of 2: matched
+        (5, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_var
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end | match_count 
+----+-------+-------------+-----------+-------------
+  1 | {A}   |             |           |           0
+  2 | {_}   |             |           |           0
+  3 | {A}   |           3 |         4 |           2
+  4 | {A}   |             |           |           0
+  5 | {_}   |             |           |           0
+(5 rows)
+
+-- Same for a GROUP child: ((A B){2,})* must not collapse to (A B)*.
+-- An isolated single (A B) pair must yield an EMPTY match (count 0).
+WITH test_nested_quant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated (A B) pair: matches empty here
+        (2, ARRAY['B']),
+        (3, ARRAY['_']),
+        (4, ARRAY['A']),
+        (5, ARRAY['B']),
+        (6, ARRAY['A']),
+        (7, ARRAY['B']),  -- run of 2 pairs: matched
+        (8, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B){2,})*)
+    DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end | match_count 
+----+-------+-------------+-----------+-------------
+  1 | {A}   |             |           |           0
+  2 | {B}   |             |           |           0
+  3 | {_}   |             |           |           0
+  4 | {A}   |           4 |         7 |           4
+  5 | {B}   |             |           |           0
+  6 | {A}   |             |           |           0
+  7 | {B}   |             |           |           0
+  8 | {_}   |             |           |           0
+(8 rows)
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index a527615849a..c8b159e30e6 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -499,6 +499,29 @@ WINDOW w AS (
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );');
 
+-- (A{2,})* must NOT flatten to a* (H-1): counts {0} UNION [2, INF) leave 1
+-- unreachable.  The planner keeps it as (a{2,})*, not a*.
+CREATE VIEW rpr_ev_nested_quant_no_flatten AS
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nested_quant_no_flatten'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);');
+
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 128476aa1d1..febf834565d 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -1583,6 +1583,59 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Nested quantifier flattening must not widen the matching language (H-1).
+-- (A{k,})* with k >= 2 reaches repetition counts {0} UNION [k, INF); the gap
+-- 1..k-1 is unreachable, so it must NOT collapse to A*.  An isolated single A
+-- must yield an EMPTY match (count 0), not a length-1 match.
+WITH test_nested_quant_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated A: (A{2,})* matches empty here, not 1
+        (2, ARRAY['_']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),  -- run of 2: matched
+        (5, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_var
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS 'A' = ANY(flags)
+);
+
+-- Same for a GROUP child: ((A B){2,})* must not collapse to (A B)*.
+-- An isolated single (A B) pair must yield an EMPTY match (count 0).
+WITH test_nested_quant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated (A B) pair: matches empty here
+        (2, ARRAY['B']),
+        (3, ARRAY['_']),
+        (4, ARRAY['A']),
+        (5, ARRAY['B']),
+        (6, ARRAY['A']),
+        (7, ARRAY['B']),  -- run of 2 pairs: matched
+        (8, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B){2,})*)
+    DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
-- 
2.50.1 (Apple Git-155)

From dcfeb7ba74a9acf49d41c4e395fd44952eed8837 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 17:03:04 +0900
Subject: [PATCH 36/68] Avoid INF-valued quantifier bound in RPR
 consecutive-merge optimization

mergeConsecutiveVars() and mergeConsecutiveGroups() accumulate the min and
max bounds of two adjacent pattern elements, guarding against overflow with
prev->min <= RPR_QUANTITY_INF - child->min (and likewise for max).  The <=
let a sum land exactly on RPR_QUANTITY_INF (INT32_MAX), the value reserved as
the sentinel for an unbounded max.  The merged element then carried
min == INF, which is not a valid finite lower bound: it tripped the Assert in
fillRPRPattern* at plan time, and in a non-assert build silently produced a
pattern that can never match.

Tighten both guards to a strict <, so a sum reaching INF falls back and
leaves the elements unmerged.  This matches the >= INF check already used by
the quantifier-multiply path and the strict bound in mergeGroupPrefixSuffix.

Per a report from a static analysis tool.
---
 src/backend/optimizer/plan/rpr.c       | 22 ++++---
 src/test/regress/expected/rpr_base.out | 83 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 43 +++++++++++++
 3 files changed, 140 insertions(+), 8 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2a98977b288..b989fcc5162 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -245,13 +245,16 @@ mergeConsecutiveVars(List *children)
 			/* ----------------------
 			 * Can merge consecutive VAR nodes if:
 			 * 1. Same variable name
-			 * 2. No min overflow: prev->min + child->min <= INF
-			 * 3. No max overflow: prev->max + child->max <= INF (or either is INF)
+			 * 2. No min overflow: prev->min + child->min < INF
+			 * 3. No max overflow: prev->max + child->max < INF (or either is INF)
+			 *
+			 * Strict <: a sum equal to INF would alias the unbounded sentinel
+			 * (min must stay finite; a finite max must not become INF).
 			 */
 			if (prev != NULL &&
 				strcmp(prev->varName, child->varName) == 0 &&
-				prev->min <= RPR_QUANTITY_INF - child->min &&
-				(prev->max <= RPR_QUANTITY_INF - child->max ||
+				prev->min < RPR_QUANTITY_INF - child->min &&
+				(prev->max < RPR_QUANTITY_INF - child->max ||
 				 prev->max == RPR_QUANTITY_INF ||
 				 child->max == RPR_QUANTITY_INF))
 			{
@@ -319,13 +322,16 @@ mergeConsecutiveGroups(List *children)
 			/* ----------------------
 			 * Can merge consecutive GROUP nodes if:
 			 * 1. Identical children
-			 * 2. No min overflow: prev->min + child->min <= INF
-			 * 3. No max overflow: prev->max + child->max <= INF (or either is INF)
+			 * 2. No min overflow: prev->min + child->min < INF
+			 * 3. No max overflow: prev->max + child->max < INF (or either is INF)
+			 *
+			 * Strict <: a sum equal to INF would alias the unbounded sentinel
+			 * (min must stay finite; a finite max must not become INF).
 			 */
 			if (prev != NULL &&
 				rprPatternChildrenEqual(prev->children, child->children) &&
-				prev->min <= RPR_QUANTITY_INF - child->min &&
-				(prev->max <= RPR_QUANTITY_INF - child->max ||
+				prev->min < RPR_QUANTITY_INF - child->min &&
+				(prev->max < RPR_QUANTITY_INF - child->max ||
 				 prev->max == RPR_QUANTITY_INF ||
 				 child->max == RPR_QUANTITY_INF))
 			{
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c96a1216cc3..ebc1088018a 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3459,6 +3459,25 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Consecutive VAR merge at the boundary: A{1073741823,} A{1073741823,} ->
+-- a{2147483646,}.  The min sum 2147483646 = INT32_MAX - 1 is the largest
+-- still-finite bound, so the merge proceeds; a sum of exactly INF instead
+-- falls back (see the Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A{1073741823,} A{1073741823,}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{2147483646,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -3509,6 +3528,25 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,}
+-- -> (a b){2147483646,}.  The min sum INT32_MAX - 1 is still finite, so the
+-- merge proceeds; a sum of exactly INF instead falls back (see the
+-- Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A B){1073741823,} (A B){1073741823,}) DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' b'){2147483646,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- PREFIX merge: A B (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -5269,6 +5307,51 @@ WINDOW w AS (
 (7 rows)
 
 -- Expected: Fallback - prefix elements don't match GROUP content
+-- Test: consecutive VAR merge whose min sum is exactly INF causes fallback.
+-- 1073741824 + 1073741823 = 2147483647 = INT32_MAX = RPR_QUANTITY_INF.
+-- Merging would yield a VAR with min == INF, so the merge must fall back and
+-- leave the two VARs unmerged (mirrors the multiply path's >= INF guard).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1073741824,} A{1073741823,})
+    DEFINE A AS val > 0
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{1073741824,}" a{1073741823,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_fallback
+(7 rows)
+
+-- Expected: Fallback - VARs not merged (min sum 2147483647 == INF)
+-- Test: consecutive GROUP merge whose min sum is exactly INF causes fallback.
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){1073741824,} (A B){1073741823,})
+    DEFINE A AS val > 0, B AS val > 5
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' b'){1073741824,}" (a b){1073741823,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_fallback
+(7 rows)
+
+-- Expected: Fallback - GROUPs not merged (min sum 2147483647 == INF)
 DROP TABLE rpr_fallback;
 -- ============================================================
 -- Planner Integration Tests
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index d7b63cfc690..63173615273 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2386,6 +2386,15 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN (A A+) DEFINE A AS val > 0);
 
+-- Consecutive VAR merge at the boundary: A{1073741823,} A{1073741823,} ->
+-- a{2147483646,}.  The min sum 2147483646 = INT32_MAX - 1 is the largest
+-- still-finite bound, so the merge proceeds; a sum of exactly INF instead
+-- falls back (see the Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A{1073741823,} A{1073741823,}) DEFINE A AS val > 0);
+
 -- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2406,6 +2415,15 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A B){2} (A B)+) DEFINE A AS val <= 50, B AS val > 50);
 
+-- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,}
+-- -> (a b){2147483646,}.  The min sum INT32_MAX - 1 is still finite, so the
+-- merge proceeds; a sum of exactly INF instead falls back (see the
+-- Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A B){1073741823,} (A B){1073741823,}) DEFINE A AS val <= 50, B AS val > 50);
+
 -- PREFIX merge: A B (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -3213,6 +3231,31 @@ WINDOW w AS (
 );
 -- Expected: Fallback - prefix elements don't match GROUP content
 
+-- Test: consecutive VAR merge whose min sum is exactly INF causes fallback.
+-- 1073741824 + 1073741823 = 2147483647 = INT32_MAX = RPR_QUANTITY_INF.
+-- Merging would yield a VAR with min == INF, so the merge must fall back and
+-- leave the two VARs unmerged (mirrors the multiply path's >= INF guard).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1073741824,} A{1073741823,})
+    DEFINE A AS val > 0
+);
+-- Expected: Fallback - VARs not merged (min sum 2147483647 == INF)
+
+-- Test: consecutive GROUP merge whose min sum is exactly INF causes fallback.
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){1073741824,} (A B){1073741823,})
+    DEFINE A AS val > 0, B AS val > 5
+);
+-- Expected: Fallback - GROUPs not merged (min sum 2147483647 == INF)
+
 DROP TABLE rpr_fallback;

From 176f477a820f40ab31cb34fcf4493de60b004579 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 21:36:38 +0900
Subject: [PATCH 37/68] Fix count slot leak in row pattern recognition
 absorption

When a leaf variable in an absorbable group reaches its maximum count,
the match-phase inline advance walks it through the enclosing END
elements without clearing the variable's own counts[] slot, unlike the
regular exit in nfa_advance_var.  Sibling elements at the same depth
share a slot, so the stale count leaked into a following sibling group
and tripped the max-count Assert -- or, in a non-assert build, produced
a wrong match count.  PATTERN ((A B)+ (C D)+) is the smallest reproducer.

Clear the leaf variable's count on the inline exit.  Assert the
resulting contract -- each element zeroes its own slot on exit, so a
variable or group is entered with a zero count -- at the entry points,
and drop the now-redundant loop-back body reset.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c        | 55 +++++++++++++++++++++------
 src/test/regress/expected/rpr_nfa.out | 42 ++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 31 +++++++++++++++
 3 files changed, 117 insertions(+), 11 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 16a0f4ae375..9e45920de9c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -315,8 +315,9 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 	 * The +1 is the slot arithmetic: comparing through depth N requires
 	 * counts[0..N], i.e., N+1 entries.  Deeper slots (counts[d] with d >
 	 * elem->depth) are excluded because they hold scratch state from inner
-	 * groups that gets zeroed on re-entry (see END loop-back in
-	 * nfa_advance_end), and so must not participate in equivalence judgment.
+	 * groups.  Per the count-clear policy such a slot is zeroed when its
+	 * owning element exits (see nfa_advance_var and the inline fast path in
+	 * nfa_match), so it must not participate in equivalence judgment.
 	 */
 	elem = &pattern->elements[s1->elemIdx];
 	compareDepth = elem->depth + 1;
@@ -858,6 +859,16 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 					state->elemIdx = elem->next;
 					state->counts[endDepth] = endCount;
 
+					/*
+					 * Leaf VAR exited (reached max): clear its own count so
+					 * the next occupant enters with zero, as nfa_advance_var
+					 * does on exit (this inline path replaces that exit).
+					 * depth > endDepth, so this leaves the group count just
+					 * written intact.
+					 */
+					Assert(endDepth < depth);
+					state->counts[depth] = 0;
+
 					/*
 					 * Chain through END elements within the absorbable region
 					 * (ABSORBABLE_BRANCH) until reaching the judgment point
@@ -873,7 +884,13 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 						int			outerDepth = outerEnd->depth;
 						int32		outerCount = state->counts[outerDepth];
 
-						/* Reset exited group's count */
+						/*
+						 * 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.
+						 */
 						state->counts[endDepth] = 0;
 
 						/* Increment outer group count */
@@ -925,6 +942,15 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 	{
 		RPRNFAState *skipState = NULL;
 
+		/*
+		 * Entry-side check of the count-clear policy: a VAR is always routed
+		 * to with a clean slot.  Each element zeroes its own count on exit,
+		 * so a nonzero count here would be a leak from an earlier element
+		 * (see nfa_advance_var / nfa_advance_end exit handling and the inline
+		 * fast path in nfa_match).
+		 */
+		Assert(state->counts[nextElem->depth] == 0);
+
 		/* Create skip state before add_unique, which may free state */
 		if (RPRElemCanSkip(nextElem))
 			skipState = nfa_state_clone(winstate, nextElem->next,
@@ -989,9 +1015,10 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
  * nfa_advance_begin
  *
  * Handle BEGIN element: group entry logic.
- * BEGIN is only visited at initial group entry (count is always 0).
+ * BEGIN is only visited at initial group entry; loop-back from END goes
+ * directly to first child, bypassing BEGIN.  Per the count-clear policy the
+ * group's own count slot is therefore already zero on entry (asserted below).
  * If min=0, creates a skip path past the group.
- * Loop-back from END goes directly to first child, bypassing BEGIN.
  */
 static void
 nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
@@ -1002,7 +1029,12 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	RPRPatternElement *elements = pattern->elements;
 	RPRNFAState *skipState = NULL;
 
-	state->counts[elem->depth] = 0;
+	/*
+	 * Entry-side check of the count-clear policy: the group's own count slot
+	 * is already zero here.  BEGIN is only visited at initial group entry,
+	 * and the previous occupant of this depth slot cleared it on exit.
+	 */
+	Assert(state->counts[elem->depth] == 0);
 
 	/* Optional group: create skip path (but don't route yet) */
 	if (elem->min == 0)
@@ -1094,8 +1126,6 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 									  state->counts, state->isAbsorbable);
 
 		/* Primary path: loop back for real matches */
-		for (int d = depth + 1; d < pattern->maxDepth; d++)
-			state->counts[d] = 0;
 		state->elemIdx = elem->jump;
 		jumpElem = &elements[state->elemIdx];
 		nfa_route_to_elem(winstate, ctx, state, jumpElem,
@@ -1112,6 +1142,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		{
 			RPRPatternElement *nextElem;
 
+			/* Exit the group: clear its own count (count-clear policy) */
 			ffState->counts[depth] = 0;
 			ffState->elemIdx = elem->next;
 			nextElem = &elements[ffState->elemIdx];
@@ -1130,6 +1161,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Must exit: reached max iterations. */
 		RPRPatternElement *nextElem;
 
+		/* Exit: clear the group's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
@@ -1161,6 +1193,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 */
 		exitState = nfa_state_clone(winstate, elem->next,
 									state->counts, state->isAbsorbable);
+		/* Exit branch: clear the group's own count (count-clear policy) */
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
@@ -1169,8 +1202,6 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			exitState->counts[nextElem->depth]++;
 
 		/* Prepare loop state */
 
@@ -1253,6 +1284,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			/* Clone for exit, original stays for loop */
 			cloneState = nfa_state_clone(winstate, elem->next,
 										 state->counts, state->isAbsorbable);
+			/* Exit: clear the VAR's own count (count-clear policy) */
 			cloneState->counts[depth] = 0;
 			nextElem = &elements[cloneState->elemIdx];
 
@@ -1289,7 +1321,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			/* Loop first (preferred for greedy) */
 			nfa_add_state_unique(winstate, ctx, cloneState);
 
-			/* Exit second */
+			/* Exit second: clear the VAR's own count (count-clear policy) */
 			state->counts[depth] = 0;
 			state->elemIdx = elem->next;
 			nextElem = &elements[state->elemIdx];
@@ -1326,6 +1358,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Exit only: advance to next element */
 		RPRPatternElement *nextElem;
 
+		/* Exit: clear the VAR's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 59b91ff9aa4..2a0d4f11e74 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -447,6 +447,48 @@ WINDOW w AS (
   7 | {X}   |             |          
 (7 rows)
 
+-- Two consecutive unbounded groups: (A B)+ (C D)+
+-- The leading group (A B)+ is absorbable (unbounded multi-element); (C D)+ is
+-- a distinct sibling group that does not merge with it.  When the leading group
+-- exits into the sibling, its body leaf-VAR count must be cleared so it does
+-- not leak into the sibling's shared depth slot.
+WITH test_absorb_two_groups AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['C']),
+        (8, ARRAY['D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_two_groups
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B)+ (C D)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {A}   |           5 |         8
+  6 | {B}   |             |          
+  7 | {C}   |             |          
+  8 | {D}   |             |          
+(8 rows)
+
 -- Fixed-length group absorption: (A B{2})+ C
 -- B{2} has min == max, equivalent to unrolling to (A B B)+ C
 WITH test_absorb_fixedlen AS (
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index febf834565d..6362c69f385 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -346,6 +346,37 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Two consecutive unbounded groups: (A B)+ (C D)+
+-- The leading group (A B)+ is absorbable (unbounded multi-element); (C D)+ is
+-- a distinct sibling group that does not merge with it.  When the leading group
+-- exits into the sibling, its body leaf-VAR count must be cleared so it does
+-- not leak into the sibling's shared depth slot.
+WITH test_absorb_two_groups AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['C']),
+        (8, ARRAY['D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_two_groups
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B)+ (C D)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+
 -- Fixed-length group absorption: (A B{2})+ C
 -- B{2} has min == max, equivalent to unrolling to (A B B)+ C
 WITH test_absorb_fixedlen AS (
-- 
2.50.1 (Apple Git-155)

From cd08e4296f499b9c3db3b7926a625f63455a815e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 22:15:04 +0900
Subject: [PATCH 38/68] Demote dead runtime checks in the RPR executor to
 assertions

Three checks in the row pattern recognition NFA executor can never fail,
given how the planner builds the pattern:

- The final arm of nfa_advance_var's loop/exit cascade tested canExit,
  but reaching it implies !canLoop, which with min <= max forces canExit
  true.
- nfa_state_clone guarded the counts memcpy with counts != NULL &&
  maxDepth > 0, but every caller passes a live state's counts and the
  stored maxDepth is always >= 1.
- nfa_advance_alt bounded the branch walk with altIdx < numElements, but
  every jump/next link is -1 or a valid index, and the altIdx >= 0 test
  and the depth break already terminate the walk.

Replace each with an assertion that documents the invariant and drop the
runtime branch.  Also document why the nullable fast-forward path in
nfa_advance_end intentionally omits the isAbsorbable update: it runs only
for EMPTY_LOOP groups, which are never inside an absorbable region.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c | 25 +++++++++++++++++++------
 1 file changed, 19 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 9e45920de9c..eac0c04c38d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -279,8 +279,9 @@ nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
 	RPRPatternElement *elem = &pattern->elements[elemIdx];
 
 	state->elemIdx = elemIdx;
-	if (counts != NULL && maxDepth > 0)
-		memcpy(state->counts, counts, sizeof(int32) * maxDepth);
+	/* Every reachable caller passes a live state's counts; maxDepth >= 1. */
+	Assert(counts != NULL && maxDepth > 0);
+	memcpy(state->counts, counts, sizeof(int32) * maxDepth);
 
 	/*
 	 * Compute isAbsorbable immediately at transition time. isAbsorbable =
@@ -981,11 +982,15 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 	RPRPatternElement *elements = pattern->elements;
 	RPRElemIdx	altIdx = elem->next;
 
-	while (altIdx >= 0 && altIdx < pattern->numElements)
+	while (altIdx >= 0)
 	{
-		RPRPatternElement *altElem = &elements[altIdx];
+		RPRPatternElement *altElem;
 		RPRNFAState *newState;
 
+		/* Branch jump/next links are always -1 or a valid index */
+		Assert(altIdx < pattern->numElements);
+		altElem = &elements[altIdx];
+
 		/*
 		 * Stop if element is outside ALT scope (not a branch).  The check
 		 * fires when the last branch is a quantified group whose BEGIN.jump
@@ -1147,6 +1152,13 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			ffState->elemIdx = elem->next;
 			nextElem = &elements[ffState->elemIdx];
 
+			/*
+			 * Unlike the must-exit path, no isAbsorbable update is needed:
+			 * the fast-forward path runs only for EMPTY_LOOP (nullable)
+			 * groups, which are never inside an absorbable region, so
+			 * isAbsorbable is already false here.
+			 */
+
 			/* END->END: increment outer END's count */
 			if (RPRElemIsEnd(nextElem) &&
 				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
@@ -1353,11 +1365,12 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Loop only: keep state as-is */
 		nfa_add_state_unique(winstate, ctx, state);
 	}
-	else if (canExit)
+	else
 	{
-		/* Exit only: advance to next element */
+		/* Exit only: advance to next element (canExit necessarily true) */
 		RPRPatternElement *nextElem;
 
+		Assert(canExit);
 		/* Exit: clear the VAR's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
-- 
2.50.1 (Apple Git-155)

From fb200f06d1202c13b85935c40d2a5d80c1da584e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 23:49:46 +0900
Subject: [PATCH 40/68] Honor reluctant quantifier for non-leading optional RPR
 variables

A reluctant optional pattern variable that is not the leading element --
for example A?? in PATTERN (B A?? C) -- matched greedily.  nfa_route_to_elem
builds the skip path of a min=0 variable in a fixed order, adding the enter
(match) state before the skip state.  Since the state list order encodes
match preference, the enter path always won, ignoring RPRElemIsReluctant.
Leading variables and optional groups were unaffected: they go through
nfa_advance_var / nfa_advance_begin, which already order the skip path first
when reluctant.

Mirror that handling in nfa_route_to_elem: when the optional variable is
reluctant, route the skip path first and drop the enter state if the skip
path has already reached FIN.

Add regression tests for non-leading reluctant optional variables and groups.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c        | 29 ++++++++++-
 src/test/regress/expected/rpr_nfa.out | 69 +++++++++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 56 ++++++++++++++++++++++
 3 files changed, 152 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index a2c304da0d1..580b25f398d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -957,10 +957,35 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 			skipState = nfa_state_clone(winstate, nextElem->next,
 										state->counts, state->isAbsorbable);
 
-		nfa_add_state_unique(winstate, ctx, state);
+		if (skipState != NULL && RPRElemIsReluctant(nextElem))
+		{
+			RPRNFAState *savedMatch = ctx->matchedState;
 
-		if (skipState != NULL)
+			/*
+			 * Reluctant optional VAR: prefer skipping.  Explore the skip path
+			 * first so it outranks the enter (match) path; if it reaches FIN
+			 * the shortest match is found and the enter state is dropped.
+			 * This mirrors the reluctant branch of nfa_advance_begin used by
+			 * the leading-position and optional-group paths.
+			 */
 			nfa_advance_state(winstate, ctx, skipState, currentPos);
+
+			if (ctx->matchedState != savedMatch)
+			{
+				nfa_state_free(winstate, state);
+				return;
+			}
+
+			nfa_add_state_unique(winstate, ctx, state);
+		}
+		else
+		{
+			/* Greedy (or non-skippable): enter first, then skip */
+			nfa_add_state_unique(winstate, ctx, state);
+
+			if (skipState != NULL)
+				nfa_advance_state(winstate, ctx, skipState, currentPos);
+		}
 	}
 	else
 	{
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 2a0d4f11e74..829e8251aed 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1222,6 +1222,75 @@ WINDOW w AS (
   3 | {C}   |             |          
 (3 rows)
 
+-- Non-leading reluctant optional VAR: (B A?? C)
+-- Reluctant A?? should prefer to skip, matching B(1) C(2) with A left
+-- unmatched (match_end 2).  The leading/group reluctant cases above go through
+-- the begin path; this exercises the non-leading skip path in
+-- nfa_route_to_elem, which must honor reluctant ordering too.
+WITH test_nonleading_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B A?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {A,C} |             |          
+  3 | {C}   |             |          
+(3 rows)
+
+-- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
+-- Like the VAR case above but a multi-element group; it goes through the
+-- begin path (nfa_advance_begin), which already honors reluctant ordering.
+-- Reluctant (A X)?? should skip, matching B(1) C(2), with the group skipped
+-- to the following C (not to FIN).
+WITH test_nonleading_reluctant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['X']),
+        (4, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B (A X)?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        X AS 'X' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {A,C} |             |          
+  3 | {X}   |             |          
+  4 | {C}   |             |          
+(4 rows)
+
 -- Greedy/reluctant sequence: A+ B+? (greedy A, reluctant B at end)
 -- A consumes greedily, B+? exits to FIN after minimum match
 WITH test_greedy_then_reluctant AS (
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 6362c69f385..3bbec496279 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -860,6 +860,62 @@ WINDOW w AS (
         C AS 'C' = ANY(flags)
 );
 
+-- Non-leading reluctant optional VAR: (B A?? C)
+-- Reluctant A?? should prefer to skip, matching B(1) C(2) with A left
+-- unmatched (match_end 2).  The leading/group reluctant cases above go through
+-- the begin path; this exercises the non-leading skip path in
+-- nfa_route_to_elem, which must honor reluctant ordering too.
+WITH test_nonleading_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B A?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
+-- Like the VAR case above but a multi-element group; it goes through the
+-- begin path (nfa_advance_begin), which already honors reluctant ordering.
+-- Reluctant (A X)?? should skip, matching B(1) C(2), with the group skipped
+-- to the following C (not to FIN).
+WITH test_nonleading_reluctant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['X']),
+        (4, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B (A X)?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        X AS 'X' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
 -- Greedy/reluctant sequence: A+ B+? (greedy A, reluctant B at end)
 -- A consumes greedily, B+? exits to FIN after minimum match
 WITH test_greedy_then_reluctant AS (
-- 
2.50.1 (Apple Git-155)

From 30e1c3dcea5abe8b0909d047550eb589e8e0a248 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 1 Jun 2026 22:38:55 +0900
Subject: [PATCH 39/68] Fix memory leak in row pattern recognition DEFINE
 evaluation

The row pattern recognition scan evaluates every DEFINE expression once
per partition row in update_reduced_frame, but did so with the plain
ExecEvalExpr in the long-lived query context and never reset it.  By-ref
scratch -- a NUMERIC DEFINE such as price * 1.1 > 165 is the typical case
-- therefore accumulated for the whole forward scan, growing memory
O(partition size) within a single window.

Evaluate the DEFINE expressions in the per-tuple memory context with
ExecEvalExprSwitchContext, in both nfa_evaluate_row and
nfa_reevaluate_dependent_vars, and reset that context once per processed
row in the scan loop.  The boolean results are consumed immediately, and
the cross-row state -- nfaVarMatched in the node context and the NFA
states in the partition context -- lives elsewhere, so the reset is safe.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c       |  3 ++-
 src/backend/executor/nodeWindowAgg.c | 10 ++++++++--
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index eac0c04c38d..a2c304da0d1 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1706,7 +1706,8 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 			Datum		result;
 			bool		isnull;
 
-			result = ExecEvalExpr(exprState, econtext, &isnull);
+			/* Per-tuple context; scratch freed by the per-row reset */
+			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
 			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
 		}
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 0b0196e7e40..408bbc120b7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4515,6 +4515,12 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		 * appropriately as pruned or mismatched.
 		 */
 		ExecRPRCleanupDeadContexts(winstate, targetCtx);
+
+		/*
+		 * Free this row's per-tuple DEFINE-evaluation scratch; cross-row
+		 * state (nfaVarMatched, NFA states) lives in other contexts.
+		 */
+		ResetExprContext(winstate->ss.ps.ps_ExprContext);
 	}
 
 register_result:
@@ -4606,8 +4612,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 		Datum		result;
 		bool		isnull;
 
-		/* Evaluate DEFINE expression */
-		result = ExecEvalExpr(exprState, econtext, &isnull);
+		/* Per-tuple context so by-ref scratch is freed by the per-row reset */
+		result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
 
 		varMatched[varIdx] = (!isnull && DatumGetBool(result));
 
-- 
2.50.1 (Apple Git-155)

From 8112fe3f2c1bee4fc330df0b12d737abd4d27903 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Tue, 2 Jun 2026 00:05:05 +0900
Subject: [PATCH 41/68] Reject column-less compound navigation in row pattern
 recognition

A row pattern navigation argument must contain at least one column
reference.  The check was applied only to simple navigation; a compound
form such as PREV(FIRST(1)) took the nesting-validation/flatten branch in
parse_rpr.c's define_walker, which never re-checked has_column_ref.  So
column-less compound navigation -- PREV(FIRST(1)), NEXT(LAST(1 + 2)),
PREV(FIRST(1), 2), and so on -- was silently accepted, while the simple
PREV(1) was correctly rejected.

Apply the same has_column_ref check after flattening a compound
navigation, so both forms are rejected consistently.  A compound form
whose argument references a column, such as PREV(FIRST(price)), is
unaffected.

Add regression tests covering column-less compound navigation with the
offset on the inner nav, the outer nav, both, or neither.

Per a report from a static analysis tool.
---
 src/backend/parser/parse_rpr.c    | 10 +++++
 src/test/regress/expected/rpr.out | 62 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 52 ++++++++++++++++++++++++++
 3 files changed, 124 insertions(+)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index fa8c375f48b..6f44edfc50b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -641,6 +641,16 @@ define_walker(Node *node, void *context)
 					nav->offset_arg = inner->offset_arg;
 					nav->arg = inner->arg;
 					flattened = true;
+
+					/*
+					 * The flattened argument must include a column reference,
+					 * just like the simple-nav case below.
+					 */
+					if (!ctx->has_column_ref)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("argument of row pattern navigation operation must include at least one column reference"),
+								parser_errposition(ctx->pstate, nav->location));
 				}
 				else if (!outer_phys && inner_phys)
 					ereport(ERROR,
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 8793dda3cc3..550113700a9 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1133,6 +1133,68 @@ WINDOW w AS (
 ERROR:  argument of row pattern navigation operation must include at least one column reference
 LINE 7:     DEFINE A AS PREV(1, 1) > 0
                         ^
+-- Compound navigation without a column reference must be rejected too,
+-- consistent with the simple forms above.
+-- PREV(FIRST(1)): compound, constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1)) > 0
+                        ^
+-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(LAST(1 + 2)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS NEXT(LAST(1 + 2)) > 0
+                        ^
+-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1, 2)) > 0
+                        ^
+-- PREV(FIRST(1), 2): compound, outer offset only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1), 2) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1), 2) > 0
+                        ^
+-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+                        ^
 -- Non-constant offset: column reference as offset
 SELECT price FROM stock
 WINDOW w AS (
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e4790f75b0a..0aa17f01e84 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -531,6 +531,58 @@ WINDOW w AS (
     DEFINE A AS PREV(1, 1) > 0
 );
 
+-- Compound navigation without a column reference must be rejected too,
+-- consistent with the simple forms above.
+-- PREV(FIRST(1)): compound, constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1)) > 0
+);
+
+-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(LAST(1 + 2)) > 0
+);
+
+-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2)) > 0
+);
+
+-- PREV(FIRST(1), 2): compound, outer offset only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1), 2) > 0
+);
+
+-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+);
+
 -- Non-constant offset: column reference as offset
 SELECT price FROM stock
 WINDOW w AS (
-- 
2.50.1 (Apple Git-155)

From 7f127147ae7fd860dc71d22ac539ea7644a77c6f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 3 Jun 2026 20:10:59 +0900
Subject: [PATCH 42/68] Reserve the high varId nibble for row pattern
 recognition control elements

Row pattern recognition packs both pattern variables and pattern control
elements (group begin/end, alternation, finish) into a single uint8 varId.
Reserve the entire high nibble, the 16 values 0xF0-0xFF, for control
elements, so pattern variables use varId 0 to 0xEF and at most 240 distinct
variables are allowed.

Reserving all 16 high-nibble values, rather than only the ones currently
needed, leaves spare encodings for future control elements.  RPR_VARID_MAX
denotes the maximum variable varId (0xEF), and the parser limit check, the
bounds assertions, and the variable-name stack are sized from it.  The
boundary regression test exercises the 240/241 limits.
---
 src/backend/optimizer/plan/rpr.c       |  6 +++---
 src/backend/parser/parse_rpr.c         | 18 ++++++++++++------
 src/include/optimizer/rpr.h            | 20 ++++++++++++--------
 src/test/regress/expected/rpr_base.out | 20 ++++++++------------
 src/test/regress/sql/rpr_base.sql      | 18 +++++++-----------
 5 files changed, 42 insertions(+), 40 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index b989fcc5162..4652ca6ebeb 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1017,7 +1017,7 @@ collectDefineVariables(List *defineVariableList, char **varNames)
 	foreach(lc, defineVariableList)
 	{
 		/* Parser already checked this limit in transformDefineClause */
-		Assert(numVars < RPR_VARID_MAX);
+		Assert(numVars <= RPR_VARID_MAX);
 
 		varNames[numVars++] = strVal(lfirst(lc));
 	}
@@ -1076,7 +1076,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			 * to varNames so they get a varId >= defineVariableList length,
 			 * which executor treats as TRUE.
 			 */
-			Assert(*numVars < RPR_VARID_MAX);
+			Assert(*numVars <= RPR_VARID_MAX);
 			varNames[(*numVars)++] = node->varName;
 			break;
 
@@ -1954,7 +1954,7 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 {
 	RPRPattern *result;
 	RPRPatternNode *optimized;
-	char	   *varNamesStack[RPR_VARID_MAX];
+	char	   *varNamesStack[RPR_VARID_MAX + 1];
 	int			numVars;
 	int			numElements;
 	RPRDepth	maxDepth;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 6f44edfc50b..3872035110a 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -6,7 +6,7 @@
  * This file transforms RPR-related clauses from raw parse tree to planner
  * structures during query analysis:
  *   - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
- *   - Validates PATTERN variable count (max RPR_VARID_MAX)
+ *   - Validates PATTERN variable count (max RPR_VARID_MAX + 1)
  *   - Transforms DEFINE clause into TargetEntry list
  *   - Stores PATTERN/SKIP TO/INITIAL clauses for planner
  *
@@ -192,10 +192,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 /*
  * validateRPRPatternVarCount
- *		Validate that PATTERN variables don't exceed RPR_VARID_MAX.
+ *		Validate that PATTERN variable count fits the varId range.
  *
  * Recursively traverses the pattern tree, collecting unique variable names.
- * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
+ * 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.
@@ -231,12 +232,17 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 				}
 				if (!found)
 				{
-					/* Check against RPR_VARID_MAX before adding */
-					if (list_length(*varNames) >= RPR_VARID_MAX)
+					/*
+					 * Check against RPR_VARID_MAX before adding.  varId
+					 * values run 0 to RPR_VARID_MAX inclusive, so the next
+					 * varId to be assigned (the current list length) must not
+					 * exceed it.
+					 */
+					if (list_length(*varNames) > RPR_VARID_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
 								errmsg("too many pattern variables"),
-								errdetail("Maximum is %d.", RPR_VARID_MAX),
+								errdetail("Maximum is %d.", RPR_VARID_MAX + 1),
 								parser_errposition(pstate,
 												   exprLocation((Node *) node)));
 
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 63c4b09daff..2fa483320ce 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -18,10 +18,14 @@
 
 /* Limits and special values */
 /*
- * Maximum number of unique pattern variables (varId 0 to RPR_VARID_MAX - 1).
- * Values from RPR_VARID_BEGIN (252) onward are reserved for control elements.
+ * Maximum pattern variable ID.  Pattern variables occupy varId 0 to
+ * RPR_VARID_MAX inclusive (240 distinct variables); any varId with the high
+ * nibble set (0xF0 to 0xFF) is reserved for control elements.  Reserving the
+ * whole high nibble, rather than just the values currently in use, leaves
+ * room for future control elements; this range can only be narrowed safely
+ * before release.
  */
-#define RPR_VARID_MAX		251
+#define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
 #define RPR_QUANTITY_INF	INT32_MAX	/* unbounded quantifier */
 #define RPR_COUNT_MAX		INT32_MAX	/* max runtime count (NFA state) */
 #define RPR_ELEMIDX_MAX		INT16_MAX	/* max pattern elements */
@@ -29,11 +33,11 @@
 #define RPR_DEPTH_MAX		(UINT8_MAX - 1) /* max pattern nesting depth: 254 */
 #define RPR_DEPTH_NONE		UINT8_MAX	/* no enclosing group (top-level) */
 
-/* Special varId values for control elements (252-255) */
-#define RPR_VARID_BEGIN		((RPRVarId) 252)	/* group begin */
-#define RPR_VARID_END		((RPRVarId) 253)	/* group end */
-#define RPR_VARID_ALT		((RPRVarId) 254)	/* alternation start */
-#define RPR_VARID_FIN		((RPRVarId) 255)	/* pattern finish */
+/* Reserved control-element varIds (high nibble 0xF; 0xF0-0xFB spare) */
+#define RPR_VARID_BEGIN		((RPRVarId) 0xFC)	/* group begin */
+#define RPR_VARID_END		((RPRVarId) 0xFD)	/* group end */
+#define RPR_VARID_ALT		((RPRVarId) 0xFE)	/* alternation start */
+#define RPR_VARID_FIN		((RPRVarId) 0xFF)	/* pattern finish */
 
 /* Element flags */
 #define RPR_ELEM_RELUCTANT			0x01	/* reluctant (non-greedy)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index ebc1088018a..e9303af6384 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -6280,12 +6280,12 @@ ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:       B AS TRUE
               ^
 -- Expected: Error - B is not used in PATTERN
--- Test: 251 variables in PATTERN and DEFINE (boundary - should succeed)
+-- Test: 240 variables in PATTERN and DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -6310,9 +6310,7 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
  count 
 -------
@@ -6321,12 +6319,12 @@ WINDOW w AS (
 (2 rows)
 
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
--- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+-- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -6351,13 +6349,11 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 ERROR:  too many pattern variables
-DETAIL:  Maximum is 251.
--- Expected: ERROR - too many pattern variables (Maximum is 251)
+DETAIL:  Maximum is 240.
+-- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 63173615273..475541d4550 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -3948,12 +3948,12 @@ WINDOW w AS (
 );
 -- Expected: Error - B is not used in PATTERN
 
--- Test: 251 variables in PATTERN and DEFINE (boundary - should succeed)
+-- Test: 240 variables in PATTERN and DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -3978,18 +3978,16 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
 
--- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+-- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -4014,11 +4012,9 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
--- Expected: ERROR - too many pattern variables (Maximum is 251)
+-- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
-- 
2.50.1 (Apple Git-155)

From c0aa0a1ca48f6c520658a50560b86d708118b9a0 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Wed, 3 Jun 2026 23:10:49 +0900
Subject: [PATCH 43/68] Generalize quantifier multiplication in row pattern
 recognition

The planner flattens a nested pattern quantifier (child{p,q}){m,n} into
child{p*m, q*n}.  This is valid only when the repetition counts the nested
quantifiers can produce form exactly one contiguous interval.  Replace the
previous handful of special cases with that general test: fold when the
outer count is exact (m == n), or the child is skippable (p == 0), or the
per-iteration intervals touch (p <= Max(m,1)*(q-p) + 1) and, for a skippable
outer, the zero case connects to the child range (m >= 1 or p <= 1).

This additionally folds contiguous cases that were previously left
unflattened, such as (A{2,3}){2,3} -> a{4,9}, (A+){3} -> a{3,}, and
(A+){2,4} -> a{2,}, while still leaving gap-producing patterns like
(A{2}){2,3} and (A{2,})* untouched.  The int64 overflow guards on the
multiplied bounds are retained.

Expand the quatifier-multiplication regression cases to exercise each
branch with both folding and gap (no-fold) patterns.  The pattern-nesting
depth tests now use reluctant quantifiers, which are not subject to
multiplication, so they still build the intended nesting depth.
---
 src/backend/optimizer/plan/rpr.c          | 130 +++++++++---------
 src/test/regress/expected/rpr_base.out    | 154 +++++++++++++++++++++-
 src/test/regress/expected/rpr_explain.out |  16 +--
 src/test/regress/sql/rpr_base.sql         |  72 +++++++++-
 4 files changed, 284 insertions(+), 88 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 4652ca6ebeb..617a3869948 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -784,21 +784,25 @@ optimizeAltPattern(RPRPatternNode *pattern)
 
 /*
  * tryMultiplyQuantifiers
- *		Try to multiply quantifiers.
- *
- * Multiplication is SAFE when:
- *   1. Both unbounded, with skipless outer or child->min <= 1:
- *      (A*)* -> A*, (A+)+ -> A+, (A+)* -> A*, (A{2,})+ -> A{2,}
- *   2. Outer exact: (A{m,n}){k} -> A{m*k, n*k}
- *   3. Outer range + child {1,1}: (A){2,} -> A{2,}
- *
- * Multiplication is NOT safe when:
- *   - Only child unbounded: (A+){3} has different semantics
- *   - Outer range + child not {1,1}: gaps possible
- *     e.g., (A{2}){2,3} yields 4,6 only (not 4,5,6)
- *   - Skippable outer (min 0) + child->min >= 2: (A{2,})* reaches
- *     {0} UNION [child->min, INF), so 1..child->min-1 are unreachable
- *     and A* would wrongly admit them
+ *		Try to flatten (child{p,q}){m,n} into child{p*m, q*n}.
+ *
+ * Below, p,q are the child's {min,max} and m,n the outer {min,max}.
+ *
+ * Flattening is valid only when the repetition counts the nested quantifiers
+ * can produce form exactly the contiguous interval [p*m, q*n].  For an outer
+ * iteration count t (m <= t <= n) the child contributes any count in
+ * [t*p, t*q], and t = 0 contributes {0}.  The union of those intervals is
+ * contiguous, hence flattenable, when:
+ *
+ *   - m == n: a single outer count, so the result is just [m*p, m*q]; or
+ *   - p == 0: every interval starts at 0, so they all overlap; or
+ *   - consecutive intervals touch and the zero case (if any) connects:
+ *       p <= Max(m,1)*(q-p) + 1   (touch; trivially true if q is unbounded)
+ *       and (m >= 1 or p <= 1)    (when m == 0, {0} must reach [p,q])
+ *
+ * Otherwise gaps appear and the pattern is left unflattened: (A{2}){2,3}
+ * yields {4,6} (not 4..6), and (A{2,})* yields {0} UNION [2,INF) (not
+ * [0,INF), so A* would wrongly admit a single A).
  *
  * Returns the child node with multiplied quantifiers if successful,
  * otherwise returns the original pattern unchanged.
@@ -807,6 +811,7 @@ static RPRPatternNode *
 tryMultiplyQuantifiers(RPRPatternNode *pattern)
 {
 	RPRPatternNode *child;
+	bool		safe;
 	int64		new_min_64;
 	int64		new_max_64;
 
@@ -823,69 +828,60 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 		child->reluctant)
 		return pattern;
 
-	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
-	if (child->max == RPR_QUANTITY_INF && pattern->max == RPR_QUANTITY_INF)
+	/*
+	 * Decide whether the achievable counts form one contiguous interval.  The
+	 * child quantifier is {child->min, child->max} and the outer one is
+	 * {pattern->min, pattern->max}; either max may be RPR_QUANTITY_INF.
+	 */
+	if (pattern->min == pattern->max || child->min == 0)
+		safe = true;
+	else
 	{
+		bool		touch;
+		bool		zero_ok;
+
 		/*
-		 * A skippable outer (min 0) over a child with min >= 2 reaches
-		 * repetition counts {0} UNION [child->min, INF): the counts
-		 * 1..child->min-1 are unreachable, and no single quantifier can
-		 * express that gap.  Flattening to A{0,INF} = A* would wrongly admit
-		 * them, e.g. (A{2,})* would match a single A.  Multiplication is safe
-		 * here only when child->min <= 1 (the reachable set is then
-		 * contiguous from 0); otherwise leave the pattern unflattened.
+		 * Consecutive intervals [t*min, t*max] and [(t+1)*min, (t+1)*max]
+		 * touch when (t+1)*min <= t*max + 1, i.e. min <= t*(max-min) + 1.
+		 * This is tightest at the smallest t in play, Max(pattern->min, 1).
+		 * An unbounded child->max makes every interval reach INF, so they
+		 * always touch.
 		 */
-		if (pattern->min == 0 && child->min >= 2)
-			return pattern;
+		if (child->max == RPR_QUANTITY_INF)
+			touch = true;
+		else
+			touch = ((int64) child->min <=
+					 (int64) Max(pattern->min, 1) * (child->max - child->min) + 1);
 
-		new_min_64 = (int64) child->min * pattern->min;
-		if (new_min_64 >= RPR_QUANTITY_INF)
-			return pattern;		/* overflow, skip optimization */
+		/*
+		 * A skippable outer (min 0) also needs {0} adjacent to the child
+		 * range.
+		 */
+		zero_ok = (pattern->min >= 1 || child->min <= 1);
 
-		child->min = (int) new_min_64;
-		child->max = RPR_QUANTITY_INF;
-		return child;
+		safe = touch && zero_ok;
 	}
 
-	/*----------
-	 * Case 2: Outer exact (min == max): (A{2,3}){4} -> A{8,12}.
-	 *         Safe because every iteration produces the same range.
-	 *
-	 * Case 3: Child {1,1}: (A){2,5} -> A{2,5}.
-	 *         Safe because the child contributes exactly one per
-	 *         iteration, so the outer range maps directly.
-	 *
-	 * Unsafe example: (A{2}){2,3} produces counts 4 or 6 only, not the full
-	 * range 4..6, so we cannot flatten when child has a non-trivial range AND
-	 * outer is also a range.
-	 *----------
-	 */
-	if (child->max != RPR_QUANTITY_INF &&
-		(pattern->min == pattern->max ||
-		 (child->min == 1 && child->max == 1)))
-	{
-		new_min_64 = (int64) pattern->min * child->min;
-		if (new_min_64 >= RPR_QUANTITY_INF)
-			return pattern;
-
-		/* Outer unbounded: result is unbounded regardless of child */
-		if (pattern->max == RPR_QUANTITY_INF)
-			new_max_64 = RPR_QUANTITY_INF;
-		else
-		{
-			new_max_64 = (int64) pattern->max * child->max;
+	if (!safe)
+		return pattern;
 
-			if (new_max_64 >= RPR_QUANTITY_INF)
-				return pattern;
-		}
+	/* Flatten the child quantifier, guarding against overflow. */
+	new_min_64 = (int64) pattern->min * child->min;
+	if (new_min_64 >= RPR_QUANTITY_INF)
+		return pattern;			/* overflow, skip optimization */
 
-		child->min = (int) new_min_64;
-		child->max = (int) new_max_64;
-		return child;
+	if (pattern->max == RPR_QUANTITY_INF || child->max == RPR_QUANTITY_INF)
+		new_max_64 = RPR_QUANTITY_INF;
+	else
+	{
+		new_max_64 = (int64) pattern->max * child->max;
+		if (new_max_64 >= RPR_QUANTITY_INF)
+			return pattern;
 	}
 
-	/* Not safe to multiply */
-	return pattern;
+	child->min = (int) new_min_64;
+	child->max = (int) new_max_64;
+	return child;
 }
 
 /*
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index e9303af6384..1410ba75395 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3785,8 +3785,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
--- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
--- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
+-- Quantifier multiply: (A{2,3}){2,3} -> a{4,9}
+-- outer range, child range: counts [4,6] U [6,9] = [4,9] are contiguous, so it folds
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -3795,7 +3795,24 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 -------------------------------------------------------------------------------
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a{2,3}){2,3}
+   Pattern: a{4,9}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{4,5}){2,3} stays as (a{4,5}){2,3}
+-- outer range, child range with a gap: [8,10] U [12,15] misses 11
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{4,5}){2,3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{4,5}){2,3}
    Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
@@ -3850,6 +3867,125 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Quantifier multiply with an unbounded child: an exact outer count (m == n)
+-- always folds regardless of the child's max - (A+){3} -> a{3,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{3,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A{2,}){3} -> a{6,}  (m == n, unbounded child with min 2)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,}){3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{6,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A+){2,4} -> a{2,}  (outer range, unbounded child: every interval reaches INF,
+-- so they always touch)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){2,4}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{2,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A{2,3}){2,4} -> a{4,12}  (outer range x child range, contiguous:
+-- [4,6] U [6,9] U [8,12] = [4,12])
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{4,12}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Skippable outer (min 0) folds only when the zero case connects to the child
+-- range: (A{1,3})? -> a{0,3}  (child min <= 1, so {0} U [1,3] = [0,3] is contiguous)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{1,3})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{0,3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{2,3})? stays as (a{2,3})?
+-- min 0 with child min >= 2: {0} U [2,3] leaves 1 unreachable (intervals touch but
+-- the zero case does not connect)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{2,3})?
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{3,4})? stays as (a{3,4})?
+-- min 0 with child min >= 2: {0} U [3,4] leaves 1,2 unreachable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{3,4})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{3,4})?
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Unwrap GROUP{1,1}: (A) -> a
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -6355,12 +6491,14 @@ ERROR:  too many pattern variables
 DETAIL:  Maximum is 240.
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
--- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
+-- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting (and depth 253) is
+-- preserved after optimization.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
  id | val | count 
@@ -6371,12 +6509,14 @@ WINDOW w AS (
 
 -- Expected: Should succeed
 -- Test: Pattern nesting depth exceeds maximum (depth 254)
--- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+-- Note: 254 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting reaches depth 254 and
+-- exceeds the limit.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 ERROR:  pattern nesting too deep
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 9ba302b11ae..5cddd1a56df 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2178,10 +2178,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){0,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 6 peak, 20 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2217,10 +2217,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){1,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 5 peak, 16 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2256,10 +2256,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){2,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 5 peak, 16 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2295,10 +2295,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=4.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){2,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 8 peak, 26 total, 5 merged
+   NFA States: 6 peak, 13 total, 0 merged
    NFA Contexts: 4 peak, 5 total, 1 pruned
    NFA: 3 matched (len 0/2/1.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=4.00 loops=1)
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 475541d4550..53bf090b903 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2516,13 +2516,20 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2}){3,5}) DEFINE A AS val > 0);
 
--- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
--- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
+-- Quantifier multiply: (A{2,3}){2,3} -> a{4,9}
+-- outer range, child range: counts [4,6] U [6,9] = [4,9] are contiguous, so it folds
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2,3}){2,3}) DEFINE A AS val > 0);
 
+-- Quantifier NO multiply: (A{4,5}){2,3} stays as (a{4,5}){2,3}
+-- outer range, child range with a gap: [8,10] U [12,15] misses 11
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{4,5}){2,3}) DEFINE A AS val > 0);
+
 -- Nested unbounded: (A*)* -> a*
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2541,6 +2548,55 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A+)+) DEFINE A AS val > 0);
 
+-- Quantifier multiply with an unbounded child: an exact outer count (m == n)
+-- always folds regardless of the child's max - (A+){3} -> a{3,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){3}) DEFINE A AS val > 0);
+
+-- (A{2,}){3} -> a{6,}  (m == n, unbounded child with min 2)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,}){3}) DEFINE A AS val > 0);
+
+-- (A+){2,4} -> a{2,}  (outer range, unbounded child: every interval reaches INF,
+-- so they always touch)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){2,4}) DEFINE A AS val > 0);
+
+-- (A{2,3}){2,4} -> a{4,12}  (outer range x child range, contiguous:
+-- [4,6] U [6,9] U [8,12] = [4,12])
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0);
+
+-- Skippable outer (min 0) folds only when the zero case connects to the child
+-- range: (A{1,3})? -> a{0,3}  (child min <= 1, so {0} U [1,3] = [0,3] is contiguous)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{1,3})?) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{2,3})? stays as (a{2,3})?
+-- min 0 with child min >= 2: {0} U [2,3] leaves 1 unreachable (intervals touch but
+-- the zero case does not connect)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3})?) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{3,4})? stays as (a{3,4})?
+-- min 0 with child min >= 2: {0} U [3,4] leaves 1,2 unreachable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{3,4})?) DEFINE A AS val > 0);
+
 -- Unwrap GROUP{1,1}: (A) -> a
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -4017,23 +4073,27 @@ WINDOW w AS (
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
--- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
+-- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting (and depth 253) is
+-- preserved after optimization.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 -- Expected: Should succeed
 
 -- Test: Pattern nesting depth exceeds maximum (depth 254)
--- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+-- Note: 254 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting reaches depth 254 and
+-- exceeds the limit.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 -- Expected: ERROR - pattern nesting too deep
-- 
2.50.1 (Apple Git-155)

From 1e9d66e961bae1e3884be7c844c7faba252c4bed Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Thu, 4 Jun 2026 20:28:51 +0900
Subject: [PATCH 44/68] Tidy up row pattern recognition pattern compilation

Mark the unreachable tails of the three node-type switches
(rprPatternEqual, optimizeRPRPattern, fillRPRPattern) with
pg_unreachable() instead of a "keep compiler quiet" return.
RPRPatternNodeType has only four members and each switch handles
all of them, so the trailing return can never be reached.

In buildRPRPattern(), the absorption-eligibility test guarded
against a ROWS frame redundantly: RPR is ROWS-only because
transformRPR() rejects RANGE and GROUPS up front, so the
FRAMEOPTION_ROWS term was always true.  Drop the hasLimitedFrame
variable, assert the ROWS-only invariant instead, and test
FRAMEOPTION_END_UNBOUNDED_FOLLOWING directly.

No behavior change.

Patch by Jian He, with minor adjustments.
---
 src/backend/optimizer/plan/rpr.c | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 617a3869948..3205559c03a 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -132,7 +132,8 @@ rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b)
 			return rprPatternChildrenEqual(a->children, b->children);
 	}
 
-	return false;				/* keep compiler quiet */
+	pg_unreachable();
+	return false;
 }
 
 /*
@@ -993,7 +994,8 @@ optimizeRPRPattern(RPRPatternNode *pattern)
 			return optimizeGroupPattern(pattern);
 	}
 
-	return pattern;				/* keep compiler quiet */
+	pg_unreachable();
+	return pattern;
 }
 
 /*
@@ -1450,7 +1452,8 @@ fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 			return fillRPRPatternAlt(node, pat, idx, depth);
 	}
 
-	return false;				/* unreachable */
+	pg_unreachable();
+	return false;
 }
 
 /*
@@ -1955,10 +1958,11 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 	int			numElements;
 	RPRDepth	maxDepth;
 	int			idx;
-	bool		hasLimitedFrame;
 
 	/* Caller must check for NULL pattern before calling */
 	Assert(pattern != NULL);
+	/* RPR is ROWS-only: transformRPR() rejects RANGE/GROUPS up front */
+	Assert(frameOptions & FRAMEOPTION_ROWS);
 
 	/* Optimize the pattern tree */
 	optimized = optimizeRPRPattern(copyObject(pattern));
@@ -1997,10 +2001,8 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 	 * absorption semantics - older contexts don't necessarily produce longer
 	 * matches when frame limits apply differently to each context.
 	 */
-	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
-		!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
-
-	if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame &&
+	if (rpSkipTo == ST_PAST_LAST_ROW &&
+		(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING) &&
 		!hasMatchStartDependent)
 	{
 		/* Runtime conditions met - check structural absorbability */
-- 
2.50.1 (Apple Git-155)

From 427916e331cd97c6ee68e3e09b3ee1ccfcb14cb3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 11:29:33 +0900
Subject: [PATCH 45/68] Drop redundant parentheses from row pattern recognition
 ereports

The row pattern recognition patch added a number of ereport() calls
that still wrap their auxiliary functions in the optional outer
parentheses.  The newer style omits them, and most of the patch --
all of parse_rpr.c, for one -- already does.

Convert the remaining calls to the parenthesis-free style so the
feature is internally consistent: the DEFINE-clause checks in
ParseFuncOrColumn() and transformColumnRef(), the navigation offset
checks in execExprInterp.c, and the navigation placeholder window
functions in windowfuncs.c.  Pre-existing core ereport() calls are
left untouched.

No behavior change.
---
 src/backend/executor/execExprInterp.c | 16 +++++++-------
 src/backend/parser/parse_expr.c       | 16 +++++++-------
 src/backend/parser/parse_func.c       |  8 +++----
 src/backend/utils/adt/windowfuncs.c   | 32 +++++++++++++--------------
 4 files changed, 36 insertions(+), 36 deletions(-)

diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 324b9a962a8..805c8583fb2 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -6026,15 +6026,15 @@ rpr_nav_get_compound_offset(ExprEvalStep *op)
 
 	if (op->d.rpr_nav.offset_isnull[1])
 		ereport(ERROR,
-				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-				 errmsg("row pattern navigation offset must not be null")));
+				errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				errmsg("row pattern navigation offset must not be null"));
 
 	val = DatumGetInt64(op->d.rpr_nav.offset_value[1]);
 
 	if (val < 0)
 		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("row pattern navigation offset must not be negative")));
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("row pattern navigation offset must not be negative"));
 
 	return val;
 }
@@ -6071,15 +6071,15 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	{
 		if (*op->d.rpr_nav.offset_isnull)
 			ereport(ERROR,
-					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-					 errmsg("row pattern navigation offset must not be null")));
+					errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					errmsg("row pattern navigation offset must not be null"));

 		if (offset < 0)
 			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("row pattern navigation offset must not be negative")));
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("row pattern navigation offset must not be negative"));
 	}
 	else
 	{
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 78abdc88f86..2344aaef9ae 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -666,17 +666,17 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
 		if (is_pattern_var)
 			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
-							NameListToString(cref->fields)),
-					 parser_errposition(pstate, cref->location)));
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
 		else if (refnameNamespaceItem(pstate, NULL, qualifier,
 									  cref->location, NULL) != NULL)
 			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
-							NameListToString(cref->fields)),
-					 parser_errposition(pstate, cref->location)));
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
 		/* else: unknown qualifier -- fall through to normal resolution */
 	}
 
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 49646b728c6..8cae9ba52b6 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -789,10 +789,10 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 
 		/* 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)));
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("cannot use %s outside a DEFINE clause",
+					   NameListToString(funcname)),
+				parser_errposition(pstate, location));
 	}
 
 	/* build the appropriate output structure */
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 46e7a03a666..3869f6c8994 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -739,8 +739,8 @@ Datum
 window_prev(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use PREV() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use PREV() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -753,8 +753,8 @@ Datum
 window_next(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use NEXT() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use NEXT() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -767,8 +767,8 @@ Datum
 window_prev_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use PREV() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use PREV() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -781,8 +781,8 @@ Datum
 window_next_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use NEXT() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use NEXT() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -795,8 +795,8 @@ Datum
 window_first(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use FIRST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use FIRST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -809,8 +809,8 @@ Datum
 window_last(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use LAST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use LAST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -823,8 +823,8 @@ Datum
 window_first_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use FIRST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use FIRST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -837,7 +837,7 @@ Datum
 window_last_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use LAST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use LAST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
-- 
2.50.1 (Apple Git-155)

From 178fe9979b1a5c18e06af67a696e12f0ee9da107 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 12:32:38 +0900
Subject: [PATCH 46/68] Tidy up forward declarations and helper placement for
 row pattern recognition

Bring the row pattern recognition code in line with the surrounding
conventions for static helpers:

- each static helper has a forward declaration;
- static helpers are defined above the "API exposed" banner, public
  entry points below;
- the forward-declaration block uses a single banner, not per-group
  labels;
- forward declarations follow definition order.

Pure code movement and comments, no behavior change.

Per review comments from Tatsuo Ishii and Jian He.
---
 src/backend/executor/execRPR.c       | 110 +++----
 src/backend/executor/nodeWindowAgg.c | 454 +++++++++++++--------------
 src/backend/optimizer/plan/rpr.c     |  12 +-
 src/backend/parser/parse_rpr.c       |   2 +-
 4 files changed, 289 insertions(+), 289 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 580b25f398d..56399c0c7fd 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -83,8 +83,6 @@ static bool nfa_eval_var_match(WindowAggState *winstate,
 							   RPRPatternElement *elem, bool *varMatched);
 static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
 					  bool *varMatched);
-static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
-							  RPRNFAState *state, int64 currentPos);
 static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 							  RPRNFAState *state, RPRPatternElement *nextElem,
 							  int64 currentPos);
@@ -100,9 +98,15 @@ static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 							RPRNFAState *state, RPRPatternElement *elem,
 							int64 currentPos);
+static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, int64 currentPos);
 static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
 						int64 currentPos);
 
+static void nfa_reevaluate_dependent_vars(WindowAggState *winstate,
+										  RPRNFAContext *ctx,
+										  int64 currentPos);
+
 /*
  * NFA-based pattern matching implementation
  *
@@ -1552,6 +1556,57 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 	}
 }
 
+/*
+			/* Per-tuple context; scratch freed by the per-row reset */
+			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
 
 /***********************************************************************
  * API exposed to nodeWindowAgg.c
@@ -1696,57 +1751,6 @@ ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
 	}
 }
 
-/*
- * nfa_reevaluate_dependent_vars
- *		Re-evaluate match_start-dependent DEFINE variables for a specific
- *		context whose matchStartRow differs from the shared evaluation's
- *		nav_match_start.
- *
- * Only variables in defineMatchStartDependent are re-evaluated.  The
- * current row's slot (ecxt_outertuple) must already be set up by
- * nfa_evaluate_row().
- */
-static void
-nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
-							  int64 currentPos)
-{
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
-	int64		saved_match_start = winstate->nav_match_start;
-	int64		saved_pos = winstate->currentpos;
-	int			varIdx = 0;
-	ListCell   *lc;
-
-	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
-	winstate->nav_match_start = ctx->matchStartRow;
-	winstate->currentpos = currentPos;
-
-	/* Invalidate nav_slot cache since match_start changed */
-	winstate->nav_slot_pos = -1;
-
-	foreach(lc, winstate->defineClauseList)
-	{
-		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
-		{
-			ExprState  *exprState = (ExprState *) lfirst(lc);
-			Datum		result;
-			bool		isnull;
-
-			/* Per-tuple context; scratch freed by the per-row reset */
-			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
-			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
-		}
-
-		varIdx++;
-		if (varIdx >= list_length(winstate->defineVariableList))
-			break;
-	}
-
-	/* Restore original match_start, currentpos, and invalidate cache */
-	winstate->nav_match_start = saved_match_start;
-	winstate->currentpos = saved_pos;
-	winstate->nav_slot_pos = -1;
-}
-
 /*
  * ExecRPRProcessRow
  *
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 408bbc120b7..86b39bf7f61 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4627,6 +4627,233 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	return true;				/* Row exists */
 }
 
+/*
+ * WinGetSlotInFrame
 
 /***********************************************************************
  * API exposed to window functions
@@ -5019,233 +5246,6 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 	return (Datum) 0;
 }
 
-/*
- * WinGetSlotInFrame
- * Returns 0 if we successfully got the slot, or nonzero if out of frame.
- * (isout is also set in the latter case.)
-	int64		num_reduced_frame;
-				if (-relpos >= num_reduced_frame)
-		 * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the
-		 * mark position unconditionally to frameheadpos. In this case the
-		 * frame always starts at CURRENT_ROW and never goes back, thus
-		 * setting the mark at the position is safe.
-		if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
  * WinGetFuncArgCurrent
  *		Evaluate a window function's argument expression on the current row.
index 3205559c03a..43114088c3f 100644
@@ -44,14 +44,12 @@
 
-/* Forward declarations - pattern comparison */
+/* Forward declarations */
 static bool rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b);
 static bool rprPatternChildrenEqual(List *a, List *b);
 
-/* Forward declarations - pattern optimization (shared) */
 static RPRPatternNode *tryUnwrapSingleChild(RPRPatternNode *pattern);
 
-/* Forward declarations - SEQ optimization */
 static List *flattenSeqChildren(List *children);
 static List *mergeConsecutiveVars(List *children);
 static List *mergeConsecutiveGroups(List *children);
@@ -59,20 +57,16 @@ static List *mergeConsecutiveAlts(List *children);
 static List *mergeGroupPrefixSuffix(List *children);
 static RPRPatternNode *optimizeSeqPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - ALT optimization */
 static List *flattenAltChildren(List *children);
 static List *removeDuplicateAlternatives(List *children);
 static RPRPatternNode *optimizeAltPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - GROUP optimization */
 static RPRPatternNode *tryMultiplyQuantifiers(RPRPatternNode *pattern);
 static RPRPatternNode *tryUnwrapGroup(RPRPatternNode *pattern);
 static RPRPatternNode *optimizeGroupPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - optimization dispatcher */
 static RPRPatternNode *optimizeRPRPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - pattern compilation */
 static int	collectDefineVariables(List *defineVariableList, char **varNames);
 static void scanRPRPatternRecursive(RPRPatternNode *node, char **varNames,
 									int *numVars, int *numElements,
@@ -92,7 +86,6 @@ static bool fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
 						   int *idx, RPRDepth depth);
 static void finalizeRPRPattern(RPRPattern *result);
 
-/* Forward declarations - context absorption */
 static bool isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx,
 								  RPRDepth scopeDepth);
 static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
@@ -101,6 +94,9 @@ 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.
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3872035110a..c9469b56b7b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -61,8 +61,8 @@ static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 									   List *rpDefs, List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
 								   WindowDef *windef, List **targetlist);
-static bool define_walker(Node *node, void *context);
 static bool nav_volatile_func_checker(Oid funcid, void *context);
+static bool define_walker(Node *node, void *context);
 
 /*
  * transformRPR
-- 
2.50.1 (Apple Git-155)

From 04f18c586669af22eef26eab7f7cb383f0ccf6d4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 12:45:09 +0900
Subject: [PATCH 47/68] Update the varId documentation for row pattern
 recognition

Reserving the whole high varId nibble (0xF0-0xFF) for control elements
left 240 pattern variables (0x00-0xEF), but the documentation still
described the old 0-250 / 252-255 layout.

Update README.rpr's element table and control-code list to the hex
nibble boundaries, note the 0xF0-0xFB spare range, and correct the
user-facing limit in select.sgml from 251 to 240 unique pattern
variables.

Documentation only.
---
 doc/src/sgml/ref/select.sgml    |  2 +-
 src/backend/executor/README.rpr | 15 ++++++++++-----
 2 files changed, 11 insertions(+), 6 deletions(-)

index e4708331439..be5ed814853 100644
 
    <para>
     Note that the maximum number of unique pattern variables
-    used in the <literal>PATTERN</literal> clause is 251.
+    used in the <literal>PATTERN</literal> clause is 240.
     If this limit is exceeded, an error will be raised.
     Additionally, the maximum nesting depth of pattern groups
     (parentheses) is 253 levels.
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 3a215f2566b..55f899f7fef 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -246,7 +246,7 @@ RPRPatternElement struct (16 bytes):
 
   Field      Size     Description
   ---------------------------------------------------------
-  varId      1B      Variable ID (0-250) or control code (252-255)
+  varId      1B      Variable ID (0-0xEF) or control code (0xFC-0xFF)
   depth      1B      Group nesting depth
   flags      1B      Bit flags (see below)
   reserved   1B      Padding
@@ -255,12 +255,17 @@ RPRPatternElement struct (16 bytes):
   next       2B      Next element index (sequential flow)
   jump       2B      Branch target index (for ALT/GROUP)
 
+Pattern variables occupy varId 0 to RPR_VARID_MAX (0xEF) inclusive,
+giving 240 distinct variables.  Any varId with the high nibble set
+(0xF0-0xFF) is reserved for control elements; 0xF0-0xFB are currently
+spare.
+
 Control codes:
 
-  RPR_VARID_BEGIN (252)  Group start marker
-  RPR_VARID_END   (253)  Group end marker
-  RPR_VARID_ALT   (254)  Alternation start marker
-  RPR_VARID_FIN   (255)  Pattern completion marker
+  RPR_VARID_BEGIN (0xFC)  Group start marker
+  RPR_VARID_END   (0xFD)  Group end marker
+  RPR_VARID_ALT   (0xFE)  Alternation start marker
+  RPR_VARID_FIN   (0xFF)  Pattern completion marker
 
 Element flags (1 byte, bitmask):
 
-- 
2.50.1 (Apple Git-155)

From c2539388101eaf8c9cc5fa198a3b90d7e9346889 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 13:07:22 +0900
Subject: [PATCH 48/68] Reformat the design-decisions chapter in the row
 pattern recognition README

Chapter XII recorded each key design decision as a "Choice:" line plus a
bulleted "Rationale:" list.  Rewrite XII-1 through XII-4 as prose instead
-- a short paragraph stating the decision, followed by a paragraph for
its rationale -- to match the rest of the file.  No rationale is dropped.

XII-4 also picks up the detail, noted by Jian He, that the state and
context structures live in a partition-lifespan memory context that is
freed in release_partition.

Documentation only.
---
 src/backend/executor/README.rpr | 54 ++++++++++++++++-----------------
 1 file changed, 26 insertions(+), 28 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 55f899f7fef..df574a0a6f4 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -1320,47 +1320,45 @@ Chapter XII  Summary of Key Design Decisions
 
 XII-1. Flat Array vs Tree-Based NFA
 
-  Choice: Flat array (RPRPatternElement[])
+  The compiled pattern is stored as a flat array of fixed-size 16-byte
+  RPRPatternElement structs rather than as a tree.
 
-  Rationale:
-  - Cache-friendly: 16-byte fixed size, contiguous memory
-  - Index-based references: 2-byte indices instead of pointers
-  - Easy to serialize: can use memcpy when passing to plan nodes
+  The array is contiguous and cache-friendly, elements reference each
+  other by 2-byte index instead of by pointer, and the whole structure
+  can be serialized with memcpy when passed to plan nodes.
 
 XII-2. Forward-only Execution vs Backtracking
 
-  Choice: Forward-only (state set tracking)
+  The NFA is simulated forward-only, tracking a set of live states,
+  rather than by backtracking.
 
-  Rationale:
-  - Backtracking takes exponential time in the worst case
-  - NFA simulation guarantees polynomial time
-  - DFS order naturally guarantees preferment.
-    Greedy/reluctant per quantifier requires only reversing the DFS order
-  - Window functions receive sorted rows sequentially.
-    Forward-only fits directly into this pipeline,
-    whereas backtracking requires re-fetching previous rows
-  - DEFINE conditions are SQL expressions (PREV, RUNNING aggregates, etc.)
-    with high re-evaluation cost. Forward-only requires only one evaluation
-    per row
+  Backtracking would take exponential time in the worst case, whereas
+  forward-only NFA simulation is polynomial.  Forward-only also fits the
+  window pipeline, which delivers sorted rows sequentially: it needs no
+  re-fetching of earlier rows, and each row's DEFINE conditions (SQL
+  expressions such as PREV or running aggregates, with high re-evaluation
+  cost) are evaluated only once.  DFS order yields preferment naturally,
+  with greedy or reluctant behavior per quantifier obtained by reversing
+  that order.
 
 XII-3. Per-Context Management
 
-  Choice: Independent context per start row
+  A separate match context is maintained for each start row.
 
-  Rationale:
-  - Supports overlapping matches under SKIP TO NEXT ROW
-  - Determines the frame for each row independently
-  - Absorption optimization can eliminate redundant contexts in O(n)
+  This supports overlapping matches under SKIP TO NEXT ROW, determines
+  each row's frame independently, and lets the absorption optimization
+  eliminate redundant contexts in O(n).

-  Choice: Custom free list
+  NFA states are managed through a custom free list, and both RPRNFAState
+  and RPRNFAContext are allocated in a partition-lifespan memory context
+  that is freed in release_partition.
 
-  Rationale:
-  - NFA states are created and destroyed in large numbers per row
-  - Avoids palloc/pfree overhead
-  - State size is variable (counts[] array), but within a single query
-    maxDepth is fixed, so all states have the same size
+  NFA states are created and destroyed in large numbers per row, so the
+  free list avoids palloc/pfree overhead.  Their size varies (the
+  counts[] array), but maxDepth is fixed within a single query, so all
+  states have the same size.
 
 XII-5. Execution Optimization Summary
 
-- 
2.50.1 (Apple Git-155)

From acc0ebfda053b8206b6c336a68229c7d724aea8d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 13:14:01 +0900
Subject: [PATCH 49/68] Point to the absorption-analysis docs from the RPR flag
 definitions

The RPR_ELEM_ABSORBABLE and RPR_ELEM_ABSORBABLE_BRANCH element flags are
described in README.rpr and computed in rpr.c, but the rpr.h definitions
gave no hint of where to look.  Add a short pointer comment to
README.rpr IV-5 ("Absorbability Analysis"), its Appendix C examples, and
computeAbsorbability(), without restating the explanation.

Documentation only.

Per an off-list review comment from Jian He.
---
 src/include/optimizer/rpr.h | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 2fa483320ce..73c827de2b1 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -44,6 +44,12 @@
 											 * quantifier */
 #define RPR_ELEM_EMPTY_LOOP			0x02	/* END: group body can produce
 											 * empty match */
+/*
+ * The two absorption flags below are explained in README.rpr IV-5
+ * ("Absorbability Analysis"), with worked examples in Appendix C; the
+ * analysis that sets them is computeAbsorbability() in
+ * optimizer/plan/rpr.c.
+ */
 #define RPR_ELEM_ABSORBABLE_BRANCH	0x04	/* element in absorbable region */
 #define RPR_ELEM_ABSORBABLE			0x08	/* absorption judgment point */
 
-- 
2.50.1 (Apple Git-155)

From 8e806e30fd8a7dcbeb18e13faccd50cdeff0f911 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 13:31:20 +0900
Subject: [PATCH 50/68] Fix signed-integer overflow in row pattern recognition
 frame-end clamp

ExecRPRProcessRow() computed "frameOffset + 1" before passing it to
pg_add_s64_overflow(), so a FOLLOWING offset near PG_INT64_MAX made
that add overflow on its own -- undefined behavior that crashed
cassert builds and gave wrong results otherwise.

Add the offset and the +1 in two separately checked steps, clamping to
PG_INT64_MAX on overflow, as core WindowAgg already does.  The clamp is
safe because the partition row count is an int64, so no row ever sits
at that position; a huge FOLLOWING offset thus behaves like UNBOUNDED
FOLLOWING.  Also add a regression test.
---
 src/backend/executor/execRPR.c         | 31 +++++++++++++++++++++-----
 src/test/regress/expected/rpr_base.out | 25 +++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 16 +++++++++++++
 3 files changed, 66 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 56399c0c7fd..69e3603adef 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1784,9 +1784,15 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		{
 			int64		ctxFrameEnd;
 
-			/* Clamp to INT64_MAX on overflow */
-			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset + 1,
-									&ctxFrameEnd))
+			/*
+			 * Clamp to INT64_MAX on overflow.  frameOffset can be as large as
+			 * PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the offset
+			 * and the trailing +1 in two separately checked steps to avoid
+			 * signed-integer overflow in the "frameOffset + 1" subexpression.
+			 */
+			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
+									&ctxFrameEnd) ||
+				pg_add_s64_overflow(ctxFrameEnd, 1, &ctxFrameEnd))
 				ctxFrameEnd = PG_INT64_MAX;
 
 			/*
@@ -1844,10 +1850,23 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		 * mismatch (nfa_match with NULL), which removes all states (all
 		 * states are at VAR positions after advance). So any surviving
 		 * context here must be within its frame boundary.
+		 *
+		 * Compute the (clamped) frame end the same way as Phase 1, using two
+		 * separately checked adds so that "frameOffset + 1" cannot overflow
+		 * when frameOffset is near PG_INT64_MAX.
 		 */
-		Assert(!hasLimitedFrame ||
-			   ctx->matchStartRow > PG_INT64_MAX - frameOffset - 1 ||
-			   currentPos < ctx->matchStartRow + frameOffset + 1);
+#ifdef USE_ASSERT_CHECKING
+		if (hasLimitedFrame)
+		{
+			int64		ctxFrameEnd;
+
+			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
+									&ctxFrameEnd) ||
+				pg_add_s64_overflow(ctxFrameEnd, 1, &ctxFrameEnd))
+				ctxFrameEnd = PG_INT64_MAX;
+			Assert(currentPos < ctxFrameEnd);
+		}
+#endif
 
 		nfa_advance(winstate, ctx, currentPos);
 	}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 1410ba75395..4fe7360114f 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -643,6 +643,31 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
+-- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the
+-- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes
+-- it behave like UNBOUNDED FOLLOWING).  Guards against signed-integer overflow
+-- in the "frameOffset + 1" subexpression (undefined behavior).  The cnt values
+-- must match the UNBOUNDED FOLLOWING result for the same data.
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 9223372036854775806 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   6
+  2 |  10 |   5
+  3 |  10 |   4
+  4 |  20 |   3
+  5 |  20 |   2
+  6 |  30 |   1
+(6 rows)
+
 -- RANGE frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 53bf090b903..c6fcfa3e9ff 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -515,6 +515,22 @@ WINDOW w AS (
 )
 ORDER BY id;
 
+-- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the
+-- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes
+-- it behave like UNBOUNDED FOLLOWING).  Guards against signed-integer overflow
+-- in the "frameOffset + 1" subexpression (undefined behavior).  The cnt values
+-- must match the UNBOUNDED FOLLOWING result for the same data.
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 9223372036854775806 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
 -- RANGE frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
-- 
2.50.1 (Apple Git-155)

From 8b325c572b63859781879cf645e7eb87521e1118 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 16:52:38 +0900
Subject: [PATCH 52/68] Handle row pattern navigation nodes in exprTypmod and
 isSimpleNode

RPRNavExpr, the PREV/NEXT/FIRST/LAST navigation node used in a row
pattern DEFINE clause, was missing from two type-helper switches and
fell through to their defaults.

In exprTypmod() the default returns -1, so the argument's typmod was
dropped.  A navigation expression evaluates its argument on another row
without changing its type, so its result has the same type and typmod
as the argument; return the argument's typmod, matching how exprType()
already reports its result type.

In isSimpleNode() the default returns false, which made pretty-printing
wrap a navigation operand in redundant parentheses, for example
val > (PREV(val)).  A navigation expression deparses as a function-like
FUNC(..) call that already delimits itself, so it is simple and needs
no extra parentheses; add it to the function-like group.

This affects display and node completeness only; execution and
re-parsing were already correct, as the extra parentheses were still
valid SQL.  Add a pretty-mode regression using the two-argument
pg_get_viewdef in rpr_base, a path the existing single-argument
navigation tests did not exercise.
---
 src/backend/nodes/nodeFuncs.c          |  3 +++
 src/backend/utils/adt/ruleutils.c      |  1 +
 src/test/regress/expected/rpr_base.out | 27 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 13 +++++++++++++
 4 files changed, 44 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 101c03b6ae8..aecf4e4da0a 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -392,6 +392,9 @@ exprTypmod(const Node *expr)
 			return ((const ArrayCoerceExpr *) expr)->resulttypmod;
 		case T_CollateExpr:
 			return exprTypmod((Node *) ((const CollateExpr *) expr)->arg);
+		case T_RPRNavExpr:
+			/* result has the same type/typmod as the argument expression */
+			return exprTypmod((Node *) ((const RPRNavExpr *) expr)->arg);
 		case T_CaseExpr:
 			{
 				/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 6237080fb36..2b8439e452e 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9616,6 +9616,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
 		case T_FuncExpr:
 		case T_JsonConstructorExpr:
 		case T_JsonExpr:
+		case T_RPRNavExpr:
 			/* function-like: name(..) or name[..] */
 			return true;
 
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 4fe7360114f..fa6d101f874 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2488,6 +2488,33 @@ SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
    b AS (NEXT(FIRST(val), (3)::bigint) > 0) );
 (1 row)
 
+-- Pretty deparse: navigation calls are function-like and take no extra parens
+CREATE VIEW rpr_nav_pretty_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE,
+                    B AS val > PREV(val) AND PREV(val) IS NOT NULL
+                         AND NEXT(val) > FIRST(val)
+                         AND PREV(FIRST(val)) > 0);
+SELECT pg_get_viewdef('rpr_nav_pretty_v'::regclass, true);
+                                             pg_get_viewdef                                              
+---------------------------------------------------------------------------------------------------------
+  SELECT id,                                                                                            +
+     val,                                                                                               +
+     count(*) OVER w AS count                                                                           +
+    FROM rpr_serial                                                                                     +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING                            +
+   AFTER MATCH SKIP PAST LAST ROW                                                                       +
+   INITIAL                                                                                              +
+   PATTERN (a b+)                                                                                       +
+   DEFINE                                                                                               +
+   a AS true,                                                                                           +
+   b AS val > PREV(val) AND PREV(val) IS NOT NULL AND NEXT(val) > FIRST(val) AND PREV(FIRST(val)) > 0 );
+(1 row)
+
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index c6fcfa3e9ff..e0af7199629 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1693,6 +1693,19 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0);
 SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
 
+-- Pretty deparse: navigation calls are function-like and take no extra parens
+CREATE VIEW rpr_nav_pretty_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE,
+                    B AS val > PREV(val) AND PREV(val) IS NOT NULL
+                         AND NEXT(val) > FIRST(val)
+                         AND PREV(FIRST(val)) > 0);
+SELECT pg_get_viewdef('rpr_nav_pretty_v'::regclass, true);
+
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
-- 
2.50.1 (Apple Git-155)

From c33935aecc31bc25961122e69cd96fd65710ea2a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 6 Jun 2026 06:58:01 +0900
Subject: [PATCH 54/68] Test deparse of an inline row pattern window

A window spec written inline (OVER (...) with no WINDOW alias) deparses
through a different get_window_function_node branch than a named
window, and no view exercised it.  Add an inline-OVER view to rpr_base
and capture its viewdef, covering that path.  No code change.
---
 src/test/regress/expected/rpr_base.out | 23 +++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 10 ++++++++++
 2 files changed, 33 insertions(+)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index f086ecc9a5d..c50c7a5f6a8 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2564,6 +2564,29 @@ SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
    "Up" AS (val > PREV(val)) );
 (1 row)
 
+-- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...)
+CREATE VIEW rpr_serial_inline_over AS
+SELECT id, val,
+       count(*) OVER (ORDER BY id
+                      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                      PATTERN (A B+)
+                      DEFINE A AS val > 10, B AS val > PREV(val)) AS cnt
+FROM rpr_serial;
+SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass);
+                                  pg_get_viewdef                                  
+----------------------------------------------------------------------------------
+  SELECT id,                                                                     +
+     val,                                                                        +
+     count(*) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                                +
+   INITIAL                                                                       +
+   PATTERN (a b+)                                                                +
+   DEFINE                                                                        +
+   a AS (val > 10),                                                              +
+   b AS (val > PREV(val)) ) AS cnt                                               +
+    FROM rpr_serial;
+(1 row)
+
 -- Materialized view (if supported)
 CREATE TABLE rpr_mview (id INT, val INT);
 INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e0af7199629..7dfb72f6bfd 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1727,6 +1727,16 @@ WINDOW w AS (ORDER BY id
              DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
 SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
 
+-- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...)
+CREATE VIEW rpr_serial_inline_over AS
+SELECT id, val,
+       count(*) OVER (ORDER BY id
+                      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                      PATTERN (A B+)
+                      DEFINE A AS val > 10, B AS val > PREV(val)) AS cnt
+FROM rpr_serial;
+SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass);
+
 -- Materialized view (if supported)

From a17415a855ba345bbd189869fb0b50334752c404 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sat, 6 Jun 2026 07:18:12 +0900
Subject: [PATCH 55/68] Fix a mislabeled INITIAL test in row pattern
 recognition

The "INITIAL Mode" block in rpr_nfa claimed INITIAL was unimplemented
and produced a syntax error.  INITIAL is the default match mode and is
fully supported; the error was only a clause-order violation -- INITIAL
was written before AFTER MATCH SKIP, which the grammar forbids.

Reorder INITIAL after AFTER MATCH SKIP and correct the comments.  The
queries now run and confirm that explicit INITIAL matches the default
mode.  Test-only change.
---
 src/test/regress/expected/rpr_nfa.out | 31 ++++++++++++++++-----------
 src/test/regress/sql/rpr_nfa.sql      | 10 ++++-----
 2 files changed, 23 insertions(+), 18 deletions(-)

diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 829e8251aed..02a5e517b0e 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -3524,10 +3524,8 @@ ORDER BY mode, id;
 
 -- ============================================================
 -- INITIAL Mode (Runtime)
--- Placeholder: INITIAL is not yet implemented (syntax error).
--- Kept here so tests convert to runtime tests when implemented.
 -- ============================================================
--- INITIAL mode (not yet supported - produces syntax error)
+-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default
 WITH test_initial_mode AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),  -- Unmatched
@@ -3544,15 +3542,21 @@ FROM test_initial_mode
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    INITIAL
     AFTER MATCH SKIP TO NEXT ROW
+    INITIAL
     PATTERN (A+)
     DEFINE
         A AS 'A' = ANY(flags)
 );
-ERROR:  syntax error at or near "AFTER"
-LINE 18:     AFTER MATCH SKIP TO NEXT ROW
-             ^
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {_}   |             |          
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {_}   |             |          
+  5 | {A}   |           5 |         5
+(5 rows)
+
 -- Default mode (include all rows)
 WITH test_default_mode AS (
     SELECT * FROM (VALUES
@@ -3584,7 +3588,7 @@ WINDOW w AS (
   5 | {A}   |           5 |         5
 (5 rows)
 
--- Mode difference verification (INITIAL not yet supported - produces syntax error)
+-- Mode equivalence verification: explicit INITIAL equals the default mode
 WITH test_mode_diff AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),
@@ -3598,8 +3602,8 @@ FROM (
     WINDOW w AS (
         ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        INITIAL
         AFTER MATCH SKIP TO NEXT ROW
+        INITIAL
         PATTERN (A)
         DEFINE A AS 'A' = ANY(flags)
     )
@@ -3617,9 +3621,12 @@ FROM (
     )
 ) sub
 ORDER BY mode;
-ERROR:  syntax error at or near "AFTER"
-LINE 15:         AFTER MATCH SKIP TO NEXT ROW
-                 ^
+  mode   | row_count 
+---------+-----------
+ DEFAULT |         3
+ INITIAL |         3
+(2 rows)
+
 -- ============================================================
 -- Frame Boundary Variations
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 3bbec496279..213385f143b 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2530,11 +2530,9 @@ ORDER BY mode, id;
 
 -- ============================================================
 -- INITIAL Mode (Runtime)
--- Placeholder: INITIAL is not yet implemented (syntax error).
--- Kept here so tests convert to runtime tests when implemented.
 -- ============================================================
 
--- INITIAL mode (not yet supported - produces syntax error)
+-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default
 WITH test_initial_mode AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),  -- Unmatched
@@ -2551,8 +2549,8 @@ FROM test_initial_mode
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    INITIAL
     AFTER MATCH SKIP TO NEXT ROW
+    INITIAL
     PATTERN (A+)
     DEFINE
         A AS 'A' = ANY(flags)
@@ -2581,7 +2579,7 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- Mode difference verification (INITIAL not yet supported - produces syntax error)
+-- Mode equivalence verification: explicit INITIAL equals the default mode
 WITH test_mode_diff AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),
@@ -2595,8 +2593,8 @@ FROM (
     WINDOW w AS (
         ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        INITIAL
         AFTER MATCH SKIP TO NEXT ROW
+        INITIAL
         PATTERN (A)
         DEFINE A AS 'A' = ANY(flags)
     )
-- 
2.50.1 (Apple Git-155)

From 9a191e6f099df41bc976728d83ba5f8b3190a5a9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 22:30:14 +0900
Subject: [PATCH 53/68] Restore the error cursor for too many row pattern
 variables

The "too many pattern variables" error came out without a LINE/caret
cursor: the check locates the cursor via exprLocation(), which had no
T_RPRPatternNode case and fell through to the default returning -1.

Add the case alongside T_RPRNavExpr to return the node's token
location.  Caller and message text are unchanged; only the cursor is
restored.  Update the existing 241-variable rpr_base regression.
---
 src/backend/nodes/nodeFuncs.c          | 3 +++
 src/test/regress/expected/rpr_base.out | 2 ++
 2 files changed, 5 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index aecf4e4da0a..6ab227b2b60 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1452,6 +1452,9 @@ exprLocation(const Node *expr)
 		case T_RPRNavExpr:
 			loc = ((const RPRNavExpr *) expr)->location;
 			break;
+		case T_RPRPatternNode:
+			loc = ((const RPRPatternNode *) expr)->location;
+			break;
 		case T_SubscriptingRef:
 			/* just use container argument's location */
 			loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index fa6d101f874..f086ecc9a5d 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -6540,6 +6540,8 @@ WINDOW w AS (
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 ERROR:  too many pattern variables
+LINE 5: ...V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
+                                                                  ^
 DETAIL:  Maximum is 240.
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
-- 
2.50.1 (Apple Git-155)

From 2a9b6b156ea929b24148fe617e802c6821716b83 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 5 Jun 2026 16:37:34 +0900
Subject: [PATCH 51/68] Rework row pattern EXPLAIN deparser to fix grouped
 alternation branches

The EXPLAIN "Pattern:" line reconstructs a pattern string from the
compiled RPRPattern bytecode.  When an alternation branch was a
quantified group, the previous deparser dropped the branch separator
or the enclosing parenthesis, so the displayed pattern no longer
matched the one that was compiled.  For example PATTERN (C | (A B)+ | D)
was shown as (c | (a b)+ d), collapsing a three-way alternation into a
different two-way pattern, and a leading group branch lost the opening
parenthesis of the alternation.

The cause was that branch separators and scope ends were rebuilt from
the jump field, which the compiler overloads as a group's skip target,
and from absolute next values, which it rewrites for branch tails and
nested alternations.

Rewrite the deparser as a recursive descent in which each construct is
deparsed within an inherited [start, limit) window.  Scope ends come
from depth, branch boundaries from a branch-start jump confirmed by the
relative test elem[j-1].next != j, and parentheses from structure plus
a one-step lookahead for a group that wraps a lone alternation.  Each
construct's extent is found by a short forward scan, so the walk is not
a single linear pass; as this runs only while EXPLAIN formats its
output and patterns are small, it favors clarity over a maximally
efficient deparse.

This is display-only; query execution and pg_get_viewdef were already
correct and are unchanged.  Add regression coverage in rpr_explain for
grouped alternation branches and for deeply nested alternations, which
previously had none.
---
 src/backend/commands/explain.c            | 343 ++++----
 src/test/regress/expected/rpr_explain.out | 950 ++++++++++++++++++++++
 src/test/regress/sql/rpr_explain.sql      | 523 ++++++++++++
 3 files changed, 1623 insertions(+), 193 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1a754bcdac5..7ba0b6df849 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -122,18 +122,13 @@ static void show_window_keys(StringInfo buf, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem);
 static char *deparse_rpr_pattern(RPRPattern *pattern);
-static void deparse_rpr_elements(RPRPattern *pattern, int *idx,
-								 StringInfoData *buf, RPRDepth groupDepth,
-								 RPRDepth *prevDepth, bool *needSpace);
-static void deparse_rpr_group(RPRPattern *pattern, int *idx,
-							  StringInfoData *buf, RPRDepth *prevDepth,
-							  bool *needSpace);
-static void deparse_rpr_alt(RPRPattern *pattern, int *idx,
-							StringInfoData *buf, RPRDepth *prevDepth,
-							bool *needSpace, List **altSeps);
-static void deparse_rpr_var(RPRPattern *pattern, int *idx,
-							StringInfoData *buf, RPRDepth *prevDepth,
-							bool *needSpace, List **altSeps);
+static int	deparse_rpr_seq(RPRPattern *pattern, int start, int limit,
+							StringInfo buf);
+static int	deparse_rpr_node(RPRPattern *pattern, int i, 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 void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
 							  ExplainState *es);
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -2952,244 +2947,206 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
 }
 
 /*
- * Deparse a compiled RPRPattern (bytecode) back to pattern string.
+ * Deparse a compiled RPRPattern (bytecode) back to a pattern string.
  *
- * Walks the flat bytecode array using mutual recursion: deparse_rpr_elements
- * processes sequential elements, and deparse_rpr_group handles BEGIN...END
- * groups by recursing back into deparse_rpr_elements for the group content.
+ * The flat RPRPatternElement[] array is walked by recursive descent.  Each
+ * construct is deparsed within an inherited [start, limit) window: the parent
+ * passes the boundary down, so each construct's extent is fixed by its caller.
+ * Three signals drive the walk:
+ *
+ *   - scope ends (where an ALT or GROUP body finishes) come from depth, via
+ *     rpr_alt_scope_end() and rpr_match_end().
+ *   - branch boundaries (where a "|" goes) come from a branch-start jump,
+ *     confirmed by the relative test elem[j-1].next != j, via
+ *     rpr_next_branch().
+ *   - parentheses come from structure (a BEGIN group, an ALT) plus a one-step
+ *     lookahead for a group that wraps a lone ALT.
+ *
+ * depth and the relative next test are stable across the next/jump values the
+ * compiler assigns to branch tails and nested alternations, which is what makes
+ * them suitable to anchor scope and branch boundaries.
+ *
+ * EXPLAIN parenthesizes every ALT on its own, so a top-level "A | B" deparses
+ * as "(a | b)".  This self-consistent EXPLAIN form is the correctness oracle
+ * here; pg_get_viewdef differs, as its parens come only from an enclosing
+ * GROUP.  Absorption markers (' ") are orthogonal and handled by
+ * append_rpr_quantifier().
+ *
+ * Two compiler invariants hold throughout: {1,1} groups are unwrapped before
+ * bytecode generation (so every BEGIN/END group carries a non-trivial
+ * quantifier, and a lone ALT inside a group always spans to the group's END),
+ * and a group's quantifier is read from its END element (the BEGIN copy is
+ * ignored).
  */
 static char *
 deparse_rpr_pattern(RPRPattern *pattern)
 {
 	StringInfoData buf;
-	int			idx = 0;
-	RPRDepth	prevDepth = 0;
-	bool		needSpace = false;
 
 	Assert(pattern != NULL && pattern->numElements >= 2);
 
 	initStringInfo(&buf);
-
-	deparse_rpr_elements(pattern, &idx, &buf, RPR_DEPTH_NONE,
-						 &prevDepth, &needSpace);
-
-	/* Close remaining open parens */
-	while (prevDepth > 0)
-	{
-		appendStringInfoChar(&buf, ')');
-		prevDepth--;
-	}
-
+	deparse_rpr_seq(pattern, 0, pattern->numElements, &buf);
 	return buf.data;
 }
 
 /*
- * Process pattern elements sequentially until FIN or END at groupDepth.
+ * Deparse a run of sibling elements in [start, limit), separated by spaces.
  *
- * When groupDepth >= 0, stops at the matching END element (leaving idx
- * pointing to it) so the caller (deparse_rpr_group) can consume it.
- * When groupDepth < 0, processes until FIN (top-level call).
+ * Stops at limit or at the FIN terminator (top-level call passes limit =
+ * numElements, where the last element is FIN).  Returns the index reached.
  */
-static void
-deparse_rpr_elements(RPRPattern *pattern, int *idx, StringInfoData *buf,
-					 RPRDepth groupDepth, RPRDepth *prevDepth,
-					 bool *needSpace)
+static int
+deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
 {
-	List	   *altSeps = NIL;	/* pending alternation separator indices */
+	int			i = start;
+	bool		first = true;
 
-	while (*idx < pattern->numElements)
+	while (i < limit && !RPRElemIsFin(&pattern->elements[i]))
 	{
-		RPRPatternElement *elem = &pattern->elements[*idx];
-
-		if (RPRElemIsFin(elem))
-			break;
-
-		/* Stop at END matching our group depth; caller handles it */
-		if (RPRElemIsEnd(elem) && elem->depth == groupDepth)
-			break;
-
-		/* Alternation separator */
-		if (list_member_int(altSeps, *idx))
-		{
-			/* Close parens to match separator depth first */
-			while (*prevDepth > elem->depth)
-			{
-				appendStringInfoChar(buf, ')');
-				(*prevDepth)--;
-			}
-			appendStringInfoString(buf, " | ");
-			*needSpace = false;
-			altSeps = list_delete_int(altSeps, *idx);
-		}
-
-		/* Dispatch to element-type handlers */
-		if (RPRElemIsAlt(elem))
-			deparse_rpr_alt(pattern, idx, buf, prevDepth,
-							needSpace, &altSeps);
-		else if (RPRElemIsBegin(elem))
-			deparse_rpr_group(pattern, idx, buf, prevDepth,
-							  needSpace);
-		else if (RPRElemIsVar(elem))
-			deparse_rpr_var(pattern, idx, buf, prevDepth,
-							needSpace, &altSeps);
-	}
-	list_free(altSeps);
+		if (!first)
+			appendStringInfoChar(buf, ' ');
+		first = false;
+		i = deparse_rpr_node(pattern, i, limit, buf);
+	}
+	return i;
 }
 
 /*
- * Process a BEGIN...END group.
+ * Deparse the single construct starting at index i, bounded by the inherited
+ * limit.  Returns the index just past the construct.
  *
- * Consumes BEGIN, recurses into deparse_rpr_elements for group content,
- * then consumes END and outputs the group quantifier.
- *
- * When the group wraps a single ALT with no siblings, the group-level
- * parenthesis is suppressed since the ALT-to-children depth transition
- * already provides it (avoids double parens like "((a | b))+").
+ * 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
+ * runs to the END, the ALT supplies the parentheses and the group only adds
+ * the quantifier, otherwise the group body is wrapped in its own "( )".  An
+ * ALT runs to its depth-determined scope end (capped by the inherited limit)
+ * and emits "( b1 | b2 | ... )", each branch deparsed within the boundary
+ * handed down by rpr_next_branch.
  */
-static void
-deparse_rpr_group(RPRPattern *pattern, int *idx, StringInfoData *buf,
-				  RPRDepth *prevDepth, bool *needSpace)
+static int
+deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
 {
-	RPRPatternElement *begin = &pattern->elements[*idx];
-	RPRDepth	childDepth = begin->depth + 1;
-	bool		singleAlt = false;
-	RPRPatternElement *end;
+	RPRPatternElement *elem = &pattern->elements[i];
 
-	/*
-	 * Check if this group wraps a single ALT with no siblings. Scan from
-	 * after ALT to END: if no element at childDepth exists, the ALT is the
-	 * sole child.
-	 */
-	if (*idx + 1 < pattern->numElements &&
-		RPRElemIsAlt(&pattern->elements[*idx + 1]))
+	if (RPRElemIsVar(elem))
 	{
-		int			j;
+		Assert(elem->varId < pattern->numVars);
+		appendStringInfoString(buf,
+							   quote_identifier(pattern->varNames[elem->varId]));
+		append_rpr_quantifier(buf, elem);
+		return i + 1;
+	}
 
-		singleAlt = true;
-		for (j = *idx + 2; j < pattern->numElements; j++)
-		{
-			RPRPatternElement *e = &pattern->elements[j];
+	if (RPRElemIsBegin(elem))
+	{
+		int			end = rpr_match_end(pattern, i);
+		bool		loneAlt;
 
-			if (RPRElemIsEnd(e) && e->depth == begin->depth)
-				break;
-			if (e->depth <= childDepth)
-			{
-				singleAlt = false;
-				break;
-			}
+		loneAlt = (i + 1 < end &&
+				   RPRElemIsAlt(&pattern->elements[i + 1]) &&
+				   rpr_alt_scope_end(pattern, i + 1) == end);
+
+		if (loneAlt)
+		{
+			/* The ALT child already parenthesizes the whole group body. */
+			(void) deparse_rpr_node(pattern, i + 1, end, buf);
 		}
+		else
+		{
+			appendStringInfoChar(buf, '(');
+			(void) deparse_rpr_seq(pattern, i + 1, end, buf);
+			appendStringInfoChar(buf, ')');
+		}
+		append_rpr_quantifier(buf, &pattern->elements[end]);
+		return end + 1;
 	}
 
-	/* Open group paren (unless single ALT provides it) */
-	if (!singleAlt)
+	Assert(RPRElemIsAlt(elem));
 	{
-		if (*needSpace)
-			appendStringInfoChar(buf, ' ');
-		appendStringInfoChar(buf, '(');
-		*needSpace = false;
-	}
-	*prevDepth = childDepth;
-	(*idx)++;					/* consume BEGIN */
+		int			altEnd = rpr_alt_scope_end(pattern, i);
+		int			b;
+		bool		first = true;
 
-	/* Process group children; stops at matching END */
-	deparse_rpr_elements(pattern, idx, buf, begin->depth,
-						 prevDepth, needSpace);
+		if (altEnd > limit)
+			altEnd = limit;
 
-	/* Consume END and output quantifier */
-	Assert(*idx < pattern->numElements);
-	end = &pattern->elements[*idx];
-	Assert(RPRElemIsEnd(end) && end->depth == begin->depth);
+		appendStringInfoChar(buf, '(');
+		b = i + 1;
+		while (b < altEnd)
+		{
+			int			nb = rpr_next_branch(pattern, b, altEnd);
 
-	while (*prevDepth > end->depth + 1)
-	{
+			if (!first)
+				appendStringInfoString(buf, " | ");
+			first = false;
+			(void) deparse_rpr_seq(pattern, b, nb, buf);
+			b = nb;
+		}
 		appendStringInfoChar(buf, ')');
-		(*prevDepth)--;
+		return altEnd;
 	}
-	if (!singleAlt)
-		appendStringInfoChar(buf, ')');
-	append_rpr_quantifier(buf, end);
-	*prevDepth = end->depth;
-	*needSpace = true;
-	(*idx)++;					/* consume END */
 }
 
 /*
- * Process an ALT element: adjust depth parens and register separator positions.
+ * Find the END that closes the group opened by the BEGIN at beginIdx: the
+ * first END at the same depth scanning forward.
  */
-static void
-deparse_rpr_alt(RPRPattern *pattern, int *idx, StringInfoData *buf,
-				RPRDepth *prevDepth, bool *needSpace, List **altSeps)
+static int
+rpr_match_end(RPRPattern *pattern, int beginIdx)
 {
-	RPRPatternElement *elem = &pattern->elements[*idx];
-
-	/* Close parens for depth decrease */
-	while (*prevDepth > elem->depth)
-	{
-		appendStringInfoChar(buf, ')');
-		(*prevDepth)--;
-		*needSpace = true;
-	}
-
-	/* Open parens up to ALT's depth */
-	while (*prevDepth < elem->depth)
-	{
-		if (*needSpace)
-			appendStringInfoChar(buf, ' ');
-		appendStringInfoChar(buf, '(');
-		(*prevDepth)++;
-		*needSpace = false;
-	}
+	RPRDepth	d = pattern->elements[beginIdx].depth;
+	int			j;
 
-	/* Register next alternation separator position */
-	if (elem->next != RPR_ELEMIDX_INVALID)
+	for (j = beginIdx + 1; j < pattern->numElements; j++)
 	{
-		RPRPatternElement *firstElem = &pattern->elements[elem->next];
+		RPRPatternElement *e = &pattern->elements[j];
 
-		if (firstElem->jump != RPR_ELEMIDX_INVALID)
-			*altSeps = lappend_int(*altSeps, firstElem->jump);
+		if (RPRElemIsEnd(e) && e->depth == d)
+			return j;
 	}
-	if (elem->jump != RPR_ELEMIDX_INVALID)
-		*altSeps = lappend_int(*altSeps, elem->jump);
-	(*idx)++;
+	pg_unreachable();			/* a BEGIN always has a matching END */
 }
 
 /*
- * Process a VAR element: adjust depth parens and output variable name.
+ * 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.
  */
-static void
-deparse_rpr_var(RPRPattern *pattern, int *idx, StringInfoData *buf,
-				RPRDepth *prevDepth, bool *needSpace, List **altSeps)
+static int
+rpr_alt_scope_end(RPRPattern *pattern, int i)
 {
-	RPRPatternElement *elem = &pattern->elements[*idx];
-
-	/* Open parens for depth increase */
-	while (*prevDepth < elem->depth)
-	{
-		if (*needSpace)
-			appendStringInfoChar(buf, ' ');
-		appendStringInfoChar(buf, '(');
-		(*prevDepth)++;
-		*needSpace = false;
-	}
+	RPRDepth	d = pattern->elements[i].depth;
+	int			k;
 
-	/* Close parens for depth decrease */
-	while (*prevDepth > elem->depth)
+	for (k = i + 1; k < pattern->numElements; k++)
 	{
-		appendStringInfoChar(buf, ')');
-		(*prevDepth)--;
+		if (pattern->elements[k].depth <= d)
+			return k;
 	}
+	return pattern->numElements;
+}
 
-	if (*needSpace)
-		appendStringInfoChar(buf, ' ');
-
-	Assert(elem->varId < pattern->numVars);
-	appendStringInfoString(buf, quote_identifier(pattern->varNames[elem->varId]));
-	append_rpr_quantifier(buf, elem);
-	*needSpace = true;
+/*
+ * Boundary of the alternation branch starting at b (i.e. the start of the next
+ * branch, or altEnd if b 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
+ * skip path), so confirm a real branch boundary with the relative test
+ * elem[j-1].next != j: at a true boundary the preceding branch's tail has its
+ * next redirected past the alternation, so it does not point at j.
+ */
+static int
+rpr_next_branch(RPRPattern *pattern, int b, int altEnd)
+{
+	int			j = pattern->elements[b].jump;
 
-	if (elem->jump != RPR_ELEMIDX_INVALID)
-		*altSeps = lappend_int(*altSeps, elem->jump);
-	(*idx)++;
+	if (j != RPR_ELEMIDX_INVALID && j < altEnd &&
+		pattern->elements[j - 1].next != j)
+		return j;
+	return altEnd;
 }
 
 /*
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 5cddd1a56df..bcbf4f941ba 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3761,6 +3761,956 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
 (9 rows)
 
+-- Quantified group as the first alternation branch
+-- Pattern: ((A B)+ | C) - leading group branch must open the enclosing paren
+CREATE VIEW rpr_ev_alt_grp_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line           
+-------------------------
+   PATTERN ((a b)+ | c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" | c)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 67 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 7 pruned
+   NFA: 9 matched (len 1/2/1.4), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group as the last alternation branch
+-- Pattern: (C | (A B)+) - trailing group branch, no separator follows
+CREATE VIEW rpr_ev_alt_grp_last AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line           
+-------------------------
+   PATTERN (c | (a b)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | (a' b')+")
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 67 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 7 pruned
+   NFA: 9 matched (len 1/2/1.4), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group as the middle branch of a three-way alternation
+-- Pattern: (C | (A B)+ | D) - separator before D must survive the group branch
+CREATE VIEW rpr_ev_alt_grp_mid AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_mid'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN (c | (a b)+ | d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | (a' b')+" | d)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 88 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 2 pruned
+   NFA: 14 matched (len 1/2/1.3), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group as the first branch of a three-way alternation
+-- Pattern: ((A B)+ | C | D) - leading group branch with two following branches
+CREATE VIEW rpr_ev_alt_grp_first3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN ((a b)+ | c | d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" | c | d)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 88 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 2 pruned
+   NFA: 14 matched (len 1/2/1.3), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Bounded-quantifier group as the first alternation branch
+-- Pattern: ((A B){2} | C) - leading group branch with a range quantifier
+CREATE VIEW rpr_ev_alt_grp_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2} | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN ((a b){2} | c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2} | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a b){2} | c)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 63 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 11 pruned
+   NFA: 5 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Two quantified groups in one alternation
+-- Pattern: ((A B)+ | (C D)+) - both branches are groups
+CREATE VIEW rpr_ev_alt_grp_both AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | (C D)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_both'), E'\n')) AS line WHERE line ~ 'PATTERN';
+             line             
+------------------------------
+   PATTERN ((a b)+ | (c d)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | (C D)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" | (c' d')+")
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 72 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 2 pruned
+   NFA: 9 matched (len 2/2/2.0), 0 mismatched
+   NFA: 0 absorbed, 9 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Leading group branch in an alternation nested in a sequence
+-- Pattern: (((A B)+ | C) D) - inner alternation opens with a group branch
+CREATE VIEW rpr_ev_alt_grp_seq_head AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ | C) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_head'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN (((a b)+ | c) d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ | C) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" | c) d
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 67 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 3/3/3.0)
+   NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Trailing group branch in an alternation nested in a sequence
+-- Pattern: ((C | (A B)+) D) - group as last branch, then a sequence element
+CREATE VIEW rpr_ev_alt_grp_seq_tail AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((C | (A B)+) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_tail'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN ((c | (a b)+) d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((C | (A B)+) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | (a' b')+") d
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 67 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 3/3/3.0)
+   NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified alternation whose first branch is a quantified group
+-- Pattern: (((A B){2} | C)+) - single-ALT group wraps a leading group branch
+CREATE VIEW rpr_ev_alt_grp_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B){2} | C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
+             line             
+------------------------------
+   PATTERN (((a b){2} | c)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B){2} | C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a b){2} | c)+
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 78 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 11 pruned
+   NFA: 5 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Unit (1,1) group as an alternation branch (emits no BEGIN/END)
+-- Pattern: ((A B) | C) - control: takes the variable path, not deparse_rpr_group
+CREATE VIEW rpr_ev_alt_grp_unit AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_unit'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a b) | c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b | c)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 63 total, 0 merged
+   NFA Contexts: 2 peak, 21 total, 7 pruned
+   NFA: 9 matched (len 1/2/1.4), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified variable as the first alternation branch
+-- Pattern: (A+ | C) - control: deparse_rpr_var already opens the leading paren
+CREATE VIEW rpr_ev_alt_var_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | C)
+    DEFINE A AS v % 4 = 0, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_var_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        line         
+---------------------
+   PATTERN (a+ | c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | C)
+    DEFINE A AS v % 4 = 0, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a+" | c)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 68 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 10 pruned
+   NFA: 10 matched (len 1/1/1.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Quantified group as the last branch of a three-way alternation
+-- Pattern: (C | D | (A B)+) - control: trailing group needs no separator
+CREATE VIEW rpr_ev_alt_grp_last3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | D | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN (c | d | (a b)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | D | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | d | (a' b')+")
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 88 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 2 pruned
+   NFA: 14 matched (len 1/2/1.3), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Alternation nested in a leading branch must not swallow the trailing branch
+-- Pattern: (D (A | B) | E) - inherited limit bounds the inner alternation
+CREATE VIEW rpr_ev_alt_inner_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (D (A | B) | E)
+    DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_inner_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line            
+----------------------------
+   PATTERN (d (a | b) | e) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (D (A | B) | E)
+    DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (d (a | b) | e)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 71 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 12 pruned
+   NFA: 4 matched (len 1/1/1.0), 4 mismatched (len 2/2/2.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Group mid-branch followed by a sequence element needs no separator before it
+-- Pattern: (C | (A B)+ D) - relative-next blocks a spurious separator at D
+CREATE VIEW rpr_ev_alt_grp_then_seq AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_then_seq'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN (c | (a b)+ d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | (a' b')+" d)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 88 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 10 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Quantified group wrapping a lone alternation: the ALT supplies the parens
+-- Pattern: ((A | B)+) - loneAlt path, single pair of parens
+CREATE VIEW rpr_ev_grp_lone_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_lone_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN ((a | b)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b)+
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 93 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 10 pruned
+   NFA: 6 matched (len 1/2/1.7), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group wrapping a sequence whose last element is an alternation
+-- Pattern: ((A (B | C))+) - group paren plus a nested alternation paren
+CREATE VIEW rpr_ev_grp_seq_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B | C))+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_seq_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN ((a (b | c))+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B | C))+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b | c))+
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 35 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 12 pruned
+   NFA: 4 matched (len 2/2/2.0), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group wrapping a sequence whose first element is an alternation
+-- Pattern: (((A | B) C)+) - leading nested alternation inside a group sequence
+CREATE VIEW rpr_ev_grp_alt_seq AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_alt_seq'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN (((a | b) c)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a | b) c)+
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 78 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Alternation non-last in a non-last branch, stacked three deep: each level's
+-- inherited limit must bound the inner alternation against the next branch
+-- Pattern: (((A | B) C | D) E | F) - three nested inherited-limit boundaries
+CREATE VIEW rpr_ev_alt_stack3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                line                
+------------------------------------
+   PATTERN (((a | b) c | d) e | f) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (((a | b) c | d) e | f)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 147 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 4 pruned
+   NFA: 6 matched (len 1/2/1.5), 7 mismatched (len 2/3/2.4)
+   NFA: 0 absorbed, 3 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Same interaction stacked four deep, to exercise the induction one step further
+-- Pattern: ((((A | B) C | D) E | F) G | H) - four nested inherited-limit boundaries
+CREATE VIEW rpr_ev_alt_stack4 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B) C | D) E | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack4'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                    line                    
+--------------------------------------------
+   PATTERN ((((a | b) c | d) e | f) g | h) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B) C | D) E | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((((a | b) c | d) e | f) g | h)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 7 peak, 189 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 4 matched (len 1/2/1.5), 8 mismatched (len 2/3/2.6)
+   NFA: 0 absorbed, 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Three-deep stack whose innermost branch is a quantified group: the group's
+-- skip-target jump must not be mistaken for a branch separator at any depth
+-- Pattern: (((A | B)+ C | D) E | F) - inherited limit plus loneAlt at the base
+CREATE VIEW rpr_ev_alt_stack3_grp AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B)+ C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3_grp'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                line                 
+-------------------------------------
+   PATTERN (((a | b)+ c | d) e | f) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B)+ C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (((a | b)+ c | d) e | f)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 11 peak, 177 total, 0 merged
+   NFA Contexts: 4 peak, 21 total, 4 pruned
+   NFA: 6 matched (len 1/2/1.5), 7 mismatched (len 2/4/3.1)
+   NFA: 0 absorbed, 3 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Alternation trailing a paren-less sequence (last element of a non-last
+-- branch, no same-depth sibling to bound it), nested three deep
+-- Pattern: (A (B (C | D) | E) | F) - each inner alternation is branch-tail
+CREATE VIEW rpr_ev_alt_tail3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C | D) | E) | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                line                
+------------------------------------
+   PATTERN (a (b (c | d) | e) | f) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C | D) | E) | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b (c | d) | e) | f)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 75 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 11 pruned
+   NFA: 6 matched (len 1/3/2.0), 0 mismatched
+   NFA: 0 absorbed, 3 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Same branch-tail alternation nested four deep
+-- Pattern: (A (B (C (D | E) | F) | G) | H) - branch-tail alternation x4
+CREATE VIEW rpr_ev_alt_tail4 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail4'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                    line                    
+--------------------------------------------
+   PATTERN (a (b (c (d | e) | f) | g) | h) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b (c (d | e) | f) | g) | h)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 75 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 14 pruned
+   NFA: 4 matched (len 1/4/2.5), 0 mismatched
+   NFA: 0 absorbed, 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- A nested alternation tail neighbouring a multi-element sequence branch: the
+-- branch boundary must split "...branch-tail ALT" from a plain "G A" sequence
+-- Pattern: (A (B (C (D | E) | F) | G A) | H) - seq branch beside an ALT tail
+CREATE VIEW rpr_ev_alt_tail_seqbranch AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G A) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail_seqbranch'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                     line                     
+----------------------------------------------
+   PATTERN (a (b (c (d | e) | f) | g a) | h) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G A) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b (c (d | e) | f) | g a) | h)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 75 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 14 pruned
+   NFA: 4 matched (len 1/4/2.5), 0 mismatched
+   NFA: 0 absorbed, 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- A nested alternation that is sibling-bounded by a trailing sequence element
+-- at the outer level (the ALT is not the branch tail; G follows it in-branch)
+-- Pattern: ((A (B (C | D) | E) | F) G | H) - ALT bounded by a following element
+CREATE VIEW rpr_ev_alt_mid_seqtail AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B (C | D) | E) | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_mid_seqtail'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                    line                    
+--------------------------------------------
+   PATTERN ((a (b (c | d) | e) | f) g | h) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B (C | D) | E) | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a (b (c | d) | e) | f) g | h)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 113 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 12 pruned
+   NFA: 4 matched (len 1/2/1.5), 2 mismatched (len 4/4/4.0)
+   NFA: 0 absorbed, 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
 -- ============================================================
 -- Group Pattern Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index c8b159e30e6..aa78ffed260 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2118,6 +2118,529 @@ WINDOW w AS (
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
 
+-- Quantified group as the first alternation branch
+-- Pattern: ((A B)+ | C) - leading group branch must open the enclosing paren
+CREATE VIEW rpr_ev_alt_grp_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Quantified group as the last alternation branch
+-- Pattern: (C | (A B)+) - trailing group branch, no separator follows
+CREATE VIEW rpr_ev_alt_grp_last AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Quantified group as the middle branch of a three-way alternation
+-- Pattern: (C | (A B)+ | D) - separator before D must survive the group branch
+CREATE VIEW rpr_ev_alt_grp_mid AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_mid'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Quantified group as the first branch of a three-way alternation
+-- Pattern: ((A B)+ | C | D) - leading group branch with two following branches
+CREATE VIEW rpr_ev_alt_grp_first3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Bounded-quantifier group as the first alternation branch
+-- Pattern: ((A B){2} | C) - leading group branch with a range quantifier
+CREATE VIEW rpr_ev_alt_grp_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2} | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2} | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Two quantified groups in one alternation
+-- Pattern: ((A B)+ | (C D)+) - both branches are groups
+CREATE VIEW rpr_ev_alt_grp_both AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | (C D)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_both'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | (C D)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Leading group branch in an alternation nested in a sequence
+-- Pattern: (((A B)+ | C) D) - inner alternation opens with a group branch
+CREATE VIEW rpr_ev_alt_grp_seq_head AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ | C) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_head'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ | C) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Trailing group branch in an alternation nested in a sequence
+-- Pattern: ((C | (A B)+) D) - group as last branch, then a sequence element
+CREATE VIEW rpr_ev_alt_grp_seq_tail AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((C | (A B)+) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_tail'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((C | (A B)+) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Quantified alternation whose first branch is a quantified group
+-- Pattern: (((A B){2} | C)+) - single-ALT group wraps a leading group branch
+CREATE VIEW rpr_ev_alt_grp_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B){2} | C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B){2} | C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Unit (1,1) group as an alternation branch (emits no BEGIN/END)
+-- Pattern: ((A B) | C) - control: takes the variable path, not deparse_rpr_group
+CREATE VIEW rpr_ev_alt_grp_unit AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_unit'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Quantified variable as the first alternation branch
+-- Pattern: (A+ | C) - control: deparse_rpr_var already opens the leading paren
+CREATE VIEW rpr_ev_alt_var_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | C)
+    DEFINE A AS v % 4 = 0, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_var_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | C)
+    DEFINE A AS v % 4 = 0, C AS v % 4 = 2
+);');
+
+-- Quantified group as the last branch of a three-way alternation
+-- Pattern: (C | D | (A B)+) - control: trailing group needs no separator
+CREATE VIEW rpr_ev_alt_grp_last3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | D | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | D | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Alternation nested in a leading branch must not swallow the trailing branch
+-- Pattern: (D (A | B) | E) - inherited limit bounds the inner alternation
+CREATE VIEW rpr_ev_alt_inner_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (D (A | B) | E)
+    DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_inner_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (D (A | B) | E)
+    DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3
+);');
+
+-- Group mid-branch followed by a sequence element needs no separator before it
+-- Pattern: (C | (A B)+ D) - relative-next blocks a spurious separator at D
+CREATE VIEW rpr_ev_alt_grp_then_seq AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_then_seq'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Quantified group wrapping a lone alternation: the ALT supplies the parens
+-- Pattern: ((A | B)+) - loneAlt path, single pair of parens
+CREATE VIEW rpr_ev_grp_lone_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_lone_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1
+);');
+
+-- Quantified group wrapping a sequence whose last element is an alternation
+-- Pattern: ((A (B | C))+) - group paren plus a nested alternation paren
+CREATE VIEW rpr_ev_grp_seq_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B | C))+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_seq_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B | C))+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Quantified group wrapping a sequence whose first element is an alternation
+-- Pattern: (((A | B) C)+) - leading nested alternation inside a group sequence
+CREATE VIEW rpr_ev_grp_alt_seq AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_alt_seq'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Alternation non-last in a non-last branch, stacked three deep: each level's
+-- inherited limit must bound the inner alternation against the next branch
+-- Pattern: (((A | B) C | D) E | F) - three nested inherited-limit boundaries
+CREATE VIEW rpr_ev_alt_stack3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+
+-- Same interaction stacked four deep, to exercise the induction one step further
+-- Pattern: ((((A | B) C | D) E | F) G | H) - four nested inherited-limit boundaries
+CREATE VIEW rpr_ev_alt_stack4 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B) C | D) E | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack4'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B) C | D) E | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+
+-- Three-deep stack whose innermost branch is a quantified group: the group's
+-- skip-target jump must not be mistaken for a branch separator at any depth
+-- Pattern: (((A | B)+ C | D) E | F) - inherited limit plus loneAlt at the base
+CREATE VIEW rpr_ev_alt_stack3_grp AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B)+ C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3_grp'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B)+ C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+
+-- Alternation trailing a paren-less sequence (last element of a non-last
+-- branch, no same-depth sibling to bound it), nested three deep
+-- Pattern: (A (B (C | D) | E) | F) - each inner alternation is branch-tail
+CREATE VIEW rpr_ev_alt_tail3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C | D) | E) | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C | D) | E) | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+
+-- Same branch-tail alternation nested four deep
+-- Pattern: (A (B (C (D | E) | F) | G) | H) - branch-tail alternation x4
+CREATE VIEW rpr_ev_alt_tail4 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail4'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+
+-- A nested alternation tail neighbouring a multi-element sequence branch: the
+-- branch boundary must split "...branch-tail ALT" from a plain "G A" sequence
+-- Pattern: (A (B (C (D | E) | F) | G A) | H) - seq branch beside an ALT tail
+CREATE VIEW rpr_ev_alt_tail_seqbranch AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G A) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail_seqbranch'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G A) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+
+-- A nested alternation that is sibling-bounded by a trailing sequence element
+-- at the outer level (the ALT is not the branch tail; G follows it in-branch)
+-- Pattern: ((A (B (C | D) | E) | F) G | H) - ALT bounded by a following element
+CREATE VIEW rpr_ev_alt_mid_seqtail AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B (C | D) | E) | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_mid_seqtail'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B (C | D) | E) | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+
 -- ============================================================
 -- Group Pattern Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)

From 37ae9552adb03842984f4305b9b72bb26af3591d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 7 Jun 2026 18:27:16 +0900
Subject: [PATCH 56/68] Reject invalid column references in row pattern DEFINE
 clauses

A DEFINE expression could reference an outer query's column (a correlated
reference), which surfaced as the internal "Upper-level Var found where not
expected" error (XX000) raised by pull_var_clause.  Separately, a schema- or
catalog-qualified column reference (three or more name parts) slipped past
the existing two-part classifier and was wrongly accepted.

Reject both in transformColumnRef, right after the reference is resolved: an
outer-level Var (varlevelsup > 0) with ERRCODE_FEATURE_NOT_SUPPORTED, and a
reference with three or more name parts with ERRCODE_SYNTAX_ERROR.  The latter
uses "qualified expression" wording because a composite-typed qualifier
(e.g. "a.items" in (a.items).amount) is not a column name.
---
 src/backend/parser/parse_expr.c   | 24 ++++++++++
 src/test/regress/expected/rpr.out | 74 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 61 +++++++++++++++++++++++++
 3 files changed, 159 insertions(+)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 2344aaef9ae..f65a270d20e 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -947,6 +947,30 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		}
 	}
 
+	/*
+	 * Restrict column references in a row pattern DEFINE clause.  node is now
+	 * a successfully resolved reference, so reject the two forms RPR does not
+	 * allow: a correlated reference to an outer query's column, and a
+	 * schema/catalog-qualified reference (three or more name parts).  Simple
+	 * two-part qualifiers (pattern or range variable) are handled earlier,
+	 * before resolution.
+	 */
+	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE)
+	{
+		if (IsA(node, Var) && ((Var *) node)->varlevelsup > 0)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot use outer query column in DEFINE clause"),
+					parser_errposition(pstate, cref->location));
+
+		if (list_length(cref->fields) >= 3)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("qualified expression \"%s\" is not allowed in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
+	}
+
 	return node;
 }
 
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 550113700a9..1b409b923dd 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1295,6 +1295,80 @@ ERROR:  volatile functions are not allowed in DEFINE clause
 LINE 7:     DEFINE A AS price > nextval('rpr_seq')
                                 ^
 DROP SEQUENCE rpr_seq;
+-- DEFINE cannot reference an outer query's column.  A correlated outer
+-- reference must produce a clean error, not the internal "Upper-level Var"
+-- elog that pull_var_clause would otherwise raise.
+-- Qualified outer reference (o.threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > o.threshold
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 9:         DEFINE A AS price > o.threshold
+                                    ^
+-- Unqualified name resolving to the outer column (threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > threshold
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 9:         DEFINE A AS price > threshold
+                                    ^
+-- Outer reference inside a navigation argument is rejected too:
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company ORDER BY tdate
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS PREV(o.threshold, 1) > 0
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 8:         DEFINE A AS PREV(o.threshold, 1) > 0
+                                 ^
+-- DEFINE rejects a schema-qualified column reference (three or more name
+-- parts) once it resolves; the qualified form itself is not allowed.  (stock
+-- is a temp table, so it is qualified with pg_temp here.)
+-- 3-part (schema.table.column):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS pg_temp.stock.price > 0
+);
+ERROR:  qualified expression "pg_temp.stock.price" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS pg_temp.stock.price > 0
+                        ^
+-- whole-row variant (schema.table.*):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+);
+ERROR:  qualified expression "pg_temp.stock.*" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+                         ^
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 0aa17f01e84..56dff9b6725 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -667,6 +667,67 @@ WINDOW w AS (
 );
 DROP SEQUENCE rpr_seq;
 
+-- DEFINE cannot reference an outer query's column.  A correlated outer
+-- reference must produce a clean error, not the internal "Upper-level Var"
+-- elog that pull_var_clause would otherwise raise.
+-- Qualified outer reference (o.threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > o.threshold
+    )
+) s;
+-- Unqualified name resolving to the outer column (threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > threshold
+    )
+) s;
+-- Outer reference inside a navigation argument is rejected too:
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company ORDER BY tdate
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS PREV(o.threshold, 1) > 0
+    )
+) s;
+
+-- DEFINE rejects a schema-qualified column reference (three or more name
+-- parts) once it resolves; the qualified form itself is not allowed.  (stock
+-- is a temp table, so it is qualified with pg_temp here.)
+-- 3-part (schema.table.column):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS pg_temp.stock.price > 0
+);
+-- whole-row variant (schema.table.*):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+);
+
 --
 -- 2-arg PREV/NEXT: functional tests
 --
-- 
2.50.1 (Apple Git-155)

From fcb8bccafe66cf7f1ded30e5237bb157a94c82e5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 7 Jun 2026 20:38:00 +0900
Subject: [PATCH 58/68] Compare varno when preserving DEFINE-referenced columns
 in row pattern recognition

remove_unused_subquery_outputs keeps subquery output columns that an RPR
DEFINE clause references.  The check compared only varattno, so an unrelated
output column of a different relation that happened to share an attribute
number was over-retained and propagated through the plan.

Also compare varno (and, as paranoia, varlevelsup), matching the pattern
already used in set_function_pathlist.  The result was already correct; this
just stops retaining the needless column.
---
 src/backend/optimizer/path/allpaths.c         | 13 ++++++-
 src/test/regress/expected/rpr_integration.out | 35 +++++++++++++++++++
 src/test/regress/sql/rpr_integration.sql      | 16 +++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index fb0506933cd..22339f7491f 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4776,7 +4776,18 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 					{
 						Var		   *dvar = (Var *) lfirst(vlc);
 
-						if (dvar->varattno == var->varattno)
+						/*
+						 * Match varno as well as varattno: a Var pulled from
+						 * a DEFINE clause can share an attribute number with
+						 * an unrelated output column of a different relation,
+						 * which would otherwise be over-retained.  Checking
+						 * varlevelsup is just paranoia, since outer
+						 * references in DEFINE are rejected during parse
+						 * analysis.
+						 */
+						if (dvar->varno == var->varno &&
+							dvar->varattno == var->varattno &&
+							dvar->varlevelsup == var->varlevelsup)
 						{
 							needed_by_define = true;
 							break;
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index b598ef95776..2133e2dfe13 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1481,6 +1481,41 @@ ORDER BY o.id;
  10 |  45 |         2
 (10 rows)
 
+-- A column referenced only by DEFINE must not keep an unrelated column that
+-- merely shares its attribute number.  DEFINE references a (rpr_over1); c
+-- (rpr_over2) has the same attno but is unused, so it must be dropped.
+CREATE TABLE rpr_over1 (a int);
+CREATE TABLE rpr_over2 (c int);
+INSERT INTO rpr_over1 VALUES (1),(2),(3);
+INSERT INTO rpr_over2 VALUES (1),(2),(3);
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT cnt FROM (
+  SELECT a AS oa, c AS oc, count(*) OVER w AS cnt
+  FROM rpr_over1 CROSS JOIN rpr_over2
+  WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+               PATTERN (X+) DEFINE X AS a > 0)
+) s;
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ Subquery Scan on s
+   Output: s.cnt
+   ->  WindowAgg
+         Output: rpr_over1.a, NULL::integer, count(*) OVER w
+         Window: w AS (ORDER BY rpr_over1.a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: x+"
+         Nav Mark Lookback: 0
+         ->  Sort
+               Output: rpr_over1.a
+               Sort Key: rpr_over1.a
+               ->  Nested Loop
+                     Output: rpr_over1.a
+                     ->  Seq Scan on public.rpr_over1
+                           Output: rpr_over1.a
+                     ->  Materialize
+                           ->  Seq Scan on public.rpr_over2
+(16 rows)
+
+DROP TABLE rpr_over1, rpr_over2;
 -- Cleanup
 DROP TABLE rpr_integ;
 DROP TABLE rpr_integ2;
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 5f3853becba..24b0b1811b9 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -929,6 +929,22 @@ SELECT o.id, o.val,
 FROM rpr_integ o
 ORDER BY o.id;
 
+-- A column referenced only by DEFINE must not keep an unrelated column that
+-- merely shares its attribute number.  DEFINE references a (rpr_over1); c
+-- (rpr_over2) has the same attno but is unused, so it must be dropped.
+CREATE TABLE rpr_over1 (a int);
+CREATE TABLE rpr_over2 (c int);
+INSERT INTO rpr_over1 VALUES (1),(2),(3);
+INSERT INTO rpr_over2 VALUES (1),(2),(3);
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT cnt FROM (
+  SELECT a AS oa, c AS oc, count(*) OVER w AS cnt
+  FROM rpr_over1 CROSS JOIN rpr_over2
+  WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+               PATTERN (X+) DEFINE X AS a > 0)
+) s;
+DROP TABLE rpr_over1, rpr_over2;
+
 -- Cleanup
 DROP TABLE rpr_integ;
 DROP TABLE rpr_integ2;
-- 
2.50.1 (Apple Git-155)

From 426451992a4a865203f4fed65a5d38839bc1402b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Sun, 7 Jun 2026 19:38:16 +0900
Subject: [PATCH 57/68] Fix shortest match for reluctant nullable quantifiers
 in row pattern recognition

When a reluctant outer quantifier wrapped a nullable reluctant body, such as
(A??)+?, the match consumed rows instead of producing the required shortest
(empty) match.

nfa_advance_end decides a group's repeat-or-exit by comparing count with the
quantifier's min and max.  The count < min branch always routed the loop-back
(real match) before the fast-forward exit and never suppressed it, so a longer
match could replace the shortest one.  The sibling min <= count < max branch
already handles this correctly for reluctant groups, leaving the two branches
asymmetric.

Split the count < min branch into reluctant and greedy cases, mirroring the
sibling: a reluctant group takes the fast-forward exit first and, if it reaches
FIN, frees the loop-back state so a longer match cannot replace the shortest
one.  Greedy and non-nullable groups keep the existing loop-first behaviour.
---
 src/backend/executor/execRPR.c        | 72 +++++++++++++++++----------
 src/test/regress/expected/rpr_nfa.out | 33 ++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 26 ++++++++++
 3 files changed, 106 insertions(+), 25 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 69e3603adef..b7e3c4a8274 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1139,43 +1139,32 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 	{
 		RPRPatternElement *jumpElem;
 		RPRNFAState *ffState = NULL;
+		RPRPatternElement *nextElem = NULL;
 
 		/*----------
-		 * Two paths are explored in parallel when the group body is nullable
+		 * Two paths are explored when the group body is nullable
 		 * (RPR_ELEM_EMPTY_LOOP):
 		 *
-		 * 1. Primary path: loop back and attempt real matches in the
-		 *    next iteration (state, modified below).
+		 * 1. Loop-back path: attempt real matches in the next iteration
+		 *    (state, modified below).
 		 *
-		 * 2. Fast-forward path: skip directly to after the group,
-		 *    treating all remaining required iterations as empty
-		 *    matches (ffState, handled after the primary path).
+		 * 2. Fast-forward path: skip directly to after the group, treating
+		 *    all remaining required iterations as empty matches (ffState).
+		 *    Route to elem->next (not nfa_advance_end) to avoid creating
+		 *    competing greedy/reluctant loop states.
 		 *
-		 * The snapshot must be taken BEFORE modifying state for the loop-back,
-		 * since both paths diverge from the same point.
+		 * Greedy prefers the loop-back first (more iterations); reluctant
+		 * prefers the fast-forward (exit) first and, if it reaches FIN, drops
+		 * the loop-back so a longer match cannot replace the shortest one --
+		 * mirroring the min<=count<max branch below.  The ffState snapshot is
+		 * taken BEFORE modifying state, since both paths diverge from here.
 		 *----------
 		 */
 		if (RPRElemCanEmptyLoop(elem))
+		{
 			ffState = nfa_state_clone(winstate, state->elemIdx,
 									  state->counts, state->isAbsorbable);
 
-		/* Primary path: loop back for real matches */
-		state->elemIdx = elem->jump;
-		jumpElem = &elements[state->elemIdx];
-		nfa_route_to_elem(winstate, ctx, state, jumpElem,
-						  currentPos);
-
-		/*
-		 * Fast-forward path for nullable bodies.  E.g. (A?){2,3} when A
-		 * doesn't match: the primary loop-back produces empty iterations that
-		 * cycle detection would kill.  Instead, exit directly with count
-		 * satisfied.  Route to elem->next (not nfa_advance_end) to avoid
-		 * creating competing greedy/reluctant loop states.
-		 */
-		if (ffState != NULL)
-		{
-			RPRPatternElement *nextElem;
-
 			/* Exit the group: clear its own count (count-clear policy) */
 			ffState->counts[depth] = 0;
 			ffState->elemIdx = elem->next;
@@ -1192,9 +1181,42 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			if (RPRElemIsEnd(nextElem) &&
 				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
 				ffState->counts[nextElem->depth]++;
+		}
+
+		/* Prepare the loop-back state */
+		state->elemIdx = elem->jump;
+		jumpElem = &elements[state->elemIdx];
 
+		if (ffState != NULL && RPRElemIsReluctant(elem))
+		{
+			RPRNFAState *savedMatch = ctx->matchedState;
+
+			/* Reluctant: take the fast-forward (exit) first */
 			nfa_route_to_elem(winstate, ctx, ffState, nextElem,
 							  currentPos);
+
+			/*
+			 * If the exit reached FIN, the shortest match is found.  Skip the
+			 * loop-back to prevent longer matches from replacing it.
+			 */
+			if (ctx->matchedState != savedMatch)
+			{
+				nfa_state_free(winstate, state);
+				return;
+			}
+
+			/* Loop-back second */
+			nfa_route_to_elem(winstate, ctx, state, jumpElem,
+							  currentPos);
+		}
+		else
+		{
+			/* Greedy (or non-nullable): loop-back first, fast-forward second */
+			nfa_route_to_elem(winstate, ctx, state, jumpElem,
+							  currentPos);
+			if (ffState != NULL)
+				nfa_route_to_elem(winstate, ctx, ffState, nextElem,
+								  currentPos);
 		}
 	}
 	else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 02a5e517b0e..3b9975a83df 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1255,6 +1255,39 @@ WINDOW w AS (
   3 | {C}   |             |          
 (3 rows)
 
+-- Reluctant outer quantifier over a nullable reluctant body: SQL/RPR
+-- semantics call for the shortest (empty) match.  The count<min branch of
+-- nfa_advance_end must prefer the fast-forward (exit) path for reluctant
+-- groups and suppress longer matches once exit reaches FIN, mirroring the
+-- sibling min<=count<max branch.  The 2-level greedy/reluctant matrix plus a
+-- min>=2 boundary and single-quantifier controls localize the behaviour: only
+-- the all-reluctant case (rr) should differ.
+WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, true), (4, false))
+SELECT id,
+       count(*) OVER gg  AS gg,     -- (A?)+      greedy / greedy
+       count(*) OVER gr  AS gr,     -- (A??)+     greedy / reluctant
+       count(*) OVER rg  AS rg,     -- (A?)+?     reluctant / greedy
+       count(*) OVER rr  AS rr,     -- (A??)+?    reluctant / reluctant
+       count(*) OVER rr2 AS rr2,    -- (A??){2,}? reluctant, min>=2 boundary
+       count(*) OVER ca  AS ca,     -- A??        single reluctant control
+       count(*) OVER cs  AS cs      -- A*?        single reluctant control
+FROM t
+WINDOW gg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+)      DEFINE A AS isa),
+       gr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+)     DEFINE A AS isa),
+       rg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+?)     DEFINE A AS isa),
+       rr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?)    DEFINE A AS isa),
+       rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa),
+       ca  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??)        DEFINE A AS isa),
+       cs  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?)        DEFINE A AS isa)
+ORDER BY id;
+ id | gg | gr | rg | rr | rr2 | ca | cs 
+----+----+----+----+----+-----+----+----
+  1 |  3 |  3 |  1 |  0 |   0 |  0 |  0
+  2 |  0 |  0 |  1 |  0 |   0 |  0 |  0
+  3 |  0 |  0 |  1 |  0 |   0 |  0 |  0
+  4 |  0 |  0 |  0 |  0 |   0 |  0 |  0
+(4 rows)
+
 -- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
 -- Like the VAR case above but a multi-element group; it goes through the
 -- begin path (nfa_advance_begin), which already honors reluctant ordering.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 213385f143b..61072d1d6f1 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -887,6 +887,32 @@ WINDOW w AS (
         C AS 'C' = ANY(flags)
 );
 
+-- Reluctant outer quantifier over a nullable reluctant body: SQL/RPR
+-- semantics call for the shortest (empty) match.  The count<min branch of
+-- nfa_advance_end must prefer the fast-forward (exit) path for reluctant
+-- groups and suppress longer matches once exit reaches FIN, mirroring the
+-- sibling min<=count<max branch.  The 2-level greedy/reluctant matrix plus a
+-- min>=2 boundary and single-quantifier controls localize the behaviour: only
+-- the all-reluctant case (rr) should differ.
+WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, true), (4, false))
+SELECT id,
+       count(*) OVER gg  AS gg,     -- (A?)+      greedy / greedy
+       count(*) OVER gr  AS gr,     -- (A??)+     greedy / reluctant
+       count(*) OVER rg  AS rg,     -- (A?)+?     reluctant / greedy
+       count(*) OVER rr  AS rr,     -- (A??)+?    reluctant / reluctant
+       count(*) OVER rr2 AS rr2,    -- (A??){2,}? reluctant, min>=2 boundary
+       count(*) OVER ca  AS ca,     -- A??        single reluctant control
+       count(*) OVER cs  AS cs      -- A*?        single reluctant control
+FROM t
+WINDOW gg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+)      DEFINE A AS isa),
+       gr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+)     DEFINE A AS isa),
+       rg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+?)     DEFINE A AS isa),
+       rr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?)    DEFINE A AS isa),
+       rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa),
+       ca  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??)        DEFINE A AS isa),
+       cs  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?)        DEFINE A AS isa)
+ORDER BY id;
+
 -- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
 -- Like the VAR case above but a multi-element group; it goes through the
 -- begin path (nfa_advance_begin), which already honors reluctant ordering.
-- 
2.50.1 (Apple Git-155)

From fcd882526042fa7bec72c0658f02b8dddd2109af Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 11:23:03 +0900
Subject: [PATCH 59/68] Allow a row pattern quantifier with no space before the
 alternation operator

The scanner lexes an operator such as "*|" as a single token, so a PATTERN
like (A*|B) previously failed with "unsupported quantifier".  Recognize these
glued forms in the quantifier rules and, once the surrounding sequence is
built, re-split it at the affected term so "|" keeps its lowest precedence;
(A*|B) now parses identically to the spaced (A* | B).  This covers the op-char
quantifiers (*| +| ?| *?| +?| ??|), the reluctant range forms ({n}?| and the
like), and mixed spacing such as "A* ?|B".  A dangling "|" with no right-hand
pattern is still rejected.
---
 src/backend/parser/gram.y              | 195 +++++++++++++++++++--
 src/backend/parser/parse_rpr.c         |   6 +
 src/include/nodes/parsenodes.h         |   8 +
 src/test/regress/expected/rpr_base.out | 228 +++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 112 ++++++++++++
 5 files changed, 535 insertions(+), 14 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a2fafb717cd..147b5f37293 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -210,6 +210,8 @@ static void preprocess_pub_all_objtype_list(List *all_objects_list,
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+static RPRPatternNode *makeRPRSeqOrSingle(List *children, int location);
+static RPRPatternNode *splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner);
 static RPRPatternNode *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
 									   core_yyscan_t yyscanner);
 
@@ -17624,23 +17626,30 @@ row_pattern:
 		;
 
 row_pattern_alt:
-			row_pattern_seq						{ $$ = $1; }
+			row_pattern_seq
+				{
+					$$ = (Node *) splitRPRTrailingAlt((RPRPatternNode *) $1,
+													 yyscanner);
+				}
 			| row_pattern_alt '|' row_pattern_seq
 				{
 					RPRPatternNode *n;
+					RPRPatternNode *rhs = splitRPRTrailingAlt((RPRPatternNode *) $3,
+															 yyscanner);
+
 					/* If left side is already ALT, append to it */
 					if (IsA($1, RPRPatternNode) &&
 						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_ALT)
 					{
 						n = (RPRPatternNode *) $1;
-						n->children = lappend(n->children, $3);
+						n->children = lappend(n->children, rhs);
 						$$ = (Node *) n;
 					}
 					else
 					{
 						n = makeNode(RPRPatternNode);
 						n->nodeType = RPR_PATTERN_ALT;
-						n->children = list_make2($1, $3);
+						n->children = list_make2($1, rhs);
 						n->min = 1;
 						n->max = 1;
 						n->reluctant = false;
@@ -17656,7 +17665,12 @@ row_pattern_seq:
 			| row_pattern_seq row_pattern_term
 				{
 					RPRPatternNode *n;
-					/* If left side is already SEQ, append to it */
+
+					/*
+					 * If left side is already SEQ, append to it.  A glued
+					 * quantifier's trailing_alt stays on the child term;
+					 * row_pattern_alt splits on it once the seq is complete.
+					 */
 					if (IsA($1, RPRPatternNode) &&
 						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_SEQ)
 					{
@@ -17689,6 +17703,7 @@ row_pattern_term:
 					n->max = q->max;
 					n->reluctant = q->reluctant;
 					n->reluctant_location = q->reluctant_location;
+					n->trailing_alt = q->trailing_alt;
 					$$ = (Node *) n;
 				}
 		;
@@ -17739,6 +17754,36 @@ row_pattern_quantifier_opt:
 						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "??") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+					else if (strcmp($1, "*|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "+|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, 1, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "*?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "+?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "??|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
 					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17749,33 +17794,60 @@ row_pattern_quantifier_opt:
 			/* RELUCTANT quantifiers (when lexer separates tokens) */
 			| '*' Op
 				{
-					if (strcmp($2, "?") != 0)
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A* ?|B" = reluctant "A*?" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after \"*\" quantifier"),
 								errhint("Did you mean \"*?\" for reluctant quantifier?"),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
 				}
 			| '+' Op
 				{
-					if (strcmp($2, "?") != 0)
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A+ ?|B" = reluctant "A+?" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after \"+\" quantifier"),
 								errhint("Did you mean \"+?\" for reluctant quantifier?"),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
 				}
 			| Op Op
 				{
-					if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0)
+					if (strcmp($1, "?") != 0)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid quantifier combination"),
+								errhint("Did you mean \"??\" for reluctant quantifier?"),
+								parser_errposition(@1));
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A? ?|B" = reluctant "A??" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid quantifier combination"),
 								errhint("Did you mean \"??\" for reluctant quantifier?"),
 								parser_errposition(@1));
-					$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
 				}
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
 			| '{' Iconst '}'
@@ -17822,7 +17894,7 @@ row_pattern_quantifier_opt:
 			/* Reluctant versions: {n}?, {n,}?, {,m}?, {n,m}? */
 			| '{' Iconst '}' Op
 				{
-					if (strcmp($4, "?") != 0)
+					if (strcmp($4, "?") != 0 && strcmp($4, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17834,10 +17906,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
+					if (strcmp($4, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' Iconst ',' '}' Op
 				{
-					if (strcmp($5, "?") != 0)
+					if (strcmp($5, "?") != 0 && strcmp($5, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17849,10 +17923,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+					if (strcmp($5, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' ',' Iconst '}' Op
 				{
-					if (strcmp($5, "?") != 0)
+					if (strcmp($5, "?") != 0 && strcmp($5, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17864,10 +17940,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);
+					if (strcmp($5, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' Iconst ',' Iconst '}' Op
 				{
-					if (strcmp($6, "?") != 0)
+					if (strcmp($6, "?") != 0 && strcmp($6, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17884,6 +17962,8 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier minimum bound must not exceed maximum"),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $4, @6, @1, yyscanner);
+					if (strcmp($6, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 		;
 
@@ -21325,6 +21405,93 @@ makeRPRQuantifier(int min, int max, ParseLoc reluctant_location, int location,
 	return n;
 }
 
+/*
+ * Build a SEQ node from children, or return the lone child unchanged.
+ */
+static RPRPatternNode *
+makeRPRSeqOrSingle(List *children, int location)
+{
+	RPRPatternNode *n;
+
+	if (list_length(children) == 1)
+		return (RPRPatternNode *) linitial(children);
+
+	n = makeNode(RPRPatternNode);
+	n->nodeType = RPR_PATTERN_SEQ;
+	n->children = children;
+	n->min = 1;
+	n->max = 1;
+	n->reluctant = false;
+	n->reluctant_location = -1;
+	n->location = location;
+	return n;
+}
+
+/*
+ * A glued quantifier such as "A*|" leaves trailing_alt set on its term while
+ * the enclosing sequence is built.  Once the sequence is complete, split it at
+ * the flagged term into alt(left, right), where the right operand is the whole
+ * remaining sequence -- this keeps "|" as the lowest-precedence operator, so
+ * "A*|B C" parses as "A* | (B C)", identical to the spaced form.  A flag with
+ * nothing to its right is a dangling "|" and is rejected.
+ */
+static RPRPatternNode *
+splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner)
+{
+	ListCell   *lc;
+	int			i = 0;
+
+	if (node->nodeType != RPR_PATTERN_SEQ)
+	{
+		if (node->trailing_alt)
+		{
+			node->trailing_alt = false;
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("alternation operator \"|\" requires a pattern on both sides"),
+					parser_errposition(node->location));
+		}
+		return node;
+	}
+
+	foreach(lc, node->children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+		if (child->trailing_alt)
+		{
+			List	   *lefthalf = list_copy_head(node->children, i + 1);
+			List	   *righthalf = list_copy_tail(node->children, i + 1);
+			RPRPatternNode *altn;
+			RPRPatternNode *rightnode;
+
+			child->trailing_alt = false;
+			if (righthalf == NIL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("alternation operator \"|\" requires a pattern on both sides"),
+						parser_errposition(node->location));
+
+			/* the right branch starts at its own first element, not the seq start */
+			rightnode = splitRPRTrailingAlt(makeRPRSeqOrSingle(righthalf,
+															   ((RPRPatternNode *) linitial(righthalf))->location),
+											yyscanner);
+			altn = makeNode(RPRPatternNode);
+			altn->nodeType = RPR_PATTERN_ALT;
+			altn->children = list_make2(makeRPRSeqOrSingle(lefthalf, node->location),
+										rightnode);
+			altn->min = 1;
+			altn->max = 1;
+			altn->reluctant = false;
+			altn->reluctant_location = -1;
+			altn->location = node->location;
+			return altn;
+		}
+		i++;
+	}
+	return node;
+}
+
 /* parser_init()
  * Initialize to parse one query string
  */
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index c9469b56b7b..4e1d2650cf6 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -213,6 +213,12 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
 
+	/*
+	 * trailing_alt is a transient grammar flag; splitRPRTrailingAlt must have
+	 * cleared it on every node before the pattern reaches parse analysis.
+	 */
+	Assert(!node->trailing_alt);
+
 	check_stack_depth();
 
 	switch (node->nodeType)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5200182aa46..e371f04a403 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -632,6 +632,14 @@ typedef struct RPRPatternNode
 	ParseLoc	location;		/* token location, or -1 */
 	char	   *varName;		/* VAR: variable name */
 	List	   *children;		/* SEQ, ALT, GROUP: child nodes */
+
+	/*
+	 * Transient parse flag, cleared by splitRPRTrailingAlt before the tree is
+	 * finalized: a glued quantifier such as "*|" is immediately followed by
+	 * the alternation operator '|'.  It is always false in a finalized tree,
+	 * so query_jumble_ignore keeps it off the pg_stat_statements queryid.
+	 */
+	bool		trailing_alt pg_node_attr(query_jumble_ignore);
 } RPRPatternNode;
 
 /*
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c50c7a5f6a8..1fcb2ce22f0 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -13,6 +13,7 @@
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
 --
@@ -3083,6 +3084,233 @@ SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
    a AS (val > 0) );
 (1 row)
 
+-- ============================================================
+-- Glued Quantifier / Alternation Tests
+-- ============================================================
+CREATE TABLE rpr_glue (id INT, val INT);
+INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2);
+-- Quantifier glued to the alternation operator '|' without a space (0059).
+-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it
+-- as the lowest-precedence alternation once the surrounding sequence is built.
+-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all
+-- reduce to the same PATTERN -- one deparse per shape proves the parse tree.
+-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'.
+CREATE VIEW rpr_dp_op AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a* | b) 
+   PATTERN (a+ | b) 
+   PATTERN (a? | b) 
+   PATTERN (a*? | b) 
+   PATTERN (a+? | b) 
+   PATTERN (a?? | b) 
+(6 rows)
+
+DROP VIEW rpr_dp_op;
+-- Spaced reference: the fully-spaced canonical forms.  Identical deparse to the
+-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence.
+CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a* | b) 
+   PATTERN (a*? | b) 
+(2 rows)
+
+DROP VIEW rpr_dp_spc;
+-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and
+-- reluctant {n}?| (} + Op "?|").
+CREATE VIEW rpr_dp_rng AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4,
+    count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN (a{2} | b) 
+   PATTERN (a{2,} | b) 
+   PATTERN (a{0,3} | b) 
+   PATTERN (a{2,3} | b) 
+   PATTERN (a{2}? | b) 
+   PATTERN (a{2,}? | b) 
+   PATTERN (a{0,3}? | b) 
+   PATTERN (a{2,3}? | b) 
+(8 rows)
+
+DROP VIEW rpr_dp_rng;
+-- Mixed spacing: a space inside the quantifier with '|' still glued.
+-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation.
+CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a*? | b) 
+   PATTERN (a+? | b) 
+   PATTERN (a?? | b) 
+(3 rows)
+
+DROP VIEW rpr_dp_mix;
+-- Structure: precedence (| is lowest, so its right operand is the whole
+-- following sequence), chaining, concatenation, and grouping.
+CREATE VIEW rpr_dp_struct AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN (a* | b c) 
+   PATTERN (a* | b* | c) 
+   PATTERN (a b* | c d) 
+   PATTERN ((a* | b)) 
+   PATTERN (a* | (b | c)) 
+   PATTERN ((a* | b)+) 
+(6 rows)
+
+DROP VIEW rpr_dp_struct;
+-- Execution semantics (deparse cannot show reluctant shortest-match).  The
+-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative
+-- reachable: with "*" the greedy form matches the whole run while the
+-- reluctant form matches empty; with "+" the greedy form matches the run and
+-- the reluctant form matches one row, and on a B row (where "A+" fails) the B
+-- alternative fires.
+SELECT id, val,
+       count(*) OVER gs AS gstar, count(*) OVER rs AS rstar,
+       count(*) OVER gp AS gplus, count(*) OVER rp AS rplus
+FROM rpr_glue
+WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0)
+ORDER BY id;
+ id | val | gstar | rstar | gplus | rplus 
+----+-----+-------+-------+-------+-------
+  1 |   5 |     3 |     0 |     3 |     1
+  2 |   8 |     0 |     0 |     0 |     1
+  3 |   9 |     0 |     0 |     0 |     1
+  4 |  -1 |     1 |     1 |     1 |     1
+  5 |   6 |     1 |     0 |     1 |     1
+  6 |  -2 |     1 |     1 |     1 |     1
+(6 rows)
+
+-- Patterns that must stay rejected.  "&" is an invalid op; a '|' with an empty
+-- side (leading, trailing, doubled, or alone in a group) has no operand; "||"
+-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled
+-- quantifiers.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0);
+ERROR:  unsupported quantifier "&"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFIN...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DE...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "*||"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "||"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFIN...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  syntax error at or near ")"
+LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0);
+ERROR:  syntax error at or near "|"
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFI...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFIN...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFI...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid quantifier combination
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DE...
+                                                             ^
+HINT:  Did you mean "??" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after range quantifier
+LINE 1: ... CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFI...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after range quantifier
+LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFI...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether
+-- glued to '|' ("**|", "*+|", "???|") or on their own ("**").
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "**|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "*+|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "???|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEF...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0);
+ERROR:  unsupported quantifier "**"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFIN...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+DROP TABLE rpr_glue;
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 7dfb72f6bfd..cc79843aeb7 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -13,6 +13,7 @@
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
 --
@@ -2074,6 +2075,117 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
 
+-- ============================================================
+-- Glued Quantifier / Alternation Tests
+-- ============================================================
+CREATE TABLE rpr_glue (id INT, val INT);
+INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2);
+-- Quantifier glued to the alternation operator '|' without a space (0059).
+-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it
+-- as the lowest-precedence alternation once the surrounding sequence is built.
+-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all
+-- reduce to the same PATTERN -- one deparse per shape proves the parse tree.
+
+-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'.
+CREATE VIEW rpr_dp_op AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_op;
+-- Spaced reference: the fully-spaced canonical forms.  Identical deparse to the
+-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence.
+CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_spc;
+-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and
+-- reluctant {n}?| (} + Op "?|").
+CREATE VIEW rpr_dp_rng AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4,
+    count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_rng;
+-- Mixed spacing: a space inside the quantifier with '|' still glued.
+-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation.
+CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_mix;
+-- Structure: precedence (| is lowest, so its right operand is the whole
+-- following sequence), chaining, concatenation, and grouping.
+CREATE VIEW rpr_dp_struct AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_struct;
+-- Execution semantics (deparse cannot show reluctant shortest-match).  The
+-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative
+-- reachable: with "*" the greedy form matches the whole run while the
+-- reluctant form matches empty; with "+" the greedy form matches the run and
+-- the reluctant form matches one row, and on a B row (where "A+" fails) the B
+-- alternative fires.
+SELECT id, val,
+       count(*) OVER gs AS gstar, count(*) OVER rs AS rstar,
+       count(*) OVER gp AS gplus, count(*) OVER rp AS rplus
+FROM rpr_glue
+WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0)
+ORDER BY id;
+-- Patterns that must stay rejected.  "&" is an invalid op; a '|' with an empty
+-- side (leading, trailing, doubled, or alone in a group) has no operand; "||"
+-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled
+-- quantifiers.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0);
+-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether
+-- glued to '|' ("**|", "*+|", "???|") or on their own ("**").
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0);
+DROP TABLE rpr_glue;
+
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)

From 071126a8559aceea9299a2801a78f241777071dd Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 11:46:56 +0900
Subject: [PATCH 60/68] Fix outdated function and file references in row
 pattern recognition docs

Two comments in rpr_nfa.sql attributed nfa_advance_begin/end to
nodeWindowAgg.c, but both are defined in execRPR.c.  README.rpr referred to a
nonexistent compute_nav_offsets; the planner routine that computes the trim
offsets is compute_define_metadata in createplan.c.  These are comment and
documentation fixes only, with no behavior change; the rpr_nfa expected output
is updated to match the corrected comment.
---
 src/backend/executor/README.rpr       | 4 ++--
 src/test/regress/expected/rpr_nfa.out | 4 ++--
 src/test/regress/sql/rpr_nfa.sql      | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index df574a0a6f4..c1822d8240b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -667,7 +667,7 @@ VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
 
 Navigation functions require access to past rows via the tuplestore.
 To allow tuplestore_trim() to free rows that are no longer reachable,
-the planner computes two offsets (see compute_nav_offsets):
+the planner computes two offsets (see compute_define_metadata):
 
   navMaxOffset (Nav Mark Lookback):
     Maximum backward reach from currentpos.  Contributed by PREV,
@@ -1499,7 +1499,7 @@ Appendix A. Key Function Index
   ExecRPRFinalizeAllContexts    execRPR.c             Partition-end finalize
   ExecRPRRecordContextSuccess   execRPR.c             Stats: match success
   ExecRPRRecordContextFailure   execRPR.c             Stats: match failure
-  compute_nav_offsets           createplan.c          Trim offset computation
+  compute_define_metadata       createplan.c          Trim offset computation
 
 Appendix B. Data Structure Relationship Diagram
 ============================================================================
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 3b9975a83df..d7146168f2b 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1357,7 +1357,7 @@ WINDOW w AS (
 
 -- Reluctant optional group skip-to-FIN
 -- When a reluctant optional group's skip path reaches FIN, the group
--- entry path is abandoned (nodeWindowAgg.c nfa_advance_begin).
+-- entry path is abandoned (execRPR.c nfa_advance_begin).
 -- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)??
 -- prefers to skip.  Skip goes to FIN (group is last element), so
 -- the match completes with just C.
@@ -3364,7 +3364,7 @@ WINDOW w AS (
 -- Nested END->END fast-forward
 -- When an inner group has a nullable body and count < min, the
 -- fast-forward path exits through the outer END, incrementing
--- the outer group's count (nodeWindowAgg.c nfa_advance_end).
+-- the outer group's count (execRPR.c nfa_advance_end).
 -- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses
 -- because the optimizer cannot safely multiply non-exact quantifiers.
 -- Data has no A rows, forcing all-empty iterations via fast-forward.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 61072d1d6f1..8daa0a73725 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -968,7 +968,7 @@ WINDOW w AS (
 
 -- Reluctant optional group skip-to-FIN
 -- When a reluctant optional group's skip path reaches FIN, the group
--- entry path is abandoned (nodeWindowAgg.c nfa_advance_begin).
+-- entry path is abandoned (execRPR.c nfa_advance_begin).
 -- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)??
 -- prefers to skip.  Skip goes to FIN (group is last element), so
 -- the match completes with just C.
@@ -2406,7 +2406,7 @@ WINDOW w AS (
 -- Nested END->END fast-forward
 -- When an inner group has a nullable body and count < min, the
 -- fast-forward path exits through the outer END, incrementing
--- the outer group's count (nodeWindowAgg.c nfa_advance_end).
+-- the outer group's count (execRPR.c nfa_advance_end).
 -- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses
 -- because the optimizer cannot safely multiply non-exact quantifiers.
 -- Data has no A rows, forcing all-empty iterations via fast-forward.
-- 
2.50.1 (Apple Git-155)

From 8d43d10030a6fad989e2ed8bbde1d6b036d348ce Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 12:22:21 +0900
Subject: [PATCH 61/68] Assert that row pattern nesting depth never aliases the
 RPR_DEPTH_NONE sentinel

allocateRPRPattern stores maxDepth + 1, and scanRPRPatternRecursive rejects
nesting at or above RPR_DEPTH_MAX, so the stored value stays <= RPR_DEPTH_MAX
and never reaches the RPR_DEPTH_NONE sentinel (UINT8_MAX).  Assert that bound
so raising RPR_DEPTH_MAX or the depth limit trips immediately.  Also note in
compute_define_metadata that its default offset (1) serves only PREV; the
guarded LAST sub-case never uses it.
---
 src/backend/optimizer/plan/createplan.c | 3 +++
 src/backend/optimizer/plan/rpr.c        | 3 +++
 2 files changed, 6 insertions(+)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c8ecaeea7cf..c5b0857ff20 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2581,6 +2581,9 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 		{
 			int64		offset;
 
+			/*
+			 * default 1 is for PREV; the guarded LAST sub-case never uses it.
+			 */
 			if (extract_const_offset(nav->offset_arg, 1, &offset))
 				context->maxOffset = Max(context->maxOffset, offset);
 			else
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 43114088c3f..e7276c42ca6 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1157,6 +1157,9 @@ allocateRPRPattern(int numVars, int numElements, RPRDepth maxDepth,
 
 	result = makeNode(RPRPattern);
 	result->numVars = numVars;
+
+	/* depth < RPR_DEPTH_MAX, so maxDepth+1 never aliases RPR_DEPTH_NONE. */
+	Assert(maxDepth < RPR_DEPTH_MAX);
 	result->maxDepth = maxDepth + 1;	/* +1: depth is 0-based */
 	result->numElements = numElements;
 
-- 
2.50.1 (Apple Git-155)

From f55bf199846bb94e42cb3238a2dc92ccff1c50fe Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 12:44:45 +0900
Subject: [PATCH 62/68] Invalidate the row pattern nav slot cache when a window
 partition changes

release_partition clears the per-partition tuplestore, leaving the cached
nav slot position (which is partition-relative) stale.  This is not a live
bug: nfa_evaluate_row resets nav_slot_pos to -1 before each row's DEFINE
evaluation, the only path that fetches nav slots, so a stale position never
produces a cache hit.  Reset it here too for consistency with the rest of the
per-partition cleanup.  Also note in visit_nav_plan that a constant
LAST(x, 0) is conservatively flagged match-start-dependent, causing a
harmless redundant re-evaluation.
---
 src/backend/executor/nodeWindowAgg.c    | 3 +++
 src/backend/optimizer/plan/createplan.c | 7 ++++++-
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 86b39bf7f61..95f4f06501e 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -1574,6 +1574,9 @@ release_partition(WindowAggState *winstate)
 	winstate->nfaLastProcessedRow = -1;
 	winstate->nfaStatesActive = 0;
 	winstate->nfaContextsActive = 0;
+
+	/* Invalidate the nav slot position cache for the new partition. */
+	winstate->nav_slot_pos = -1;
 }
 
 /*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c5b0857ff20..5608d71cbe2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2693,7 +2693,12 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 		}
 	}
 
-	/* Match-start dependency: classify the outer nav kind. */
+	/*
+	 * Match-start dependency: classify the outer nav kind.  A constant
+	 * LAST(x, 0) is conservatively included (offset_arg is a non-NULL Const),
+	 * causing a harmless extra re-evaluation; since LAST(x, 0) is the current
+	 * row, its result is independent of the match start.
+	 */
 	if (nav->kind == RPR_NAV_FIRST ||
 		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL) ||
 		nav->kind == RPR_NAV_PREV_FIRST ||
-- 
2.50.1 (Apple Git-155)

From a3ef40ae024cd1db787a1c19dc967dbe03bb28f2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 13:48:10 +0900
Subject: [PATCH 63/68] Tidy up formatting in row pattern recognition code

Reformat the first three row_pattern_quantifier_opt alternatives as
multiline blocks to match the rest of the rule, change the RF_* reduced
frame macros from a tab to a space after #define, add a trailing comma to
the last RPSkipTo enumerator, and move the RPRNav* entries to their correct
C-collation position in typedefs.list.  These are formatting and alignment
changes only, with no effect on behavior.
---
 src/backend/optimizer/plan/rpr.c |  2 +-
 src/backend/parser/gram.y        | 14 +++++++++++---
 src/include/nodes/execnodes.h    | 10 +++++-----
 src/include/nodes/parsenodes.h   |  2 +-
 src/tools/pgindent/typedefs.list |  6 +++---
 5 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index e7276c42ca6..143ea034fb3 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -484,7 +484,7 @@ mergeConsecutiveAlts(List *children)
  *
  * When a GROUP's children appear as a prefix before and/or suffix after
  * the GROUP in a SEQ, merge them by incrementing the GROUP's quantifier.
- * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
+ * This runs iteratively: A B A B (A B)+ A B -> (A B){4,}.
  *
  * Algorithm:
  *   For each GROUP encountered in the sequence:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 147b5f37293..e0799f35638 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17740,9 +17740,17 @@ row_pattern_primary:
 row_pattern_quantifier_opt:
 			/* EMPTY - no quantifier means exactly once; @$ is unused since
 			 * min=max=1 never produces an error */
-			{ $$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner); }
-			| '*'					{ $$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner); }
-			| '+'					{ $$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner); }
+				{
+					$$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner);
+				}
+			| '*'
+				{
+					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+				}
+			| '+'
+				{
+					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+				}
 			| Op
 				{
 					/* Handle single Op: ? or reluctant quantifiers *?, +?, ?? */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4641ed36cee..77d75aca91f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2528,11 +2528,11 @@ typedef enum WindowAggStatus
 } WindowAggStatus;
 
 /* RPR reduced frame states returned by get_reduced_frame_status() */
-#define	RF_NOT_DETERMINED	0	/* not yet processed */
-#define	RF_FRAME_HEAD		1	/* start row of a match */
-#define	RF_SKIPPED			2	/* interior row of a match */
-#define	RF_UNMATCHED		3	/* no match at this row */
-#define	RF_EMPTY_MATCH		4	/* empty match (0 rows); treated as unmatched */
+#define RF_NOT_DETERMINED	0	/* not yet processed */
+#define RF_FRAME_HEAD		1	/* start row of a match */
+#define RF_SKIPPED			2	/* interior row of a match */
+#define RF_UNMATCHED		3	/* no match at this row */
+#define RF_EMPTY_MATCH		4	/* empty match (0 rows); treated as unmatched */
 
 /*
  * RPRNFAState - single NFA state for pattern matching
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e371f04a403..9636a8efca1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -589,7 +589,7 @@ typedef enum RPSkipTo
 	ST_NONE,					/* no AFTER MATCH clause; default for non-RPR
 								 * windows */
 	ST_NEXT_ROW,				/* SKIP TO NEXT ROW */
-	ST_PAST_LAST_ROW			/* SKIP TO PAST LAST ROW */
+	ST_PAST_LAST_ROW,			/* SKIP TO PAST LAST ROW */
 } RPSkipTo;
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 24cf2eb7860..06cd541be5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2519,9 +2519,6 @@ QuerySource
 QueueBackendStatus
 QueuePosition
 QuitSignalReason
-RPRNavExpr
-RPRNavKind
-RPRNavOffsetKind
 RBTNode
 RBTOrderControl
 RBTree
@@ -2540,6 +2537,9 @@ RPRElemFlags
 RPRElemIdx
 RPRNFAContext
 RPRNFAState
+RPRNavExpr
+RPRNavKind
+RPRNavOffsetKind
 RPRPattern
 RPRPatternElement
 RPRPatternNode
-- 
2.50.1 (Apple Git-155)

From 4f04a1aefc4a5a041e1070f0cbae2f8fa9ff5e40 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 14:59:00 +0900
Subject: [PATCH 65/68] Use width-explicit integer limit macros in row pattern
 recognition

Replace INT_MAX/INT16_MAX/INT32_MAX/INT64_MAX/UINT8_MAX with the
PG_INT*_MAX/PG_UINT8_MAX equivalents, and type RPRPatternNode.min/max
as int32 to match the plan representation.  Value- and output-preserving.
---
 src/backend/commands/explain.c          |  2 +-
 src/backend/executor/execRPR.c          | 11 +++--
 src/backend/executor/nodeWindowAgg.c    | 16 +++----
 src/backend/optimizer/plan/createplan.c | 14 +++---
 src/backend/optimizer/plan/rpr.c        |  6 +--
 src/backend/parser/gram.y               | 64 ++++++++++++-------------
 src/backend/utils/adt/ruleutils.c       |  6 +--
 src/include/nodes/execnodes.h           |  2 +-
 src/include/nodes/parsenodes.h          |  4 +-
 src/include/optimizer/rpr.h             | 11 +++--
 10 files changed, 69 insertions(+), 67 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7992829d5c4..70fd7f386a0 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3270,7 +3270,7 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 											es);
 						break;
 					case RPR_NAV_OFFSET_FIXED:
-						if (firstOffset == INT64_MAX)
+						if (firstOffset == PG_INT64_MAX)
 							ExplainPropertyText("Nav Mark Lookahead", "infinite",
 												es);
 						else
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index b7e3c4a8274..1c89875a306 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1546,7 +1546,7 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 				   sizeof(bitmapword) *
 				   (winstate->nfaVisitedMaxWord -
 					winstate->nfaVisitedMinWord + 1));
-			winstate->nfaVisitedMinWord = INT16_MAX;
+			winstate->nfaVisitedMinWord = PG_INT16_MAX;
 			winstate->nfaVisitedMaxWord = -1;
 		}
 
@@ -1807,10 +1807,11 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 			int64		ctxFrameEnd;
 
 			/*
-			 * Clamp to INT64_MAX on overflow.  frameOffset can be as large as
-			 * PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the offset
-			 * and the trailing +1 in two separately checked steps to avoid
-			 * signed-integer overflow in the "frameOffset + 1" subexpression.
+			 * Clamp to PG_INT64_MAX on overflow.  frameOffset can be as large
+			 * as PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the
+			 * offset and the trailing +1 in two separately checked steps to
+			 * avoid signed-integer overflow in the "frameOffset + 1"
+			 * subexpression.
 			 */
 			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
 									&ctxFrameEnd) ||
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 95f4f06501e..13661181986 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3070,7 +3070,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
 											nfaVisitedNWords);
 		/* High-water mark sentinels: no bits set yet. */
-		winstate->nfaVisitedMinWord = INT16_MAX;
+		winstate->nfaVisitedMinWord = PG_INT16_MAX;
 		winstate->nfaVisitedMaxWord = -1;
 	}
 
@@ -4048,7 +4048,7 @@ typedef struct
  *			NEXT_LAST (= max(inner - outer, 0))
  *		  - minFirstOffset (forward reach from match_start): FIRST,
  *			compound PREV_FIRST (= inner - outer, may be negative),
- *			compound NEXT_FIRST (= inner + outer, clamped to INT64_MAX on
+ *			compound NEXT_FIRST (= inner + outer, clamped to PG_INT64_MAX on
  *			overflow; always >= 0 so never updates minFirstOffset in practice)
  *
  * Counterpart of visit_nav_plan but using runtime evaluation instead of
@@ -4137,7 +4137,7 @@ visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 		{
 			/*
 			 * reach = inner - outer.  Both are non-negative, so the result >=
-			 * -INT64_MAX, which cannot underflow int64.
+			 * -PG_INT64_MAX, which cannot underflow int64.
 			 */
 			reach = inner - outer;
 		}
@@ -4146,10 +4146,10 @@ visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 			/*
 			 * NEXT_FIRST: reach = inner + outer.  This can overflow, but the
 			 * result is always >= 0, so it never updates minFirstOffset
-			 * (which tracks the minimum).  Clamp to INT64_MAX on overflow.
+			 * (which tracks the minimum).  Clamp to PG_INT64_MAX on overflow.
 			 */
 			if (pg_add_s64_overflow(inner, outer, &reach))
-				reach = INT64_MAX;
+				reach = PG_INT64_MAX;
 		}
 		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
@@ -4184,7 +4184,7 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	ctx.winstate = winstate;
 	ctx.maxOffset = 0;
 	ctx.maxOverflow = false;
-	ctx.minFirstOffset = INT64_MAX;
+	ctx.minFirstOffset = PG_INT64_MAX;
 
 	trav.visit = visit_nav_exec;
 	trav.data = &ctx;
@@ -4213,10 +4213,10 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	if (needsFirst)
 	{
 		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		if (ctx.minFirstOffset < INT64_MAX)
+		if (ctx.minFirstOffset < PG_INT64_MAX)
 			winstate->navFirstOffset = ctx.minFirstOffset;
 		else
-			winstate->navFirstOffset = INT64_MAX;
+			winstate->navFirstOffset = PG_INT64_MAX;
 	}
 }
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 5608d71cbe2..d2e19d61d58 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2669,8 +2669,8 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 				{
 					/*
 					 * reach = inner - outer.  Both are non-negative, so the
-					 * result >= -INT64_MAX, which cannot underflow int64. No
-					 * overflow check needed.
+					 * result >= -PG_INT64_MAX, which cannot underflow int64.
+					 * No overflow check needed.
 					 */
 					reach = inner - outer;
 				}
@@ -2680,10 +2680,10 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 					 * NEXT_FIRST: reach = inner + outer.  This can overflow,
 					 * but the result is always >= 0, so it never updates
 					 * firstOffset (which tracks the minimum).  Clamp to
-					 * INT64_MAX on overflow.
+					 * PG_INT64_MAX on overflow.
 					 */
 					if (pg_add_s64_overflow(inner, outer, &reach))
-						reach = INT64_MAX;
+						reach = PG_INT64_MAX;
 				}
 
 				context->firstOffset = Min(context->firstOffset, reach);
@@ -2741,7 +2741,7 @@ compute_define_metadata(List *defineClause,
 	ctx.maxOffset = 0;
 	ctx.maxNeedsEval = false;
 	ctx.maxOverflow = false;
-	ctx.firstOffset = INT64_MAX;	/* sentinel: no FIRST found yet */
+	ctx.firstOffset = PG_INT64_MAX; /* sentinel: no FIRST found yet */
 	ctx.hasFirst = false;
 	ctx.firstNeedsEval = false;
 	ctx.curVarIdx = 0;
@@ -2789,8 +2789,8 @@ compute_define_metadata(List *defineClause,
 		else
 		{
 			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = ctx.firstOffset; /* may be negative; INT64_MAX if
-											 * overflowed */
+			*firstResult = ctx.firstOffset; /* may be negative; PG_INT64_MAX
+											 * if overflowed */
 		}
 	}
 	else
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 9364136b071..d768422deeb 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1210,7 +1210,7 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	elem->varId = getVarIdFromPattern(pat, node->varName);
 	elem->depth = depth;
 	elem->min = node->min;
-	elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+	elem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 	Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
 		   elem->max >= 1 &&
 		   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
@@ -1263,7 +1263,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		elem->varId = RPR_VARID_BEGIN;
 		elem->depth = depth;
 		elem->min = node->min;
-		elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		elem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
 			   elem->max >= 1 &&
 			   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
@@ -1291,7 +1291,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		endElem->varId = RPR_VARID_END;
 		endElem->depth = depth;
 		endElem->min = node->min;
-		endElem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		endElem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 		Assert(endElem->min >= 0 && endElem->min < RPR_QUANTITY_INF &&
 			   endElem->max >= 1 &&
 			   (endElem->max == RPR_QUANTITY_INF || endElem->min <= endElem->max));
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e0799f35638..ac2e5d7914a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -212,7 +212,7 @@ static void preprocess_pubobj_list(List *pubobjspec_list,
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 static RPRPatternNode *makeRPRSeqOrSingle(List *children, int location);
 static RPRPatternNode *splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner);
-static RPRPatternNode *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
+static RPRPatternNode *makeRPRQuantifier(int32 min, int32 max, ParseLoc reluctant, int location,
 									   core_yyscan_t yyscanner);
 
 %}
@@ -17745,11 +17745,11 @@ row_pattern_quantifier_opt:
 				}
 			| '*'
 				{
-					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| '+'
 				{
-					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| Op
 				{
@@ -17757,19 +17757,19 @@ row_pattern_quantifier_opt:
 					if (strcmp($1, "?") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, -1, @1, yyscanner);
 					else if (strcmp($1, "*?") == 0)
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "+?") == 0)
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "??") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "*|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, -1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "+|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, -1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "?|") == 0)
@@ -17779,12 +17779,12 @@ row_pattern_quantifier_opt:
 					}
 					else if (strcmp($1, "*?|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "+?|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "??|") == 0)
@@ -17803,11 +17803,11 @@ row_pattern_quantifier_opt:
 			| '*' Op
 				{
 					if (strcmp($2, "?") == 0)
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @2, @1, yyscanner);
 					else if (strcmp($2, "?|") == 0)
 					{
 						/* "A* ?|B" = reluctant "A*?" plus alternation */
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @2, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else
@@ -17820,11 +17820,11 @@ row_pattern_quantifier_opt:
 			| '+' Op
 				{
 					if (strcmp($2, "?") == 0)
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @2, @1, yyscanner);
 					else if (strcmp($2, "?|") == 0)
 					{
 						/* "A+ ?|B" = reluctant "A+?" plus alternation */
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @2, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else
@@ -17860,37 +17860,37 @@ row_pattern_quantifier_opt:
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
 			| '{' Iconst '}'
 				{
-					if ($2 <= 0 || $2 >= INT_MAX)
+					if ($2 <= 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, -1, @1, yyscanner);
 				}
 			| '{' Iconst ',' '}'
 				{
-					if ($2 < 0 || $2 >= INT_MAX)
+					if ($2 < 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 0 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier($2, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| '{' ',' Iconst '}'
 				{
-					if ($3 <= 0 || $3 >= INT_MAX)
+					if ($3 <= 0 || $3 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, -1, @1, yyscanner);
 				}
 			| '{' Iconst ',' Iconst '}'
 				{
-					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+					if ($2 < 0 || $4 <= 0 || $2 >= PG_INT32_MAX || $4 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+								errmsg("quantifier bounds must be between 0 and %d with max >= 1", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					if ($2 > $4)
 						ereport(ERROR,
@@ -17908,10 +17908,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n} to make it reluctant."),
 								parser_errposition(@4));
-					if ($2 <= 0 || $2 >= INT_MAX)
+					if ($2 <= 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
 					if (strcmp($4, "?|") == 0)
@@ -17925,12 +17925,12 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
 								parser_errposition(@5));
-					if ($2 < 0 || $2 >= INT_MAX)
+					if ($2 < 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 0 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier($2, PG_INT32_MAX, @5, @1, yyscanner);
 					if (strcmp($5, "?|") == 0)
 						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
@@ -17942,10 +17942,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
 								parser_errposition(@5));
-					if ($3 <= 0 || $3 >= INT_MAX)
+					if ($3 <= 0 || $3 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);
 					if (strcmp($5, "?|") == 0)
@@ -17959,10 +17959,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
 								parser_errposition(@6));
-					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+					if ($2 < 0 || $4 <= 0 || $2 >= PG_INT32_MAX || $4 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+								errmsg("quantifier bounds must be between 0 and %d with max >= 1", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					if ($2 > $4)
 						ereport(ERROR,
@@ -21400,7 +21400,7 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
  *		Create an RPRPatternNode with specified quantifier bounds.
  */
 static RPRPatternNode *
-makeRPRQuantifier(int min, int max, ParseLoc reluctant_location, int location,
+makeRPRQuantifier(int32 min, int32 max, ParseLoc reluctant_location, int location,
 				  core_yyscan_t yyscanner)
 {
 	RPRPatternNode *n = makeNode(RPRPatternNode);
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index e8087d4316a..d588cd8263d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7124,13 +7124,13 @@ append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
 		/* {1,1} = no quantifier */
 		has_quantifier = false;
 	}
-	else if (node->min == 0 && node->max == INT_MAX)
+	else if (node->min == 0 && node->max == PG_INT32_MAX)
 		appendStringInfoChar(buf, '*');
-	else if (node->min == 1 && node->max == INT_MAX)
+	else if (node->min == 1 && node->max == PG_INT32_MAX)
 		appendStringInfoChar(buf, '+');
 	else if (node->min == 0 && node->max == 1)
 		appendStringInfoChar(buf, '?');
-	else if (node->max == INT_MAX)
+	else if (node->max == PG_INT32_MAX)
 		appendStringInfo(buf, "{%d,}", node->min);
 	else if (node->min == node->max)
 		appendStringInfo(buf, "{%d}", node->min);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 77d75aca91f..dca0bbc3e30 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2672,7 +2672,7 @@ typedef struct WindowAggState
 	bitmapword *nfaVisitedElems;	/* elemIdx visited bitmap for cycle
 									 * detection */
 	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
-									 * last reset (INT16_MAX = none) */
+									 * last reset (PG_INT16_MAX = none) */
 	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
 									 * last reset (-1 = none) */
 	int64		nfaLastProcessedRow;	/* last row processed by NFA (-1 =
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9636a8efca1..16f31dbe7ff 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -625,8 +625,8 @@ typedef struct RPRPatternNode
 {
 	NodeTag		type;			/* T_RPRPatternNode */
 	RPRPatternNodeType nodeType;	/* VAR, SEQ, ALT, GROUP */
-	int			min;			/* minimum repetitions (0 for *, ?) */
-	int			max;			/* maximum repetitions (INT_MAX for *, +) */
+	int32		min;			/* minimum repetitions (0 for *, ?) */
+	int32		max;			/* maximum repetitions (PG_INT32_MAX for *, +) */
 	bool		reluctant;		/* true for reluctant (non-greedy) */
 	ParseLoc	reluctant_location; /* location of '?' token, or -1 */
 	ParseLoc	location;		/* token location, or -1 */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 73c827de2b1..8e0bc7efc53 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -26,12 +26,13 @@
  * before release.
  */
 #define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
-#define RPR_QUANTITY_INF	INT32_MAX	/* unbounded quantifier */
-#define RPR_COUNT_MAX		INT32_MAX	/* max runtime count (NFA state) */
-#define RPR_ELEMIDX_MAX		INT16_MAX	/* max pattern elements */
+#define RPR_QUANTITY_INF	PG_INT32_MAX	/* unbounded quantifier */
+#define RPR_COUNT_MAX		PG_INT32_MAX	/* max runtime count (NFA state) */
+#define RPR_ELEMIDX_MAX		PG_INT16_MAX	/* max pattern elements */
 #define RPR_ELEMIDX_INVALID	((RPRElemIdx) -1)	/* invalid index */
-#define RPR_DEPTH_MAX		(UINT8_MAX - 1) /* max pattern nesting depth: 254 */
-#define RPR_DEPTH_NONE		UINT8_MAX	/* no enclosing group (top-level) */
+#define RPR_DEPTH_MAX		(PG_UINT8_MAX - 1)	/* max pattern nesting depth:
+												 * 254 */
+#define RPR_DEPTH_NONE		PG_UINT8_MAX	/* no enclosing group (top-level) */
 
 /* Reserved control-element varIds (high nibble 0xF; 0xF0-0xFB spare) */
 #define RPR_VARID_BEGIN		((RPRVarId) 0xFC)	/* group begin */
-- 
2.50.1 (Apple Git-155)

From ad3e63eea24c4f80f8d18ef7b5cc40fff14e4828 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 13:59:43 +0900
Subject: [PATCH 64/68] Modernize idioms in row pattern recognition code

Replace appendStringInfo() with appendStringInfoString() for the eight
constant string literals that contain no format specifiers, avoiding an
unnecessary trip through vsnprintf().

Replace the four palloc(n * sizeof(type)) calls in _copyRPRPattern() and
_readRPRPattern() with palloc_array()/palloc0_array(), which make the
element type and count explicit and guard against size multiplication
overflow.

Replace an open-coded maximum in scanRPRPatternRecursive() with Max().

These are mechanical idiom changes only; behavior, output, and ABI are
unchanged, and the row pattern regression tests pass with no .out diffs.
---
 src/backend/commands/explain.c    | 15 ++++++++-------
 src/backend/nodes/copyfuncs.c     |  4 ++--
 src/backend/nodes/readfuncs.c     |  4 ++--
 src/backend/optimizer/plan/rpr.c  |  3 +--
 src/backend/utils/adt/ruleutils.c |  3 ++-
 5 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7ba0b6df849..7992829d5c4 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2932,7 +2932,8 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
 	if (RPRElemIsReluctant(elem))
 	{
 		if (elem->min == 1 && elem->max == 1)
-			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
+			appendStringInfoString(buf, "{1}"); /* make reluctant ?
+												 * unambiguous */
 		appendStringInfoChar(buf, '?');
 	}
 
@@ -3929,7 +3930,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 
 		/* Match/mismatch counts with length min/max/avg */
 		ExplainIndentText(es);
-		appendStringInfo(es->str, "NFA: ");
+		appendStringInfoString(es->str, "NFA: ");
 		if (winstate->nfaMatchesSucceeded > 0)
 		{
 			double		avgLen = (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded;
@@ -3943,7 +3944,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		}
 		else
 		{
-			appendStringInfo(es->str, "0 matched");
+			appendStringInfoString(es->str, "0 matched");
 		}
 		if (winstate->nfaMatchesFailed > 0)
 		{
@@ -3958,7 +3959,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		}
 		else
 		{
-			appendStringInfo(es->str, ", 0 mismatched");
+			appendStringInfoString(es->str, ", 0 mismatched");
 		}
 		appendStringInfoChar(es->str, '\n');
 
@@ -3966,7 +3967,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		if (winstate->nfaContextsAbsorbed > 0 || winstate->nfaContextsSkipped > 0)
 		{
 			ExplainIndentText(es);
-			appendStringInfo(es->str, "NFA: ");
+			appendStringInfoString(es->str, "NFA: ");
 
 			if (winstate->nfaContextsAbsorbed > 0)
 			{
@@ -3981,7 +3982,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 			}
 			else
 			{
-				appendStringInfo(es->str, "0 absorbed");
+				appendStringInfoString(es->str, "0 absorbed");
 			}
 
 			if (winstate->nfaContextsSkipped > 0)
@@ -3997,7 +3998,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 			}
 			else
 			{
-				appendStringInfo(es->str, ", 0 skipped");
+				appendStringInfoString(es->str, ", 0 skipped");
 			}
 
 			appendStringInfoChar(es->str, '\n');
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index e67ad39bdb8..17d45930d7b 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -178,13 +178,13 @@ _copyRPRPattern(const RPRPattern *from)
 
 	/* Deep copy the varNames array (DEFINE clause is required) */
 	Assert(from->numVars > 0);
-	newnode->varNames = palloc0(from->numVars * sizeof(char *));
+	newnode->varNames = palloc0_array(char *, from->numVars);
 	for (int i = 0; i < from->numVars; i++)
 		newnode->varNames[i] = pstrdup(from->varNames[i]);
 
 	/* Deep copy the elements array (always has at least one element + FIN) */
 	Assert(from->numElements >= 2);
-	newnode->elements = palloc(from->numElements * sizeof(RPRPatternElement));
+	newnode->elements = palloc_array(RPRPatternElement, from->numElements);
 	memcpy(newnode->elements, from->elements,
 		   from->numElements * sizeof(RPRPatternElement));
 
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 5bbde5bcad2..6c39c6fe06d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -582,7 +582,7 @@ _readRPRPattern(void)
 	token = pg_strtok(&length); /* get '(' or '<>' */
 	if (local_node->numVars > 0 && token[0] == '(')
 	{
-		local_node->varNames = palloc(local_node->numVars * sizeof(char *));
+		local_node->varNames = palloc_array(char *, local_node->numVars);
 		for (int i = 0; i < local_node->numVars; i++)
 		{
 			token = pg_strtok(&length);
@@ -600,7 +600,7 @@ _readRPRPattern(void)
 	token = pg_strtok(&length); /* get '(' or '<>' */
 	if (local_node->numElements > 0 && token[0] == '(')
 	{
-		local_node->elements = palloc0(local_node->numElements * sizeof(RPRPatternElement));
+		local_node->elements = palloc0_array(RPRPatternElement, local_node->numElements);
 		for (int i = 0; i < local_node->numElements; i++)
 		{
 			RPRPatternElement *elem = &local_node->elements[i];
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 143ea034fb3..9364136b071 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1048,8 +1048,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 						  depth, RPR_DEPTH_MAX - 1));
 
 	/* Track maximum depth */
-	if (depth > *maxDepth)
-		*maxDepth = depth;
+	*maxDepth = Max(*maxDepth, depth);
 
 	switch (node->nodeType)
 	{
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2b8439e452e..e8087d4316a 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7140,7 +7140,8 @@ append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
 	if (node->reluctant)
 	{
 		if (!has_quantifier)
-			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
+			appendStringInfoString(buf, "{1}"); /* make reluctant ?
+												 * unambiguous */
 		appendStringInfoChar(buf, '?');
 	}
 }
-- 
2.50.1 (Apple Git-155)

From 033b6d0bd42a41b2501d34fb413cb889caffa366 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 16:34:09 +0900
Subject: [PATCH 67/68] Add row pattern recognition negative and coverage tests

Cover the split-token error paths after a quantifier (* / + / Op and
the {n}/{n,}/{,m}/{n,m} range forms), rejection of set-returning and
window functions in a DEFINE clause, and the reluctant-GROUP case where
quantifier multiplication is correctly suppressed.  Tests only.
---
 src/test/regress/expected/rpr_base.out | 91 ++++++++++++++++++++++++--
 src/test/regress/sql/rpr_base.sql      | 37 +++++++++--
 2 files changed, 120 insertions(+), 8 deletions(-)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index f1767305d06..2407c455164 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3283,7 +3283,48 @@ ERROR:  unsupported quantifier "+!"
 LINE 6:     PATTERN (A+!)
                       ^
 HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
--- Expected: Syntax error
+-- none of the following 4 queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE A AS TRUE);
+ERROR:  invalid token after "+" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE ...
+                                                             ^
+HINT:  Did you mean "+?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE A AS TRUE);
+ERROR:  invalid token after "+" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE...
+                                                             ^
+HINT:  Did you mean "+?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE A AS TRUE);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE A AS TRUE);
+ERROR:  invalid quantifier combination
+LINE 1: ...OWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFI...
+                                                             ^
+HINT:  Did you mean "??" for reluctant quantifier?
+-- none of the following 4 range-quantifier queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...ETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,} or {,m} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...ETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,} or {,m} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...TWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,m} to make it reluctant.
 -- Unmatched parentheses
 SET client_min_messages = NOTICE;
 DO $$
@@ -3310,7 +3351,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near ")"
 LINE 8: );
         ^
--- Expected: Syntax error
 -- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3323,7 +3363,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near ")"
 LINE 6:     PATTERN ()
                      ^
--- Expected: Syntax error
 -- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3335,7 +3374,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "DEFINE"
 LINE 6:     DEFINE A AS val > 0
             ^
--- Expected: Syntax error
 -- Qualified column references (NOT SUPPORTED)
 -- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
@@ -3484,6 +3522,18 @@ WINDOW w AS (
 ERROR:  aggregate functions are not allowed in DEFINE
 LINE 7:     DEFINE A AS COUNT(*) > 0
                         ^
+-- ERROR: set-returning function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_series(1 ,2));
+ERROR:  set-returning functions are not allowed in DEFINE
+LINE 2: ... ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_s...
+                                                             ^
+-- ERROR: window function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number() OVER ());
+ERROR:  window functions are not allowed in DEFINE
+LINE 2: ... ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number...
+                                                             ^
 -- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3925,6 +3975,39 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested
+-- a reluctant quantifier on a GROUP is not subject to multiplication
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}?){3}) DEFINE A AS val > 0, B AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a b){2}?){3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier multiply control: greedy GROUP (((A B){2}){3}) -> (a b){6}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}){3}) DEFINE A AS val > 0, B AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b){6}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
 -- outer exact, child range - optimization applies
 EXPLAIN (COSTS OFF)
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 7dc13232d89..cec7ea1e8db 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2145,7 +2145,18 @@ WINDOW w AS (
     PATTERN (A+!)
     DEFINE A AS val > 0
 );
--- Expected: Syntax error
+
+-- none of the following 4 queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE A AS TRUE);
+
+-- none of the following 4 range-quantifier queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE A AS TRUE);
 
 -- Unmatched parentheses
 SET client_min_messages = NOTICE;
@@ -2170,7 +2181,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE
 );
--- Expected: Syntax error
 
 -- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
@@ -2181,7 +2191,6 @@ WINDOW w AS (
     PATTERN ()
     DEFINE A AS val > 0
 );
--- Expected: Syntax error
 
 -- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
@@ -2191,7 +2200,6 @@ WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     DEFINE A AS val > 0
 );
--- Expected: Syntax error

@@ -2315,6 +2323,14 @@ WINDOW w AS (
     DEFINE A AS COUNT(*) > 0
 );
 
+-- ERROR: set-returning function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_series(1 ,2));
+
+-- ERROR: window function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number() OVER ());
+
 -- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2549,6 +2565,19 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2}){3}) DEFINE A AS val > 0);
 
+-- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested
+-- a reluctant quantifier on a GROUP is not subject to multiplication
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}?){3}) DEFINE A AS val > 0, B AS val > 0);
+
+-- Quantifier multiply control: greedy GROUP (((A B){2}){3}) -> (a b){6}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}){3}) DEFINE A AS val > 0, B AS val > 0);
+
 -- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
 -- outer exact, child range - optimization applies
 EXPLAIN (COSTS OFF)
-- 
2.50.1 (Apple Git-155)

From 5221784ac469bb213fae2853beab7f5b2b4562cd Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 16:22:16 +0900
Subject: [PATCH 66/68] Tidy up row pattern recognition regression test
 comments

Drop the "-- Expected: ERROR: ..." comments that repeat exact error text
(they go stale; the .out already has the real error) and reword comments
that embedded literal limits.  Rename the "Jacob's Patterns" section and
clarify the serialization section's deparse/re-parse round-trip intent.

Test comments only; rpr_base.out regenerated, no output changes.
---
 src/test/regress/expected/rpr_base.out | 169 +++++++----------------
 src/test/regress/sql/rpr_base.sql      | 184 +++++++------------------
 2 files changed, 102 insertions(+), 251 deletions(-)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 1fcb2ce22f0..f1767305d06 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -12,7 +12,7 @@
 --   Quantifiers Tests
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
---   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Serialization/Deserialization Tests
 --   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
@@ -34,7 +34,7 @@
 --   Error Limit Tests
 --
 -- Contributed Tests:
---   Jacob's Patterns
+--   Basic Pattern Matching
 --   Pathological Patterns
 -- ============================================================
 SET client_min_messages = WARNING;
@@ -50,6 +50,7 @@ CREATE TABLE rpr_keywords (
     past INT,        -- PAST keyword
     pattern INT,     -- PATTERN keyword
     seek INT,        -- SEEK keyword
+-- ERROR: SEEK is not supported
     skip INT         -- SKIP keyword (pre-existing)
 );
 INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60);
@@ -224,7 +225,7 @@ DROP TABLE rpr_auto;
 -- Duplicate variable names
 CREATE TABLE rpr_dup (id INT);
 INSERT INTO rpr_dup VALUES (1), (2);
--- Duplicate DEFINE entries
+-- Duplicate DEFINE variable name is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_dup
 WINDOW w AS (
@@ -236,12 +237,11 @@ WINDOW w AS (
 ERROR:  DEFINE variable "a" appears more than once
 LINE 7:     DEFINE A AS id > 0, A AS id < 10
                    ^
--- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
 DROP TABLE rpr_dup;
 -- Boolean coercion
 CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
 INSERT INTO rpr_bool VALUES (1, true), (2, false);
--- Non-boolean expression
+-- DEFINE clause must be a boolean expression
 SELECT COUNT(*) OVER w
 FROM rpr_bool
 WINDOW w AS (
@@ -253,7 +253,6 @@ WINDOW w AS (
 ERROR:  argument of DEFINE must be type boolean, not type integer
 LINE 7:     DEFINE A AS id
                         ^
--- Expected: ERROR: argument of DEFINE must be type boolean
 -- Boolean column reference
 SELECT id, flag, COUNT(*) OVER w as cnt
 FROM rpr_bool
@@ -393,8 +392,7 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
--- Invalid frame start positions
--- Not starting at CURRENT ROW
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -408,7 +406,6 @@ LINE 5:     ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 -- EXCLUDE options
 -- EXCLUDE not permitted
 SELECT COUNT(*) OVER w
@@ -425,7 +422,6 @@ LINE 6:     EXCLUDE CURRENT ROW
             ^
 DETAIL:  Frame definition includes EXCLUDE CURRENT ROW.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- EXCLUDE GROUP not permitted
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -441,7 +437,6 @@ LINE 6:     EXCLUDE GROUP
             ^
 DETAIL:  Frame definition includes EXCLUDE GROUP.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- EXCLUDE TIES not permitted
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -457,8 +452,7 @@ LINE 6:     EXCLUDE TIES
             ^
 DETAIL:  Frame definition includes EXCLUDE TIES.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
--- RANGE frame not starting at CURRENT ROW
+-- range frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -471,8 +465,7 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 5:     RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
--- GROUPS frame not starting at CURRENT ROW
+-- GROUPS frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -485,8 +478,7 @@ ERROR:  cannot use FRAME option GROUPS with row pattern recognition
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
--- Starting with N PRECEDING
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -500,8 +492,7 @@ LINE 5:     ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with offset PRECEDING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
--- Starting with N FOLLOWING
+-- ERROR: frame must start at current row with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -515,9 +506,7 @@ LINE 5:     ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with offset FOLLOWING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
--- Frame end bound edge cases
--- End before start: CURRENT ROW AND 1 PRECEDING
+-- ERROR: end before start: CURRENT ROW AND 1 PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -529,8 +518,7 @@ WINDOW w AS (
 ERROR:  frame starting from current row cannot have preceding rows
 LINE 5:     ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
                                          ^
--- Expected: ERROR: frame starting from current row cannot have preceding rows
--- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
+-- ERROR: end before start: CURRENT ROW AND UNBOUNDED PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -542,7 +530,6 @@ WINDOW w AS (
 ERROR:  frame end cannot be UNBOUNDED PRECEDING
 LINE 5:     ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
                                          ^
--- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 -- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
 -- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -559,7 +546,6 @@ ERROR:  cannot use CURRENT ROW as frame end with row pattern recognition
 LINE 5:     ROWS BETWEEN CURRENT ROW AND CURRENT ROW
             ^
 HINT:  Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING.
--- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 -- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
 -- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -573,7 +559,6 @@ WINDOW w AS (
 )
 ORDER BY id;
 ERROR:  frame ending offset must be positive with row pattern recognition
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 -- A non-constant frame end offset is allowed; a zero value is still rejected,
 -- this time at execution time (a literal cannot exercise that path).
 PREPARE rpr_end_offset(int8) AS
@@ -600,7 +585,6 @@ EXECUTE rpr_end_offset(2);
 
 EXECUTE rpr_end_offset(0);
 ERROR:  frame ending offset must be positive with row pattern recognition
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 DEALLOCATE rpr_end_offset;
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -669,7 +653,7 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
--- RANGE frame with RPR (not permitted)
+-- range frame is not allowed with RPR
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -684,7 +668,6 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 5:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -700,7 +683,6 @@ ERROR:  cannot use FRAME option GROUPS with row pattern recognition
 LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 DROP TABLE rpr_frame;
 -- ============================================================
 -- PARTITION BY + FRAME Tests
@@ -749,7 +731,6 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 6:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 DROP TABLE rpr_partition;
 -- ============================================================
 -- PATTERN Syntax Tests
@@ -1011,7 +992,6 @@ ORDER BY id;
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{0} B)
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 -- {0,0} is not allowed (max must be >= 1)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_quant
@@ -1025,7 +1005,6 @@ ORDER BY id;
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN (A{0,0} B)
                        ^
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 -- {0,1} (equivalent to ?)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_quant
@@ -1155,6 +1134,7 @@ DROP TABLE rpr_quant;
 CREATE TABLE rpr_reluctant (id INT, val INT);
 INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
 -- *? (zero or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1170,8 +1150,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- +? (one or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1187,8 +1167,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- ?? (zero or one, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1204,8 +1184,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n,}? (n or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1221,8 +1201,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n,m}? (n to m, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1238,8 +1218,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n}? (exactly n, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1255,8 +1235,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1272,8 +1252,6 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
--- Invalid reluctant patterns (wrong token after quantifier)
 -- {2}+ (should be {2}? not {2}+)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1286,7 +1264,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A{2}+)
                          ^
--- Expected: ERROR: syntax error at or near "+"
 -- {2,}* (should be {2,}? not {2,}*)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1299,7 +1276,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A{2,}*)
                           ^
--- Expected: ERROR: syntax error at or near "*"
 -- {,3}* (should be {,3}? not {,3}*)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1312,7 +1288,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A{,3}*)
                           ^
--- Expected: ERROR: syntax error at or near "*"
 -- {1,3}+ (should be {1,3}? not {1,3}+)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1325,9 +1300,8 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A{1,3}+)
                            ^
--- Expected: ERROR: syntax error at or near "+"
 -- Boundary errors in reluctant quantifiers
--- {-1}? (negative bound)
+-- negative bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1339,8 +1313,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {2147483647}? (INT_MAX)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1352,8 +1325,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{2147483647}?)
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {-1,}? (negative lower bound)
+-- negative lower bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1365,8 +1337,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {2147483647,}? (INT_MAX lower bound)
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1378,8 +1349,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 0 and 2147483646
 LINE 6:     PATTERN (A{2147483647,}?)
                        ^
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
--- {,0}? (zero upper bound)
+-- zero upper bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1391,8 +1361,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,0}?)
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {,2147483647}? (INT_MAX upper bound)
+-- ERROR: {,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1404,8 +1373,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,2147483647}?)
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {-1,3}? (negative lower in range)
+-- ERROR: {-1,3}? (negative lower bound in range is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1417,8 +1385,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,3}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {1,2147483647}? (INT_MAX upper in range)
+-- ERROR: {1,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1430,8 +1397,7 @@ WINDOW w AS (
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN (A{1,2147483647}?)
                        ^
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
--- {5,3}? (min > max)
+-- ERROR: {5,3}? (min > max is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1443,10 +1409,10 @@ WINDOW w AS (
 ERROR:  quantifier minimum bound must not exceed maximum
 LINE 6:     PATTERN (A{5,3}?)
                        ^
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 -- Token-separated reluctant quantifiers (space between quantifier and ?)
 -- These may be tokenized differently by the lexer
 -- * ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1462,8 +1428,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- + ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1479,8 +1445,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {2,} ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1496,8 +1462,6 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
--- Invalid token combinations
 -- * + (invalid combination)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1510,7 +1474,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A* +)
                         ^
--- Expected: ERROR: syntax error at or near "+"
 -- + * (invalid combination)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1523,8 +1486,8 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A+ *)
                         ^
--- Expected: ERROR: syntax error at or near "*"
 -- ? ? (parsed as ?? reluctant quantifier)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1540,12 +1503,11 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 DROP TABLE rpr_reluctant;
 -- Quantifier boundary conditions
 CREATE TABLE rpr_bounds (id INT);
 INSERT INTO rpr_bounds VALUES (1), (2);
--- min > max
+-- ERROR: quantifier lower bound must not exceed upper bound
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1557,7 +1519,6 @@ WINDOW w AS (
 ERROR:  quantifier minimum bound must not exceed maximum
 LINE 6:     PATTERN (A{5,3})
                        ^
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 -- Large bounds
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
@@ -1603,7 +1564,7 @@ WINDOW w AS (
      0
 (2 rows)
 
--- INT_MAX = 2147483647 (over limit)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1615,9 +1576,8 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{2147483647})
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 -- {n,} boundary errors
--- Negative lower bound in {n,}
+-- ERROR: negative lower bound in {n,} is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1629,8 +1589,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,})
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- INT_MAX in {n,}
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1642,7 +1601,6 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 0 and 2147483646
 LINE 6:     PATTERN (A{2147483647,})
                        ^
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 -- {,m} boundary errors
 -- Zero upper bound in {,m}
 SELECT COUNT(*) OVER w
@@ -1656,8 +1614,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,0})
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- INT_MAX in {,m}
+-- ERROR: quantifier upper bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1669,7 +1626,6 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,2147483647})
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 DROP TABLE rpr_bounds;
 -- ============================================================
 -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
@@ -1756,7 +1712,6 @@ ORDER BY id;
 ERROR:  cannot use prev outside a DEFINE clause
 LINE 1: SELECT PREV(id), id, val, COUNT(*) OVER w as cnt
                ^
--- Expected: ERROR: cannot use prev outside a DEFINE clause
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1772,7 +1727,6 @@ ORDER BY id;
 ERROR:  cannot use next outside a DEFINE clause
 LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
                ^
--- Expected: ERROR: cannot use next outside a DEFINE clause
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1841,13 +1795,11 @@ SELECT FIRST(id), id, val FROM rpr_nav;
 ERROR:  cannot use first outside a DEFINE clause
 LINE 1: SELECT FIRST(id), id, val FROM rpr_nav;
                ^
--- Expected: ERROR: cannot use first outside a DEFINE clause
 -- 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
 LINE 1: SELECT LAST(id), id, val FROM rpr_nav;
                ^
--- Expected: ERROR: cannot use last outside a DEFINE clause
 DROP TABLE rpr_nav;
 -- ============================================================
 -- SKIP TO / INITIAL Tests
@@ -2033,12 +1985,13 @@ ERROR:  SEEK is not supported
 LINE 6:     SEEK
             ^
 HINT:  Use INITIAL instead.
--- Expected: ERROR: SEEK is not supported
--- HINT: Use INITIAL instead.
 DROP TABLE rpr_seek;
 -- ============================================================
 -- Serialization/Deserialization Tests
 -- ============================================================
+-- RPR-defining views and tables here are intentionally left in place (not
+-- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse
+-- round-trip of the RPR window clause.
 -- View creation and deparsing
 CREATE TABLE rpr_serial (id INT, val INT);
 INSERT INTO rpr_serial VALUES
@@ -3317,7 +3270,6 @@ DROP TABLE rpr_glue;
 DROP TABLE IF EXISTS rpr_err;
 CREATE TABLE rpr_err (id INT, val INT);
 INSERT INTO rpr_err VALUES (1, 10), (2, 20);
--- Syntax errors
 -- Invalid quantifier syntax
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3346,7 +3298,7 @@ EXCEPTION
 END $$;
 NOTICE:  Unmatched parentheses: EXPECTED ERROR - syntax error at or near "AS"
 SET client_min_messages = WARNING;
--- Empty DEFINE
+-- ERROR: empty DEFINE not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3359,7 +3311,7 @@ ERROR:  syntax error at or near ")"
 LINE 8: );
         ^
 -- Expected: Syntax error
--- Empty PATTERN
+-- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3372,7 +3324,7 @@ ERROR:  syntax error at or near ")"
 LINE 6:     PATTERN ()
                      ^
 -- Expected: Syntax error
--- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+-- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3397,7 +3349,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "a.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS A.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3410,7 +3361,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "b.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS B.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3423,7 +3373,6 @@ WINDOW w AS (
 ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3436,7 +3385,6 @@ WINDOW w AS (
 ERROR:  range variable qualified expression "rpr_err.val" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS rpr_err.val > 0
                         ^
--- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
 -- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
 -- must fall through so that normal column resolution produces a sensible error.
 SELECT COUNT(*) OVER w
@@ -3450,7 +3398,6 @@ WINDOW w AS (
 ERROR:  missing FROM-clause entry for table "nosuch"
 LINE 7:     DEFINE A AS nosuch.val > 0
                         ^
--- Expected: ERROR: missing FROM-clause entry for table "nosuch"
 -- Unqualified composite field access in DEFINE works: no qualifier means no
 -- pattern/range-var navigation, so the pre-check skips and normal resolution
 -- handles "(items).amount" via A_Indirection on the current row.
@@ -3488,7 +3435,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "a.items" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS (A.items).amount > 10
                          ^
--- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_composite
 WINDOW w AS (
@@ -3500,11 +3446,9 @@ WINDOW w AS (
 ERROR:  range variable qualified expression "rpr_composite.items" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS (rpr_composite.items).amount > 10
                          ^
--- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
 DROP TABLE rpr_composite;
 DROP TYPE rpr_item;
--- Semantic errors
--- Undefined column in DEFINE
+-- ERROR: undefined column in DEFINE
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3516,8 +3460,7 @@ WINDOW w AS (
 ERROR:  column "nonexistent_column" does not exist
 LINE 7:     DEFINE A AS nonexistent_column > 0
                         ^
--- Expected: ERROR: column "nonexistent_column" does not exist
--- Type mismatch
+-- ERROR: type mismatch
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3529,8 +3472,7 @@ WINDOW w AS (
 ERROR:  invalid input syntax for type integer: "string"
 LINE 7:     DEFINE A AS val > 'string'
                               ^
--- Expected: ERROR: invalid input syntax for type integer: "string"
--- Aggregate function in DEFINE (if not allowed)
+-- ERROR: aggregate function in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3542,8 +3484,7 @@ WINDOW w AS (
 ERROR:  aggregate functions are not allowed in DEFINE
 LINE 7:     DEFINE A AS COUNT(*) > 0
                         ^
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
--- Subquery in DEFINE (NOT SUPPORTED)
+-- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3555,8 +3496,6 @@ WINDOW w AS (
 ERROR:  cannot use subquery in DEFINE expression
 LINE 7:     DEFINE A AS val > (SELECT max(val) FROM rpr_err)
                               ^
--- Expected: ERROR: cannot use subquery in DEFINE expression
--- Edge cases
 -- Pattern variable not used (should work, extra vars ignored)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_err
@@ -5703,7 +5642,6 @@ WINDOW w AS (
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN ((A{2000000000,2147483647}){2})
                         ^
--- Expected: ERROR at parse time before optimization
 -- Test: nested unbounded with large min causes overflow fallback
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
@@ -5947,7 +5885,6 @@ ORDER BY category;
 ERROR:  syntax error at or near "GROUP"
 LINE 12: GROUP BY category
          ^
--- Expected: ERROR: syntax error at or near "GROUP"
 -- (GROUP BY after WINDOW clause is not valid SQL syntax)
 -- ============================================================
 -- Subquery and CTE Tests
@@ -6427,7 +6364,6 @@ INSERT INTO rpr_sort VALUES
     (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
 -- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
        MAX(val) as max_val,
@@ -6445,7 +6381,6 @@ ERROR:  aggregate functions are not allowed in DEFINE
 LINE 11:     DEFINE A AS COUNT(*) > 0
                          ^
 -- RPR with HAVING (same aggregate-in-DEFINE error)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
        COUNT(*) OVER w as window_cnt
@@ -6758,7 +6693,7 @@ WINDOW w AS (
 (2 rows)
 
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
--- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
+-- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -6794,7 +6729,6 @@ ERROR:  too many pattern variables
 LINE 5: ...V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
                                                                   ^
 DETAIL:  Maximum is 240.
--- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
 -- subject to quantifier multiplication, so the nesting (and depth 253) is
@@ -6826,12 +6760,10 @@ WINDOW w AS (
 );
 ERROR:  pattern nesting too deep
 DETAIL:  Pattern nesting depth 254 exceeds maximum 253.
--- Expected: ERROR - pattern nesting too deep
 DROP TABLE rpr_errors;
 -- ============================================================
--- Jacob's Patterns
+-- Basic Pattern Matching
 -- ============================================================
--- Basic pattern matching tests from jacob branch
 -- Test: A? (optional, greedy)
 SELECT id, val, count(*) OVER w AS c
 FROM rpr_plan
@@ -7194,3 +7126,4 @@ FROM (SELECT id, val,
 (2 rows)
 
 DROP TABLE rpr_plan;
+RESET client_min_messages;
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index cc79843aeb7..7dc13232d89 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -12,7 +12,7 @@
 --   Quantifiers Tests
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
---   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Serialization/Deserialization Tests
 --   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
@@ -34,7 +34,7 @@
 --   Error Limit Tests
 --
 -- Contributed Tests:
---   Jacob's Patterns
+--   Basic Pattern Matching
 --   Pathological Patterns
 -- ============================================================
 
@@ -54,6 +54,7 @@ CREATE TABLE rpr_keywords (
     past INT,        -- PAST keyword
     pattern INT,     -- PATTERN keyword
     seek INT,        -- SEEK keyword
+-- ERROR: SEEK is not supported
     skip INT         -- SKIP keyword (pre-existing)
 );
 
@@ -69,7 +70,6 @@ DROP TABLE rpr_keywords;
 -- DEFINE Clause Tests
 -- ============================================================
 
-
 -- Simple column references
 CREATE TABLE stock_price (
     dt DATE,
@@ -182,7 +182,7 @@ DROP TABLE rpr_auto;
 CREATE TABLE rpr_dup (id INT);
 INSERT INTO rpr_dup VALUES (1), (2);
 
--- Duplicate DEFINE entries
+-- Duplicate DEFINE variable name is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_dup
 WINDOW w AS (
@@ -191,7 +191,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS id > 0, A AS id < 10
 );
--- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
 
 DROP TABLE rpr_dup;
 
@@ -199,7 +198,7 @@ DROP TABLE rpr_dup;
 CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
 INSERT INTO rpr_bool VALUES (1, true), (2, false);
 
--- Non-boolean expression
+-- DEFINE clause must be a boolean expression
 SELECT COUNT(*) OVER w
 FROM rpr_bool
 WINDOW w AS (
@@ -208,7 +207,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS id
 );
--- Expected: ERROR: argument of DEFINE must be type boolean
 
 -- Boolean column reference
 SELECT id, flag, COUNT(*) OVER w as cnt
@@ -301,7 +299,6 @@ DROP TABLE rpr_unused;
 -- FRAME Options Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_frame (id INT, val INT);
 INSERT INTO rpr_frame VALUES
     (1, 10), (2, 10), (3, 10),  -- Same val: 10
@@ -327,9 +324,7 @@ WINDOW w AS (
 )
 ORDER BY id;
 
--- Invalid frame start positions
-
--- Not starting at CURRENT ROW
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -338,7 +333,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
 -- EXCLUDE options
 
@@ -352,7 +346,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- EXCLUDE GROUP not permitted
 SELECT COUNT(*) OVER w
@@ -364,7 +357,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- EXCLUDE TIES not permitted
 SELECT COUNT(*) OVER w
@@ -376,9 +368,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
--- RANGE frame not starting at CURRENT ROW
+-- range frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -387,9 +378,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
--- GROUPS frame not starting at CURRENT ROW
+-- GROUPS frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -398,9 +388,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 
--- Starting with N PRECEDING
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -409,9 +398,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
--- Starting with N FOLLOWING
+-- ERROR: frame must start at current row with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -420,11 +408,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
--- Frame end bound edge cases
-
--- End before start: CURRENT ROW AND 1 PRECEDING
+-- ERROR: end before start: CURRENT ROW AND 1 PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -433,9 +418,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: frame starting from current row cannot have preceding rows
 
--- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
+-- ERROR: end before start: CURRENT ROW AND UNBOUNDED PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -444,7 +428,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 
 -- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
 -- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
@@ -458,7 +441,6 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 
 -- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
 -- and is likewise rejected (caught at execution time).
@@ -472,7 +454,6 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 
 -- A non-constant frame end offset is allowed; a zero value is still rejected,
 -- this time at execution time (a literal cannot exercise that path).
@@ -489,7 +470,6 @@ WINDOW w AS (
 ORDER BY id;
 EXECUTE rpr_end_offset(2);
 EXECUTE rpr_end_offset(0);
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 DEALLOCATE rpr_end_offset;
 
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
@@ -532,7 +512,7 @@ WINDOW w AS (
 )
 ORDER BY id;
 
--- RANGE frame with RPR (not permitted)
+-- range frame is not allowed with RPR
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -543,7 +523,6 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -556,7 +535,6 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 
 DROP TABLE rpr_frame;
 
@@ -596,7 +574,6 @@ WINDOW w AS (
     DEFINE A AS val >= 10, B AS val >= 20
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition

@@ -604,7 +581,6 @@ DROP TABLE rpr_partition;
 -- PATTERN Syntax Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_pattern (id INT, val INT);
 INSERT INTO rpr_pattern VALUES
     (1, 5), (2, 10), (3, 15), (4, 20), (5, 25),
@@ -700,7 +676,6 @@ DROP TABLE rpr_pattern;
 -- Quantifiers Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_quant (id INT, val INT);
 INSERT INTO rpr_quant VALUES
     (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
@@ -753,7 +728,6 @@ WINDOW w AS (
     DEFINE A AS val > 1000, B AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 -- {0,0} is not allowed (max must be >= 1)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -765,7 +739,6 @@ WINDOW w AS (
     DEFINE A AS val > 1000, B AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 
 -- {0,1} (equivalent to ?)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -837,6 +810,7 @@ CREATE TABLE rpr_reluctant (id INT, val INT);
 INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
 
 -- *? (zero or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -845,9 +819,9 @@ WINDOW w AS (
     PATTERN (A*?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- +? (one or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -856,9 +830,9 @@ WINDOW w AS (
     PATTERN (A+?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- ?? (zero or one, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -867,9 +841,9 @@ WINDOW w AS (
     PATTERN (A??)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n,}? (n or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -878,9 +852,9 @@ WINDOW w AS (
     PATTERN (A{2,}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n,m}? (n to m, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -889,9 +863,9 @@ WINDOW w AS (
     PATTERN (A{1,3}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n}? (exactly n, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -900,9 +874,9 @@ WINDOW w AS (
     PATTERN (A{2}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -911,9 +885,6 @@ WINDOW w AS (
     PATTERN (A{,3}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
-
--- Invalid reluctant patterns (wrong token after quantifier)
 
 -- {2}+ (should be {2}? not {2}+)
 SELECT COUNT(*) OVER w
@@ -924,7 +895,6 @@ WINDOW w AS (
     PATTERN (A{2}+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- {2,}* (should be {2,}? not {2,}*)
 SELECT COUNT(*) OVER w
@@ -935,7 +905,6 @@ WINDOW w AS (
     PATTERN (A{2,}*)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- {,3}* (should be {,3}? not {,3}*)
 SELECT COUNT(*) OVER w
@@ -946,7 +915,6 @@ WINDOW w AS (
     PATTERN (A{,3}*)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- {1,3}+ (should be {1,3}? not {1,3}+)
 SELECT COUNT(*) OVER w
@@ -957,11 +925,10 @@ WINDOW w AS (
     PATTERN (A{1,3}+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- Boundary errors in reluctant quantifiers
 
--- {-1}? (negative bound)
+-- negative bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -970,9 +937,8 @@ WINDOW w AS (
     PATTERN (A{-1}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {2147483647}? (INT_MAX)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -981,9 +947,8 @@ WINDOW w AS (
     PATTERN (A{2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {-1,}? (negative lower bound)
+-- negative lower bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -992,9 +957,8 @@ WINDOW w AS (
     PATTERN (A{-1,}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {2147483647,}? (INT_MAX lower bound)
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1003,9 +967,8 @@ WINDOW w AS (
     PATTERN (A{2147483647,}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 
--- {,0}? (zero upper bound)
+-- zero upper bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1014,9 +977,8 @@ WINDOW w AS (
     PATTERN (A{,0}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {,2147483647}? (INT_MAX upper bound)
+-- ERROR: {,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1025,9 +987,8 @@ WINDOW w AS (
     PATTERN (A{,2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {-1,3}? (negative lower in range)
+-- ERROR: {-1,3}? (negative lower bound in range is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1036,9 +997,8 @@ WINDOW w AS (
     PATTERN (A{-1,3}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {1,2147483647}? (INT_MAX upper in range)
+-- ERROR: {1,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1047,9 +1007,8 @@ WINDOW w AS (
     PATTERN (A{1,2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 
--- {5,3}? (min > max)
+-- ERROR: {5,3}? (min > max is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1058,12 +1017,12 @@ WINDOW w AS (
     PATTERN (A{5,3}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 
 -- Token-separated reluctant quantifiers (space between quantifier and ?)
 -- These may be tokenized differently by the lexer
 
 -- * ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1072,9 +1031,9 @@ WINDOW w AS (
     PATTERN (A* ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- + ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1083,9 +1042,9 @@ WINDOW w AS (
     PATTERN (A+ ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {2,} ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1094,9 +1053,6 @@ WINDOW w AS (
     PATTERN (A{2,} ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
-
--- Invalid token combinations
 
 -- * + (invalid combination)
 SELECT COUNT(*) OVER w
@@ -1107,7 +1063,6 @@ WINDOW w AS (
     PATTERN (A* +)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- + * (invalid combination)
 SELECT COUNT(*) OVER w
@@ -1118,9 +1073,9 @@ WINDOW w AS (
     PATTERN (A+ *)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- ? ? (parsed as ?? reluctant quantifier)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1129,7 +1084,6 @@ WINDOW w AS (
     PATTERN (A? ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 DROP TABLE rpr_reluctant;
 
@@ -1138,7 +1092,7 @@ DROP TABLE rpr_reluctant;
 CREATE TABLE rpr_bounds (id INT);
 INSERT INTO rpr_bounds VALUES (1), (2);
 
--- min > max
+-- ERROR: quantifier lower bound must not exceed upper bound
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1147,7 +1101,6 @@ WINDOW w AS (
     PATTERN (A{5,3})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 
 -- Large bounds
 SELECT COUNT(*) OVER w
@@ -1179,7 +1132,7 @@ WINDOW w AS (
     DEFINE A AS id > 0
 );
 
--- INT_MAX = 2147483647 (over limit)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1188,11 +1141,10 @@ WINDOW w AS (
     PATTERN (A{2147483647})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 -- {n,} boundary errors
 
--- Negative lower bound in {n,}
+-- ERROR: negative lower bound in {n,} is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1201,9 +1153,8 @@ WINDOW w AS (
     PATTERN (A{-1,})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- INT_MAX in {n,}
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1212,7 +1163,6 @@ WINDOW w AS (
     PATTERN (A{2147483647,})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 
 -- {,m} boundary errors
 
@@ -1225,9 +1175,8 @@ WINDOW w AS (
     PATTERN (A{,0})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- INT_MAX in {,m}
+-- ERROR: quantifier upper bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1236,7 +1185,6 @@ WINDOW w AS (
     PATTERN (A{,2147483647})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 DROP TABLE rpr_bounds;
 
@@ -1244,7 +1192,6 @@ DROP TABLE rpr_bounds;
 -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
 -- ============================================================
 
-
 CREATE TABLE rpr_nav (id INT, val INT);
 INSERT INTO rpr_nav VALUES
     (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
@@ -1301,7 +1248,6 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
--- Expected: ERROR: cannot use prev outside a DEFINE clause
 
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
@@ -1315,7 +1261,6 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
--- Expected: ERROR: cannot use next outside a DEFINE clause
 
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1358,11 +1303,9 @@ ORDER BY id;
 
 -- FIRST function cannot be used other than in DEFINE
 SELECT FIRST(id), id, val FROM rpr_nav;
--- Expected: ERROR: cannot use first outside a DEFINE clause
 
 -- LAST function cannot be used other than in DEFINE
 SELECT LAST(id), id, val FROM rpr_nav;
--- Expected: ERROR: cannot use last outside a DEFINE clause
 
 DROP TABLE rpr_nav;
 
@@ -1370,7 +1313,6 @@ DROP TABLE rpr_nav;
 -- SKIP TO / INITIAL Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_skip (id INT, val INT);
 INSERT INTO rpr_skip VALUES
     (1, 1), (2, 2), (3, 3), (4, 4), (5, 5),
@@ -1492,15 +1434,15 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: SEEK is not supported
--- HINT: Use INITIAL instead.
 
 DROP TABLE rpr_seek;
 
 -- ============================================================
 -- Serialization/Deserialization Tests
 -- ============================================================
-
+-- RPR-defining views and tables here are intentionally left in place (not
+-- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse
+-- round-trip of the RPR window clause.
 
 -- View creation and deparsing
 
@@ -2190,13 +2132,10 @@ DROP TABLE rpr_glue;
 -- Error Cases Tests
 -- ============================================================
 
-
 DROP TABLE IF EXISTS rpr_err;
 CREATE TABLE rpr_err (id INT, val INT);
 INSERT INTO rpr_err VALUES (1, 10), (2, 20);
 
--- Syntax errors
-
 -- Invalid quantifier syntax
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2222,7 +2161,7 @@ EXCEPTION
 END $$;
 SET client_min_messages = WARNING;
 
--- Empty DEFINE
+-- ERROR: empty DEFINE not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2233,7 +2172,7 @@ WINDOW w AS (
 );
 -- Expected: Syntax error
 
--- Empty PATTERN
+-- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2244,7 +2183,7 @@ WINDOW w AS (
 );
 -- Expected: Syntax error
 
--- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+-- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2265,7 +2204,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
@@ -2276,7 +2214,6 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
@@ -2287,7 +2224,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0, B AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
@@ -2298,7 +2234,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
--- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
 
 -- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
 -- must fall through so that normal column resolution produces a sensible error.
@@ -2310,7 +2245,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS nosuch.val > 0
 );
--- Expected: ERROR: missing FROM-clause entry for table "nosuch"
 
 -- Unqualified composite field access in DEFINE works: no qualifier means no
 -- pattern/range-var navigation, so the pre-check skips and normal resolution
@@ -2340,7 +2274,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS (A.items).amount > 10
 );
--- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_composite
 WINDOW w AS (
@@ -2349,13 +2282,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS (rpr_composite.items).amount > 10
 );
--- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
 DROP TABLE rpr_composite;
 DROP TYPE rpr_item;
 
--- Semantic errors
-
--- Undefined column in DEFINE
+-- ERROR: undefined column in DEFINE
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2364,9 +2294,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS nonexistent_column > 0
 );
--- Expected: ERROR: column "nonexistent_column" does not exist
 
--- Type mismatch
+-- ERROR: type mismatch
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2375,9 +2304,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 'string'
 );
--- Expected: ERROR: invalid input syntax for type integer: "string"
 
--- Aggregate function in DEFINE (if not allowed)
+-- ERROR: aggregate function in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2386,9 +2314,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS COUNT(*) > 0
 );
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
--- Subquery in DEFINE (NOT SUPPORTED)
+-- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2397,9 +2324,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > (SELECT max(val) FROM rpr_err)
 );
--- Expected: ERROR: cannot use subquery in DEFINE expression
-
--- Edge cases
 
 -- Pattern variable not used (should work, extra vars ignored)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -3414,7 +3338,6 @@ WINDOW w AS (
     PATTERN ((A{2000000000,2147483647}){2})
     DEFINE A AS val > 0
 );
--- Expected: ERROR at parse time before optimization
 
 -- Test: nested unbounded with large min causes overflow fallback
 EXPLAIN (COSTS OFF)
@@ -3563,7 +3486,6 @@ WINDOW w AS (
 )
 GROUP BY category
 ORDER BY category;
--- Expected: ERROR: syntax error at or near "GROUP"
 -- (GROUP BY after WINDOW clause is not valid SQL syntax)
 
 -- ============================================================
@@ -3937,7 +3859,6 @@ INSERT INTO rpr_sort VALUES
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
 
 -- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
        COUNT(*) as group_cnt,
@@ -3954,7 +3875,6 @@ WINDOW w AS (
 ORDER BY category;
 
 -- RPR with HAVING (same aggregate-in-DEFINE error)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
        COUNT(*) as group_cnt,
@@ -4189,7 +4109,7 @@ WINDOW w AS (
 );
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
 
--- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
+-- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -4221,7 +4141,6 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
--- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
@@ -4247,14 +4166,12 @@ WINDOW w AS (
     PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR - pattern nesting too deep
 
 DROP TABLE rpr_errors;
 
 -- ============================================================
--- Jacob's Patterns
+-- Basic Pattern Matching
 -- ============================================================
--- Basic pattern matching tests from jacob branch
 
 -- Test: A? (optional, greedy)
 SELECT id, val, count(*) OVER w AS c
@@ -4453,3 +4370,4 @@ FROM (SELECT id, val,
 ) s;
 
 DROP TABLE rpr_plan;
+RESET client_min_messages;
-- 
2.50.1 (Apple Git-155)

From b010580c9b62244ba323520bb7202aa348c18195 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Mon, 8 Jun 2026 21:27:44 +0900
Subject: [PATCH 68/68] Use foreach_node and friends in row pattern recognition
 code

Replace the hand-written foreach()/lfirst() loops in the row pattern
recognition code with foreach_node(), foreach_ptr(), foreach_current_index()
and forboth(), dropping the now-unneeded ListCell declarations, casts and
manual index counters.  No behavior change.
---
 src/backend/executor/execRPR.c            | 10 +--
 src/backend/executor/nodeWindowAgg.c      | 12 +--
 src/backend/optimizer/path/allpaths.c     |  9 +--
 src/backend/optimizer/path/costsize.c     |  5 +-
 src/backend/optimizer/plan/createplan.c   |  5 +-
 src/backend/optimizer/plan/rpr.c          | 90 +++++++----------------
 src/backend/optimizer/prep/prepjointree.c |  4 +-
 src/backend/parser/parse_cte.c            |  3 +-
 src/backend/parser/parse_expr.c           |  5 +-
 src/backend/parser/parse_rpr.c            | 38 +++-------
 src/backend/utils/adt/ruleutils.c         | 18 ++---
 11 files changed, 62 insertions(+), 137 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1c89875a306..5ae54495968 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1595,8 +1595,6 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
 	int64		saved_match_start = winstate->nav_match_start;
 	int64		saved_pos = winstate->currentpos;
-	int			varIdx = 0;
-	ListCell   *lc;
 
 	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
 	winstate->nav_match_start = ctx->matchStartRow;
@@ -1605,11 +1603,12 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Invalidate nav_slot cache since match_start changed */
 	winstate->nav_slot_pos = -1;
 
-	foreach(lc, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
 	{
+		int			varIdx = foreach_current_index(exprState);
+
 		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
 		{
-			ExprState  *exprState = (ExprState *) lfirst(lc);
 			Datum		result;
 			bool		isnull;
 
@@ -1618,8 +1617,7 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
 		}
 
-		varIdx++;
-		if (varIdx >= list_length(winstate->defineVariableList))
+		if (varIdx + 1 >= list_length(winstate->defineVariableList))
 			break;
 	}
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 13661181986..15579ae5ea4 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3084,9 +3084,8 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		 * by EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so
 		 * no varno rewriting is needed here.
 		 */
-		foreach(l, node->defineClause)
+		foreach_node(TargetEntry, te, node->defineClause)
 		{
-			TargetEntry *te = lfirst(l);
 			char	   *name = te->resname;
 			Expr	   *expr = te->expr;
 			ExprState  *exps;
@@ -4173,7 +4172,6 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 {
 	EvalDefineOffsetsContext ctx;
 	NavTraversal trav;
-	ListCell   *lc;
 	bool		needsMax = (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
 	bool		needsFirst = (winstate->hasFirstNav &&
 							  winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
@@ -4189,10 +4187,8 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	trav.visit = visit_nav_exec;
 	trav.data = &ctx;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		nav_traversal_walker((Node *) te->expr, &trav);
 	}
 
@@ -4586,7 +4582,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	WindowAggState *winstate = winobj->winstate;
 	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
 	int			numDefineVars = list_length(winstate->defineVariableList);
-	ListCell   *lc;
 	int			varIdx = 0;
 	TupleTableSlot *slot;
 	int64		saved_pos;
@@ -4609,9 +4604,8 @@ 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(lc, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
 	{
-		ExprState  *exprState = (ExprState *) lfirst(lc);
 		Datum		result;
 		bool		isnull;
 
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 22339f7491f..f3c9f3c0bd6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4760,21 +4760,16 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		if (IsA(texpr, Var))
 		{
 			Var		   *var = (Var *) texpr;
-			ListCell   *wlc;
 			bool		needed_by_define = false;
 
-			foreach(wlc, subquery->windowClause)
+			foreach_node(WindowClause, wc, subquery->windowClause)
 			{
-				WindowClause *wc = lfirst_node(WindowClause, wlc);
-
 				if (wc->defineClause != NIL)
 				{
 					List	   *vars = pull_var_clause((Node *) wc->defineClause, 0);
-					ListCell   *vlc;
 
-					foreach(vlc, vars)
+					foreach_node(Var, dvar, vars)
 					{
-						Var		   *dvar = (Var *) lfirst(vlc);
 
 						/*
 						 * Match varno as well as varattno: a Var pulled from
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index b38cad9f121..82472c3fe96 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3236,14 +3236,13 @@ cost_windowagg(Path *path, PlannerInfo *root,
 	if (winclause->rpPattern)
 	{
 		List	   *pattern_vars;
-		ListCell   *lc2;
 		QualCost	defcosts;
 
 		pattern_vars = collectPatternVariables(winclause->rpPattern);
 
-		foreach(lc2, pattern_vars)
+		foreach_node(String, pv, pattern_vars)
 		{
-			char	   *ptname = strVal(lfirst(lc2));
+			char	   *ptname = strVal(pv);
 
 			foreach_node(TargetEntry, def, winclause->defineClause)
 			{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index d2e19d61d58..cca4126e511 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2736,7 +2736,6 @@ compute_define_metadata(List *defineClause,
 {
 	DefineMetadataContext ctx;
 	NavTraversal trav;
-	ListCell   *lc;
 
 	ctx.maxOffset = 0;
 	ctx.maxNeedsEval = false;
@@ -2750,10 +2749,8 @@ compute_define_metadata(List *defineClause,
 	trav.visit = visit_nav_plan;
 	trav.data = &ctx;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		nav_traversal_walker((Node *) te->expr, &trav);
 		ctx.curVarIdx++;
 	}
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index d768422deeb..664c942e575 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -191,12 +191,10 @@ tryUnwrapSingleChild(RPRPatternNode *pattern)
 static List *
 flattenSeqChildren(List *children)
 {
-	ListCell   *lc;
 	List	   *newChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		RPRPatternNode *opt = optimizeRPRPattern(child);
 
 		/* GROUP{1,1} should have been unwrapped by optimizeGroupPattern */
@@ -229,14 +227,11 @@ flattenSeqChildren(List *children)
 static List *
 mergeConsecutiveVars(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_VAR && child->reluctant == false)
 		{
 			/* ----------------------
@@ -306,14 +301,11 @@ mergeConsecutiveVars(List *children)
 static List *
 mergeConsecutiveGroups(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant == false)
 		{
 			/* ----------------------
@@ -385,15 +377,12 @@ mergeConsecutiveGroups(List *children)
 static List *
 mergeConsecutiveAlts(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 	int			count = 0;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_ALT && child->reluctant == false)
 		{
 			if (prev != NULL &&
@@ -702,12 +691,10 @@ optimizeSeqPattern(RPRPatternNode *pattern)
 static List *
 flattenAltChildren(List *children)
 {
-	ListCell   *lc;
 	List	   *newChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		RPRPatternNode *opt = optimizeRPRPattern(child);
 
 		if (opt->nodeType == RPR_PATTERN_ALT)
@@ -732,18 +719,15 @@ flattenAltChildren(List *children)
 static List *
 removeDuplicateAlternatives(List *children)
 {
-	ListCell   *lc;
-	ListCell   *lc2;
 	List	   *uniqueChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		bool		isDuplicate = false;
 
-		foreach(lc2, uniqueChildren)
+		foreach_node(RPRPatternNode, uchild, uniqueChildren)
 		{
-			if (rprPatternEqual((RPRPatternNode *) lfirst(lc2), child))
+			if (rprPatternEqual(uchild, child))
 			{
 				isDuplicate = true;
 				break;
@@ -940,16 +924,13 @@ tryUnwrapGroup(RPRPatternNode *pattern)
 static RPRPatternNode *
 optimizeGroupPattern(RPRPatternNode *pattern)
 {
-	ListCell   *lc;
 	List	   *newChildren;
 	RPRPatternNode *result;
 
 	/* Recursively optimize children */
 	newChildren = NIL;
-	foreach(lc, pattern->children)
+	foreach_node(RPRPatternNode, child, pattern->children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		newChildren = lappend(newChildren, optimizeRPRPattern(child));
 	}
 	pattern->children = newChildren;
@@ -1005,15 +986,14 @@ optimizeRPRPattern(RPRPatternNode *pattern)
 static int
 collectDefineVariables(List *defineVariableList, char **varNames)
 {
-	ListCell   *lc;
 	int			numVars = 0;
 
-	foreach(lc, defineVariableList)
+	foreach_node(String, varname, defineVariableList)
 	{
 		/* Parser already checked this limit in transformDefineClause */
 		Assert(numVars <= RPR_VARID_MAX);
 
-		varNames[numVars++] = strVal(lfirst(lc));
+		varNames[numVars++] = strVal(varname);
 	}
 
 	return numVars;
@@ -1031,7 +1011,6 @@ static void
 scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 						int *numElements, RPRDepth depth, RPRDepth *maxDepth)
 {
-	ListCell   *lc;
 	int			i;
 
 	/* Pattern nodes from parser are never NULL */
@@ -1075,9 +1054,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 
 		case RPR_PATTERN_SEQ:
 			/* Sequence: just recurse into children */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth, maxDepth);
 			}
 			break;
@@ -1088,9 +1067,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 				(*numElements)++;
 
 			/* Recurse into children at increased depth */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth + 1, maxDepth);
 			}
 
@@ -1104,9 +1083,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			(*numElements)++;
 
 			/* Recurse into children at increased depth */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth + 1, maxDepth);
 			}
 			break;
@@ -1248,7 +1227,6 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 static bool
 fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 {
-	ListCell   *lc;
 	int			groupStartIdx = *idx;
 	int			beginIdx = -1;
 	bool		bodyNullable = true;
@@ -1275,9 +1253,9 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		groupStartIdx = *idx;	/* children start after BEGIN */
 	}
 
-	foreach(lc, node->children)
+	foreach_node(RPRPatternNode, child, node->children)
 	{
-		if (!fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth + 1))
+		if (!fillRPRPattern(child, pat, idx, depth + 1))
 			bodyNullable = false;
 	}
 
@@ -1350,9 +1328,8 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	(*idx)++;
 
 	/* Fill each alternative */
-	foreach(lc, node->children)
+	foreach_node(RPRPatternNode, alt, node->children)
 	{
-		RPRPatternNode *alt = (RPRPatternNode *) lfirst(lc);
 		int			branchStart = *idx;
 
 		altBranchStarts = lappend_int(altBranchStarts, branchStart);
@@ -1372,9 +1349,8 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 
 	/* Set next on last element of each alternative to after the alternation */
 	afterAltIdx = *idx;
-	lc2 = list_head(altBranchStarts);
 
-	foreach(lc, altEndPositions)
+	forboth(lc, altEndPositions, lc2, altBranchStarts)
 	{
 		int			endPos = lfirst_int(lc);
 		int			branchStart = lfirst_int(lc2);
@@ -1399,8 +1375,6 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 		{
 			pat->elements[endPos].next = afterAltIdx;
 		}
-
-		lc2 = lnext(altBranchStarts, lc2);
 	}
 
 	list_free(altBranchStarts);
@@ -1422,7 +1396,6 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 static bool
 fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 {
-	ListCell   *lc;
 	bool		allNullable = true;
 
 	/* Pattern nodes from parser are never NULL */
@@ -1433,9 +1406,9 @@ fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 	switch (node->nodeType)
 	{
 		case RPR_PATTERN_SEQ:
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				if (!fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth))
+				if (!fillRPRPattern(child, pat, idx, depth))
 					allNullable = false;
 			}
 			return allNullable;
@@ -1856,8 +1829,6 @@ computeAbsorbability(RPRPattern *pattern)
 static void
 collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 {
-	ListCell   *lc;
-
 	Assert(node != NULL);
 
 	check_stack_depth();
@@ -1866,9 +1837,9 @@ collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 	{
 		case RPR_PATTERN_VAR:
 			/* Add variable if not already in list */
-			foreach(lc, *varNames)
+			foreach_node(String, varname, *varNames)
 			{
-				if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+				if (strcmp(strVal(varname), node->varName) == 0)
 					return;		/* Already collected */
 			}
 			*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
@@ -1877,10 +1848,9 @@ collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 		case RPR_PATTERN_SEQ:
 		case RPR_PATTERN_ALT:
 		case RPR_PATTERN_GROUP:
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				collectPatternVariablesRecursive((RPRPatternNode *) lfirst(lc),
-												 varNames);
+				collectPatternVariablesRecursive(child, varNames);
 			}
 			break;
 	}
@@ -1917,14 +1887,10 @@ collectPatternVariables(RPRPatternNode *pattern)
 void
 buildDefineVariableList(List *defineClause, List **defineVariableList)
 {
-	ListCell   *lc;
-
 	*defineVariableList = NIL;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		*defineVariableList = lappend(*defineVariableList,
 									  makeString(pstrdup(te->resname)));
 	}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 02898a9106b..74dc486b1f5 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2593,10 +2593,8 @@ perform_pullup_replace_vars(PlannerInfo *root,
 	parse->returningList = (List *)
 		pullup_replace_vars((Node *) parse->returningList, rvcontext);
 
-	foreach(lc, parse->windowClause)
+	foreach_node(WindowClause, wc, parse->windowClause)
 	{
-		WindowClause *wc = lfirst_node(WindowClause, lc);
-
 		if (wc->defineClause != NIL)
 			wc->defineClause = (List *)
 				pullup_replace_vars((Node *) wc->defineClause, rvcontext);
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index 0974b43d028..3e493beba0b 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -182,9 +182,8 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 		 * rewritten to WITH RECURSIVE by makeRecursiveViewSelect() and so
 		 * flows through here as well.
 		 */
-		foreach(lc, withClause->ctes)
+		foreach_node(CommonTableExpr, cte, withClause->ctes)
 		{
-			CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
 			ContainRPRContext ctx;
 
 			ctx.location = -1;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index f65a270d20e..0f7207577b3 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -652,12 +652,11 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		list_length(cref->fields) != 1)
 	{
 		char	   *qualifier = strVal(linitial(cref->fields));
-		ListCell   *lc;
 		bool		is_pattern_var = false;
 
-		foreach(lc, pstate->p_rpr_pattern_vars)
+		foreach_node(String, pv, pstate->p_rpr_pattern_vars)
 		{
-			if (strcmp(strVal(lfirst(lc)), qualifier) == 0)
+			if (strcmp(strVal(pv), qualifier) == 0)
 			{
 				is_pattern_var = true;
 				break;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 4e1d2650cf6..d12a0d45c94 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -208,8 +208,6 @@ static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 						   List *rpDefs, List **varNames)
 {
-	ListCell   *lc;
-
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
 
@@ -228,9 +226,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			{
 				bool		found = false;
 
-				foreach(lc, *varNames)
+				foreach_node(String, varname, *varNames)
 				{
-					if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+					if (strcmp(strVal(varname), node->varName) == 0)
 					{
 						found = true;
 						break;
@@ -261,10 +259,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 		case RPR_PATTERN_ALT:
 		case RPR_PATTERN_GROUP:
 			/* Recurse into children */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				validateRPRPatternVarCount(pstate, (RPRPatternNode *) lfirst(lc),
-										   NULL, varNames);
+				validateRPRPatternVarCount(pstate, child, NULL, varNames);
 			}
 			break;
 	}
@@ -277,15 +274,13 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	 */
 	if (rpDefs)
 	{
-		foreach(lc, rpDefs)
+		foreach_node(ResTarget, rt, rpDefs)
 		{
-			ResTarget  *rt = (ResTarget *) lfirst(lc);
-			ListCell   *lc2;
 			bool		found = false;
 
-			foreach(lc2, *varNames)
+			foreach_node(String, varname, *varNames)
 			{
-				if (strcmp(strVal(lfirst(lc2)), rt->name) == 0)
+				if (strcmp(strVal(varname), rt->name) == 0)
 				{
 					found = true;
 					break;
@@ -327,10 +322,6 @@ static List *
 transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 					  List **targetlist)
 {
-	ListCell   *lc,
-			   *l;
-	ResTarget  *restarget,
-			   *r;
 	List	   *restargets;
 	List	   *defineClause = NIL;
 	char	   *name;
@@ -357,18 +348,16 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	 * equivalent.
 	 */
 	restargets = NIL;
-	foreach(lc, windef->rpCommonSyntax->rpDefs)
+	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
 	{
 		TargetEntry *teDefine;
 
-		restarget = (ResTarget *) lfirst(lc);
 		name = restarget->name;
 
-		foreach(l, restargets)
+		foreach_node(ResTarget, r, restargets)
 		{
 			char	   *n;
 
-			r = (ResTarget *) lfirst(l);
 			n = r->name;
 
 			if (!strcmp(n, name))
@@ -394,7 +383,6 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		{
 			Node	   *expr;
 			List	   *vars;
-			ListCell   *lc2;
 
 			expr = transformExpr(pstate, restarget->val,
 								 EXPR_KIND_RPR_DEFINE);
@@ -407,16 +395,12 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			 * evaluation.
 			 */
 			vars = pull_var_clause(expr, 0);
-			foreach(lc2, vars)
+			foreach_node(Var, var, vars)
 			{
-				Var		   *var = (Var *) lfirst(lc2);
 				bool		found = false;
-				ListCell   *tl;
 
-				foreach(tl, *targetlist)
+				foreach_node(TargetEntry, tle, *targetlist)
 				{
-					TargetEntry *tle = (TargetEntry *) lfirst(tl);
-
 					if (IsA(tle->expr, Var) &&
 						((Var *) tle->expr)->varno == var->varno &&
 						((Var *) tle->expr)->varattno == var->varattno)
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index d588cd8263d..415da6417d4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7153,7 +7153,6 @@ static void
 get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
-	ListCell   *lc;
 	const char *sep;
 
 	Assert(node != NULL);
@@ -7167,20 +7166,20 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 
 		case RPR_PATTERN_SEQ:
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " ";
 			}
 			break;
 
 		case RPR_PATTERN_ALT:
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " | ";
 			}
 			break;
@@ -7188,10 +7187,10 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 		case RPR_PATTERN_GROUP:
 			appendStringInfoChar(buf, '(');
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " ";
 			}
 			appendStringInfoChar(buf, ')');
@@ -7221,14 +7220,11 @@ get_rule_define(List *defineClause, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
 	const char *sep;
-	ListCell   *lc_def;
 
 	sep = "  ";
 
-	foreach(lc_def, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc_def);
-
 		appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
 		get_rule_expr((Node *) te->expr, context, false);
 		sep = ",\n  ";
-- 
2.50.1 (Apple Git-155)

---

From: Tatsuo Ishii <[email-masked]>
Date: Tue, 09 Jun 2026 17:13:07 +0900 (JST)

Hi Henson,

> omit the DEFINE clause itself.

My mistake. the DEFINE clause is mandatory in R010 too. Sorry for
noise.

Regards,

---

From: jian he <[email-masked]>
Date: Tue, 9 Jun 2026 16:57:20 +0800

Hi.

The below in advanced.sgml actually seems better suited for
explain.sgml (which currently has no changes).
At the very least, explain.sgml should mention it.

   <para>
    When examining query plans for Row Pattern Recognition with
    <command>EXPLAIN</command>, the pattern output may include special
    markers that indicate optimization opportunities. A double quote
    <literal>"</literal> marks where pattern absorption can occur,
    and a single quote <literal>'</literal> marks absorbable elements
    within a branch. For example, <literal>a+"</literal> indicates that
    repeated matches of <literal>a</literal> can be absorbed, while
    <literal>(a' b')+"</literal> shows that both <literal>a</literal>
    and <literal>b</literal> within the group are absorbable.
    These markers are primarily useful for understanding internal
    optimization behavior.
   </para>

<programlisting>
......
 PATTERN (LOWPRICE UP+ DOWN+)
 DEFINE
  LOWPRICE AS price &lt;= 100,
  UP AS price &gt; PREV(price),
  DOWN AS price &lt; PREV(price)
);
</programlisting>
LOWPRICE, UP, DOWN should be lower case, since they are not keywords?

scanRPRPatternRecursive
It would be better to replace all  ``non-trivial quantifier`` to
``non-trivial quantifier (not {1,1}).``

I want to rename allocateRPRPattern to makeRPRPattern, what do you think?

RPRPatternNode.reluctant_location is not necessary. we never use it
for error reporting,
and since it's restricted to the end of a pattern, it adds very little value.

Is nav_volatile_func_checker really necessary?
We already have contain_volatile_functions and
contain_volatile_functions_not_nextval.
Also per https://www.postgresql.org/message-id/[email-masked]
The convention is not to check expression volatility during parse
analysis stage.

+bool        trailing_alt pg_node_attr(query_jumble_ignore);
I may check whether trailing_alt is necessary later,  but
query_jumble_ignore is not needed because it's only manipulated in
gram.y and we do JumbleQuery in parse_analyze_fixedparams, which
obviously is later than all gram.y related work.
----------------------------------------------
v47-0001-v47-rpr-reformat-gram.y-again.nocfbot
The HINT message below is somewhat misleading, since the question mark
(?) is actually after the range quantifier.
Change errmsg to errmsg("invalid token \"%s\" after range quantifier",
$5) will make the error message clearer.

+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1
FOLLOWING PATTERN (A {1,2}??) DEFINE A AS TRUE);
+ERROR:  invalid token "??" after range quantifier
+LINE 1: ...TWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A {1,2}??) DEFINE...
+                                                             ^
+HINT:  Only "?" is allowed after {n,m} to make it reluctant.
----------------------------------------------
v47-0002-v47-rpr-regress-tests-refactoring.nocfbot

-- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
-- Exercises the child->max == RPR_QUANTITY_INF branch in
mergeConsecutiveGroups,
--- where a finite prev ((A B){2,2}) meets an infinite child ((A B)+).

We should just drop these comments from the regress tests.
mergeConsecutiveGroups is a static function that might change, and we don't want
to have to update all the tests every time it does.  Also the comments meaning
is still totally clear without them.
----------------------------------------------
v47-0003-refactor-many-functions-in-rpr.c.nocfbot
I did some major refactoring in rpr.c. Instead of allocating new lists to
remove duplicates, we now just mutate them in place using list_delete_nth_cell
(a common pattern in the codebase).
for overflow handling, it would be better use pg_mul_s32_overflow,
pg_add_s32_overflow.
See the commit message for the details.

v47-0004-another-v47-misc-refactoring.nocfbot
Some misc refactoring, that seems to make sense to me, see the commit
message for rationale.

The above v47-0001, v47-0002, and v47-0003, v47-0004 are based on
https://github.com/assam258-5892/postgres
latest commit.

---

From: Henson Choi <[email-masked]>
Date: Wed, 10 Jun 2026 00:15:31 +0900

Re: Row pattern recognition
To: jian he, Tatsuo Ishii (+ pgsql-hackers)

Hi jian, Tatsuo,

Thanks for the careful review -- replies inline. One question up front,
for Tatsuo: I'd like your take on the LOWPRICE/UP/DOWN casing below
(details at that point). The rest are replies to jian's points.

> At the very least, explain.sgml should mention it.

Agreed; I'll move the EXPLAIN marker description over to explain.sgml.

> LOWPRICE, UP, DOWN should be lower case, since they are not keywords?

They're user identifiers, and the standard and Oracle docs show them in
upper case, so I lean toward leaving the example as written -- though
EXPLAIN and deparse do lower-case them today. Tatsuo, I'd defer the
casing to you.

> ``non-trivial quantifier (not {1,1}).``

Agreed, that's clearer; will do.

> I want to rename allocateRPRPattern to makeRPRPattern, what do you think?

Good idea -- makeRPRPattern fits the makeNode/makeXxx convention.

> RPRPatternNode.reluctant_location is not necessary.

Agreed. The field is only written and propagated, never read back, so
I'll drop it and keep the reluctance as a plain bool.

> Is nav_volatile_func_checker really necessary? ... The convention is
> not to check expression volatility during parse analysis stage.

Good point -- I'll move the volatility rejection out of parse analysis
and into the planner, in line with that convention.

> gram.y ...

Right; I'll drop the query_jumble_ignore attribute.

> v47-0001 ... Change errmsg to errmsg("invalid token \"%s\" after range
> quantifier", $5) will make the error message clearer.

Agreed, that reads better; I'll adjust the message accordingly.

> v47-0002 ... We should just drop these comments from the regress tests.

Agreed -- the internal function names don't belong in the tests; I'll
remove them.

> v47-0003 ... mutate them in place using list_delete_nth_cell ...
> for overflow handling ... pg_mul_s32_overflow, pg_add_s32_overflow.

The overflow helpers are a clear win. The in-place list mutation is
behavior-equivalent, but I'd rather not reshape that logic right now
without a concrete need, so I'll hold off on it for the moment. The new
tests are good and I'll fold them in.

More broadly, I see the patch as being in its final verification stage
at this point, so I'd prefer to limit logic changes to those backed by a
clear review or test finding.

> v47-0004 ... Some misc refactoring ...

Mostly agree -- the rpSkipTo/initial comments read more accurately as
flag assignments. I'd keep the "PATTERN AST" wording (it's an AST, not a
flag) and the deparse note, but the rest is fine.

Best,
Henson

---

From: Henson Choi <[email-masked]>
Date: Wed, 10 Jun 2026 15:10:19 +0900

Hi Tatsuo, Jian,

While doing a self-review pass over the incremental fixes on top of v47 I
ran into two issues where I'd rather agree on an approach with you before
I pick one.  One of them is a regression I introduced myself in the DEFINE
memory-leak fix; the other is an original design point from v47.  There is
also a third bug which I plan to handle together with the second one, since
it can be affected by that change -- I describe it at the end.

I have verified both of the issues below on an assert-enabled build (and a
non-assert build where relevant).

== 1. DEFINE evaluation reuses the per-output-tuple context
(use-after-free) ==

nocfbot-0039 (the DEFINE memory-leak fix) added a ResetExprContext() in
update_reduced_frame, but it resets the wrong context.

ps_ExprContext is the per-output-tuple context that ExecWindowAgg resets
once per output row.  update_reduced_frame now resets it once per NFA row,
while the output row is still being formed -- so a pass-by-ref window
function result already datum-copied into that per-tuple memory (when
numfuncs > 1) is freed before ExecProject reads it.

Minimal trigger -- a pass-by-ref window function plus a second one over an
RPR window:

  SELECT lag(company) OVER w, count(*) OVER w FROM stock
   WINDOW w AS (PARTITION BY company ORDER BY tdate
                ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
                AFTER MATCH SKIP PAST LAST ROW INITIAL
                PATTERN (START UP+)
                DEFINE START AS TRUE, UP AS price > PREV(price));

On a CLOBBER_FREED_MEMORY build the lag column comes out as 0x7F garbage;
in production it is garbage or a crash.  (An aggregate is not required --
lag + first_value hits the same reset via the frame-access path.)

Neither v47 nor the patch is the answer on its own: v47 had no reset here,
so no use-after-free, but the DEFINE scratch accumulated over the whole
forward scan (the leak nocfbot-0039 fixed); nocfbot-0039 added the per-row
reset but on the shared per-output-tuple context.  We do want a per-row
reset -- just not on that context.

So I think this needs a dedicated ExprContext for DEFINE evaluation, reset
once per NFA row: it keeps the memory bounded without touching the
per-output-tuple results.

Question: does a dedicated DEFINE ExprContext look right to you?

== 2. PREV/NEXT/FIRST/LAST placeholders collide with user functions ==

The nav operations are polymorphic pg_catalog functions (anyelement, OIDs
8126-8133) recognized by funcid in parse_func.c, which collides with
same-name user functions.

Outside DEFINE, a same-name function masks or clashes with the placeholder:
with public.last(anyelement), SELECT last(123) fails "cannot use last
outside a DEFINE clause"; with public.next(numeric), SELECT next(10) fails
"function next(integer) is not unique"; and even with no user function,
last(123) errors instead of "function last(integer) does not exist".

Inside DEFINE, a same-name function with an exact-type match beats the
anyelement placeholder, so PREV(price) silently becomes a plain FuncExpr
instead of an RPRNavExpr -- a wrong match result with no error (reproduced
for numeric, text and int).  And ruleutils deparses a bare PREV(, so
reparsing a view under a search_path with public.prev rebinds it (pg_dump
is safe via search_path = '').

This is original v47 design, not a regression.  Per the standard,
PREV/NEXT/FIRST/LAST are navigation operations with dedicated syntax, not
general-namespace functions -- the collision comes from mapping them onto
catalog functions plus search-path resolution.

I haven't found a clean approach yet.  Inside DEFINE these names have to be
the navigation operation (per the standard), yet outside DEFINE they
shouldn't shadow or break same-name user functions the way the catalog
placeholders do -- and since the deparse output is unqualified (a bare
PREV(...)), whatever we choose also has to round-trip cleanly.  I'm not
sure how best to reconcile those.

My rough leaning is to not add catalog functions for these at all: leave
resolution outside DEFINE exactly as it is today, and only inside DEFINE
adjust the function-resolution path itself to recognize the navigation
operations.  But that is still quite abstract.

Question: how would you approach this?

== Note: a third bug, to be handled together with item 2 ==

A navigation operation nested inside another nav's offset argument -- e.g.
PREV(price, NEXT(2::bigint, 0)) in a DEFINE clause -- slips past the parser
but trips Assert(!IsA(nav->offset_arg, RPRNavExpr)) in the planner.  So it
aborts at plan time on an assert build; without asserts, the backward form
PREV(price, PREV(2::bigint, 5)) reaches a runtime "cannot fetch row ...
before mark position".

The fix is to reject a nav inside an offset argument in the DEFINE walk.
But since item 2 may reshape that walker substantially from how it works
today, I'll do it together with item 2 and add it as a regression test
there.

Thanks,
Henson